yangfei 3 days ago
commit
0cf8f9b3f5
100 changed files with 25000 additions and 0 deletions
  1. 136 0
      deploy-simple.bat
  2. 320 0
      deploy-to-server.ps1
  3. 135 0
      deploy.bat
  4. 24 0
      mqtt-vue-dashboard/.gitignore
  5. 13 0
      mqtt-vue-dashboard/index.html
  6. 2333 0
      mqtt-vue-dashboard/package-lock.json
  7. 32 0
      mqtt-vue-dashboard/package.json
  8. 0 0
      mqtt-vue-dashboard/public/favicon.svg
  9. 24 0
      mqtt-vue-dashboard/public/icons.svg
  10. 13 0
      mqtt-vue-dashboard/server/.env
  11. 13 0
      mqtt-vue-dashboard/server/.env.production
  12. 384 0
      mqtt-vue-dashboard/server/database/init.sql
  13. 30 0
      mqtt-vue-dashboard/server/ecosystem.config.js
  14. 10 0
      mqtt-vue-dashboard/server/migrations/add_rssi_to_devices.sql
  15. 47 0
      mqtt-vue-dashboard/server/migrations/create_device_logs_view.sql
  16. 18 0
      mqtt-vue-dashboard/server/migrations/create_system_logs_table.sql
  17. 15 0
      mqtt-vue-dashboard/server/migrations/create_wifi_configurations_table.sql
  18. 3351 0
      mqtt-vue-dashboard/server/package-lock.json
  19. 42 0
      mqtt-vue-dashboard/server/package.json
  20. 68 0
      mqtt-vue-dashboard/server/src/config/database.ts
  21. 480 0
      mqtt-vue-dashboard/server/src/controllers/authController.ts
  22. 533 0
      mqtt-vue-dashboard/server/src/controllers/authLogController.ts
  23. 441 0
      mqtt-vue-dashboard/server/src/controllers/clientAclController.ts
  24. 1276 0
      mqtt-vue-dashboard/server/src/controllers/clientAuthController.ts
  25. 201 0
      mqtt-vue-dashboard/server/src/controllers/clientConnectionController.ts
  26. 213 0
      mqtt-vue-dashboard/server/src/controllers/dashboardController.ts
  27. 270 0
      mqtt-vue-dashboard/server/src/controllers/deviceBindingController.ts
  28. 832 0
      mqtt-vue-dashboard/server/src/controllers/deviceController.ts
  29. 338 0
      mqtt-vue-dashboard/server/src/controllers/deviceLogController.ts
  30. 89 0
      mqtt-vue-dashboard/server/src/controllers/mqttBrokerController.ts
  31. 380 0
      mqtt-vue-dashboard/server/src/controllers/mqttMessageController.ts
  32. 897 0
      mqtt-vue-dashboard/server/src/controllers/otaController.ts
  33. 175 0
      mqtt-vue-dashboard/server/src/controllers/permissionController.ts
  34. 260 0
      mqtt-vue-dashboard/server/src/controllers/roomController.ts
  35. 263 0
      mqtt-vue-dashboard/server/src/controllers/roomDeviceController.ts
  36. 188 0
      mqtt-vue-dashboard/server/src/controllers/sensorDataController.ts
  37. 474 0
      mqtt-vue-dashboard/server/src/controllers/systemLogController.ts
  38. 126 0
      mqtt-vue-dashboard/server/src/database/connection.ts
  39. 23 0
      mqtt-vue-dashboard/server/src/database/schema.sql
  40. 149 0
      mqtt-vue-dashboard/server/src/index.ts
  41. 240 0
      mqtt-vue-dashboard/server/src/middleware/auth.ts
  42. 131 0
      mqtt-vue-dashboard/server/src/middleware/errorHandler.ts
  43. 91 0
      mqtt-vue-dashboard/server/src/middleware/requestLogger.ts
  44. 55 0
      mqtt-vue-dashboard/server/src/middleware/uploadMiddleware.ts
  45. 567 0
      mqtt-vue-dashboard/server/src/models/authLog.ts
  46. 188 0
      mqtt-vue-dashboard/server/src/models/clientAcl.ts
  47. 1035 0
      mqtt-vue-dashboard/server/src/models/clientAuth.ts
  48. 239 0
      mqtt-vue-dashboard/server/src/models/clientConnection.ts
  49. 269 0
      mqtt-vue-dashboard/server/src/models/device.ts
  50. 437 0
      mqtt-vue-dashboard/server/src/models/deviceBinding.ts
  51. 191 0
      mqtt-vue-dashboard/server/src/models/deviceLog.ts
  52. 97 0
      mqtt-vue-dashboard/server/src/models/firmware.ts
  53. 207 0
      mqtt-vue-dashboard/server/src/models/mqttMessage.ts
  54. 127 0
      mqtt-vue-dashboard/server/src/models/ota.ts
  55. 235 0
      mqtt-vue-dashboard/server/src/models/permission.ts
  56. 146 0
      mqtt-vue-dashboard/server/src/models/sensorData.ts
  57. 498 0
      mqtt-vue-dashboard/server/src/models/systemLog.ts
  58. 240 0
      mqtt-vue-dashboard/server/src/models/user.ts
  59. 130 0
      mqtt-vue-dashboard/server/src/models/wifiConfig.ts
  60. 42 0
      mqtt-vue-dashboard/server/src/routes/authLogRoutes.ts
  61. 33 0
      mqtt-vue-dashboard/server/src/routes/authRoutes.ts
  62. 39 0
      mqtt-vue-dashboard/server/src/routes/clientAclRoutes.ts
  63. 59 0
      mqtt-vue-dashboard/server/src/routes/clientAuthRoutes.ts
  64. 24 0
      mqtt-vue-dashboard/server/src/routes/clientConnectionRoutes.ts
  65. 15 0
      mqtt-vue-dashboard/server/src/routes/dashboardRoutes.ts
  66. 46 0
      mqtt-vue-dashboard/server/src/routes/deviceBindingRoutes.ts
  67. 17 0
      mqtt-vue-dashboard/server/src/routes/deviceLogRoutes.ts
  68. 52 0
      mqtt-vue-dashboard/server/src/routes/deviceRoutes.ts
  69. 44 0
      mqtt-vue-dashboard/server/src/routes/index.ts
  70. 11 0
      mqtt-vue-dashboard/server/src/routes/mqttBrokerRoutes.ts
  71. 40 0
      mqtt-vue-dashboard/server/src/routes/mqttMessageRoutes.ts
  72. 24 0
      mqtt-vue-dashboard/server/src/routes/otaRoutes.ts
  73. 30 0
      mqtt-vue-dashboard/server/src/routes/permissionRoutes.ts
  74. 43 0
      mqtt-vue-dashboard/server/src/routes/roomDeviceRoutes.ts
  75. 35 0
      mqtt-vue-dashboard/server/src/routes/roomRoutes.ts
  76. 16 0
      mqtt-vue-dashboard/server/src/routes/sensorDataRoutes.ts
  77. 69 0
      mqtt-vue-dashboard/server/src/routes/syncRoutes.ts
  78. 39 0
      mqtt-vue-dashboard/server/src/routes/systemLogRoutes.ts
  79. 119 0
      mqtt-vue-dashboard/server/src/routes/wifiConfigRoutes.ts
  80. 162 0
      mqtt-vue-dashboard/server/src/services/dataSyncService.ts
  81. 97 0
      mqtt-vue-dashboard/server/src/services/loggerService.ts
  82. 852 0
      mqtt-vue-dashboard/server/src/services/mqttBrokerService.ts
  83. 1174 0
      mqtt-vue-dashboard/server/src/services/websocketService.ts
  84. 39 0
      mqtt-vue-dashboard/server/src/types/global.d.ts
  85. 71 0
      mqtt-vue-dashboard/server/src/utils/fileUtils.ts
  86. 226 0
      mqtt-vue-dashboard/server/src/utils/helpers.ts
  87. 39 0
      mqtt-vue-dashboard/server/tsconfig.json
  88. 146 0
      mqtt-vue-dashboard/src/App.vue
  89. 0 0
      mqtt-vue-dashboard/src/assets/vite.svg
  90. 41 0
      mqtt-vue-dashboard/src/composables/useRealtimeData.ts
  91. 5 0
      mqtt-vue-dashboard/src/env.d.ts
  92. 588 0
      mqtt-vue-dashboard/src/layouts/AppLayout.vue
  93. 15 0
      mqtt-vue-dashboard/src/main.ts
  94. 134 0
      mqtt-vue-dashboard/src/router/index.ts
  95. 298 0
      mqtt-vue-dashboard/src/services/api.ts
  96. 214 0
      mqtt-vue-dashboard/src/services/websocket.ts
  97. 147 0
      mqtt-vue-dashboard/src/stores/auth.ts
  98. 101 0
      mqtt-vue-dashboard/src/stores/theme.ts
  99. 55 0
      mqtt-vue-dashboard/src/stores/websocket.ts
  100. 60 0
      mqtt-vue-dashboard/src/types/auth.ts

+ 136 - 0
deploy-simple.bat

@@ -0,0 +1,136 @@
+@echo off
+chcp 65001 > nul
+echo ========================================
+echo    MQTT Project Deployment to Server
+echo ========================================
+echo.
+
+echo Step 1: Check server connection...
+ping 192.168.1.17 -n 2 > nul
+if %errorlevel% equ 0 (
+    echo   Server connection OK
+) else (
+    echo   Server connection failed
+    exit /b 1
+)
+
+echo.
+echo Step 2: Create directory structure on server...
+ssh yangfei@192.168.1.17 "mkdir -p /home/yangfei/mqtt-vue-dashboard/backend"
+ssh yangfei@192.168.1.17 "mkdir -p /home/yangfei/mqtt-vue-dashboard/frontend"
+ssh yangfei@192.168.1.17 "mkdir -p /home/yangfei/mqtt-vue-dashboard/logs"
+echo   Directories created
+
+echo.
+echo Step 3: Build frontend project...
+cd mqtt-vue-dashboard
+if exist node_modules (
+    echo   Frontend dependencies exist, skipping install
+) else (
+    echo   Installing frontend dependencies...
+    call npm install
+    if %errorlevel% neq 0 (
+        echo   Frontend dependency installation failed
+        exit /b 1
+    )
+)
+
+echo   Building frontend...
+call npm run build
+if %errorlevel% neq 0 (
+    echo   Frontend build failed
+    exit /b 1
+)
+
+echo.
+echo Step 4: Upload frontend files to server...
+if exist dist (
+    scp -r dist yangfei@192.168.1.17:/home/yangfei/mqtt-vue-dashboard/frontend/
+    if %errorlevel% equ 0 (
+        echo   Frontend files uploaded successfully
+    ) else (
+        echo   Frontend file upload failed
+        exit /b 1
+    )
+) else (
+    echo   Frontend build directory does not exist
+    exit /b 1
+)
+
+echo.
+echo Step 5: Build backend project...
+cd server
+if exist node_modules (
+    echo   Backend dependencies exist, skipping install
+) else (
+    echo   Installing backend dependencies...
+    call npm install
+    if %errorlevel% neq 0 (
+        echo   Backend dependency installation failed
+        exit /b 1
+    )
+)
+
+echo   Building backend...
+call npm run build
+if %errorlevel% neq 0 (
+    echo   Backend build failed
+    exit /b 1
+)
+
+echo.
+echo Step 6: Upload backend files to server...
+if exist dist (
+    scp -r dist yangfei@192.168.1.17:/home/yangfei/mqtt-vue-dashboard/backend/
+    echo   Backend code uploaded successfully
+)
+
+if exist package.json (
+    scp package.json yangfei@192.168.1.17:/home/yangfei/mqtt-vue-dashboard/backend/
+    echo   Configuration file uploaded successfully
+)
+
+if exist package-lock.json (
+    scp package-lock.json yangfei@192.168.1.17:/home/yangfei/mqtt-vue-dashboard/backend/
+    echo   Dependency file uploaded successfully
+)
+
+echo.
+echo Step 7: Install dependencies on server...
+ssh yangfei@192.168.1.17 "cd /home/yangfei/mqtt-vue-dashboard/backend && npm install --production"
+echo   Server dependencies installed
+
+echo.
+echo Step 8: Start backend service...
+ssh yangfei@192.168.1.17 "cd /home/yangfei/mqtt-vue-dashboard/backend && nohup npm start > ../logs/backend.log 2>&1 &"
+echo   Backend service started
+
+echo.
+echo Step 9: Verify deployment...
+ssh yangfei@192.168.1.17 "ls -la /home/yangfei/mqtt-vue-dashboard/frontend/dist/" > nul 2>&1
+if %errorlevel% equ 0 (
+    echo   Frontend files verified successfully
+) else (
+    echo   Frontend file verification failed
+)
+
+ssh yangfei@192.168.1.17 "ps aux | grep 'node.*backend' | grep -v grep" > nul 2>&1
+if %errorlevel% equ 0 (
+    echo   Backend service is running normally
+) else (
+    echo   Backend service is not running
+)
+
+echo.
+echo ========================================
+echo   Deployment Completed!
+echo ========================================
+echo.
+echo Access Information:
+echo   Backend API: http://192.168.1.17:3002
+echo   Frontend App: http://192.168.1.17
+echo.
+echo View logs:
+echo   ssh yangfei@192.168.1.17 'tail -f /home/yangfei/mqtt-vue-dashboard/logs/backend.log'
+echo.
+pause

+ 320 - 0
deploy-to-server.ps1

@@ -0,0 +1,320 @@
+# MQTT项目部署脚本
+# 将项目部署到树莓派服务器 (192.168.1.17)
+
+param(
+    [string]$ServerIP = "192.168.1.17",
+    [string]$Username = "yangfei",
+    [string]$Password = "yangfei",
+    [string]$ProjectPath = "/home/yangfei/mqtt-vue-dashboard"
+)
+
+# 颜色定义
+$Green = "\u001b[32m"
+$Red = "\u001b[31m"
+$Yellow = "\u001b[33m"
+$Reset = "\u001b[0m"
+
+function Write-Color {
+    param([string]$Color, [string]$Message)
+    Write-Host "$Color$Message$Reset"
+}
+
+function Write-Success { param([string]$Message) Write-Color -Color $Green -Message $Message }
+function Write-Error { param([string]$Message) Write-Color -Color $Red -Message $Message }
+function Write-Warning { param([string]$Message) Write-Color -Color $Yellow -Message $Message }
+
+function Test-SSHConnection {
+    Write-Host "测试SSH连接..."
+    try {
+        $result = ssh ${Username}@${ServerIP} "echo '连接成功'"
+        if ($LASTEXITCODE -eq 0) {
+            Write-Success "SSH连接测试成功"
+            return $true
+        } else {
+            Write-Error "SSH连接失败"
+            return $false
+        }
+    } catch {
+        Write-Error "SSH连接异常: $_"
+        return $false
+    }
+}
+
+function Prepare-Server {
+    Write-Host "准备服务器环境..."
+    
+    # 在服务器上创建项目目录
+    $commands = @(
+        "mkdir -p $ProjectPath",
+        "mkdir -p $ProjectPath/backend",
+        "mkdir -p $ProjectPath/frontend",
+        "mkdir -p $ProjectPath/logs"
+    )
+    
+    foreach ($cmd in $commands) {
+        ssh ${Username}@${ServerIP} $cmd
+        if ($LASTEXITCODE -ne 0) {
+            Write-Error "服务器准备失败: $cmd"
+            return $false
+        }
+    }
+    
+    Write-Success "服务器环境准备完成"
+    return $true
+}
+
+function Deploy-Backend {
+    Write-Host "部署后端代码..."
+    
+    # 构建后端
+    Set-Location "mqtt-vue-dashboard\server"
+    
+    Write-Host "安装后端依赖..."
+    npm install
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "后端依赖安装失败"
+        return $false
+    }
+    
+    Write-Host "构建后端..."
+    npm run build
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "后端构建失败"
+        return $false
+    }
+    
+    # 复制后端文件到服务器
+    Write-Host "上传后端文件到服务器..."
+    $backendFiles = @(
+        "dist",
+        "package.json",
+        "package-lock.json",
+        ".env",
+        ".env.production"
+    )
+    
+    foreach ($file in $backendFiles) {
+        if (Test-Path $file) {
+            scp -r $file ${Username}@${ServerIP}:${ProjectPath}/backend/
+            if ($LASTEXITCODE -ne 0) {
+                Write-Error "后端文件上传失败: $file"
+                return $false
+            }
+        }
+    }
+    
+    Set-Location "..\.."
+    Write-Success "后端部署完成"
+    return $true
+}
+
+function Deploy-Frontend {
+    Write-Host "部署前端代码..."
+    
+    # 构建前端
+    Set-Location "mqtt-vue-dashboard"
+    
+    Write-Host "安装前端依赖..."
+    npm install
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "前端依赖安装失败"
+        return $false
+    }
+    
+    Write-Host "构建前端..."
+    npm run build
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "前端构建失败"
+        return $false
+    }
+    
+    # 复制前端文件到服务器
+    Write-Host "上传前端文件到服务器..."
+    if (Test-Path "dist") {
+        scp -r dist ${Username}@${ServerIP}:${ProjectPath}/frontend/
+        if ($LASTEXITCODE -ne 0) {
+            Write-Error "前端文件上传失败"
+            return $false
+        }
+    }
+    
+    Set-Location ".."
+    Write-Success "前端部署完成"
+    return $true
+}
+
+function Setup-Server {
+    Write-Host "在服务器上设置项目..."
+    
+    # 在服务器上安装依赖和启动服务
+    $setupScript = @"
+cd $ProjectPath/backend
+npm install --production
+
+# 创建启动脚本
+cat > $ProjectPath/start-backend.sh << 'EOF'
+#!/bin/bash
+cd $ProjectPath/backend
+npm start
+EOF
+
+chmod +x $ProjectPath/start-backend.sh
+
+# 创建nginx配置(如果需要)
+cat > $ProjectPath/nginx.conf << 'EOF'
+server {
+    listen 80;
+    server_name localhost;
+    
+    # 前端静态文件
+    location / {
+        root $ProjectPath/frontend/dist;
+        index index.html;
+        try_files \$uri \$uri/ /index.html;
+    }
+    
+    # 后端API代理
+    location /api {
+        proxy_pass http://localhost:3002;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade \$http_upgrade;
+        proxy_set_header Connection 'upgrade';
+        proxy_set_header Host \$host;
+        proxy_cache_bypass \$http_upgrade;
+    }
+    
+    # WebSocket代理
+    location /socket.io {
+        proxy_pass http://localhost:3002;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade \$http_upgrade;
+        proxy_set_header Connection 'upgrade';
+        proxy_set_header Host \$host;
+        proxy_cache_bypass \$http_upgrade;
+    }
+}
+EOF
+"@
+    
+    # 执行设置脚本
+    ssh ${Username}@${ServerIP} "bash -c '$setupScript'"
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "服务器设置失败"
+        return $false
+    }
+    
+    Write-Success "服务器设置完成"
+    return $true
+}
+
+function Start-Services {
+    Write-Host "启动服务..."
+    
+    # 启动后端服务
+    $startScript = @"
+# 检查是否已有进程在运行
+pkill -f "node.*backend"
+
+# 启动后端服务
+cd $ProjectPath/backend
+nohup npm start > $ProjectPath/logs/backend.log 2>&1 &
+
+# 等待服务启动
+sleep 5
+
+# 检查服务是否正常启动
+if curl -s http://localhost:3002/api/health > /dev/null; then
+    echo "后端服务启动成功"
+else
+    echo "后端服务启动失败"
+    exit 1
+fi
+"@
+    
+    ssh ${Username}@${ServerIP} "bash -c '$startScript'"
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "服务启动失败"
+        return $false
+    }
+    
+    Write-Success "服务启动完成"
+    return $true
+}
+
+function Verify-Deployment {
+    Write-Host "验证部署结果..."
+    
+    # 检查后端服务
+    $healthCheck = ssh ${Username}@${ServerIP} "curl -s http://localhost:3002/api/health || echo '服务未启动'"
+    if ($healthCheck -match "服务未启动") {
+        Write-Error "后端服务未正常运行"
+        return $false
+    }
+    
+    Write-Success "后端服务运行正常"
+    
+    # 检查前端文件
+    $frontendCheck = ssh ${Username}@${ServerIP} "ls -la $ProjectPath/frontend/dist/"
+    if ($LASTEXITCODE -ne 0) {
+        Write-Error "前端文件部署失败"
+        return $false
+    }
+    
+    Write-Success "前端文件部署成功"
+    Write-Success "部署验证完成"
+    return $true
+}
+
+# 主部署流程
+Write-Host "开始部署MQTT项目到服务器 $ServerIP" -ForegroundColor Cyan
+
+# 1. 测试连接
+if (-not (Test-SSHConnection)) {
+    Write-Error "部署失败:无法连接到服务器"
+    exit 1
+}
+
+# 2. 准备服务器环境
+if (-not (Prepare-Server)) {
+    Write-Error "部署失败:服务器环境准备失败"
+    exit 1
+}
+
+# 3. 部署后端
+if (-not (Deploy-Backend)) {
+    Write-Error "部署失败:后端部署失败"
+    exit 1
+}
+
+# 4. 部署前端
+if (-not (Deploy-Frontend)) {
+    Write-Error "部署失败:前端部署失败"
+    exit 1
+}
+
+# 5. 服务器设置
+if (-not (Setup-Server)) {
+    Write-Error "部署失败:服务器设置失败"
+    exit 1
+}
+
+# 6. 启动服务
+if (-not (Start-Services)) {
+    Write-Error "部署失败:服务启动失败"
+    exit 1
+}
+
+# 7. 验证部署
+if (-not (Verify-Deployment)) {
+    Write-Error "部署失败:验证失败"
+    exit 1
+}
+
+Write-Success "部署完成!"
+Write-Host ""
+Write-Host "访问地址:" -ForegroundColor Yellow
+Write-Host "  后端API: http://$ServerIP`:3002" -ForegroundColor White
+Write-Host "  前端应用: http://$ServerIP`" -ForegroundColor White
+Write-Host ""
+Write-Host "查看日志:" -ForegroundColor Yellow
+Write-Host "  后端日志: ssh $Username@$ServerIP 'tail -f $ProjectPath/logs/backend.log'" -ForegroundColor White

+ 135 - 0
deploy.bat

@@ -0,0 +1,135 @@
+@echo off
+echo ========================================
+echo    MQTT项目部署到服务器 192.168.1.17
+echo ========================================
+echo.
+
+echo 步骤1: 检查服务器连接...
+ping 192.168.1.17 -n 2 > nul
+if %errorlevel% equ 0 (
+    echo   服务器连接正常
+) else (
+    echo   服务器连接失败
+    exit /b 1
+)
+
+echo.
+echo 步骤2: 在服务器上创建目录结构...
+ssh yangfei@192.168.1.17 "mkdir -p /home/yangfei/mqtt-vue-dashboard/backend"
+ssh yangfei@192.168.1.17 "mkdir -p /home/yangfei/mqtt-vue-dashboard/frontend"
+ssh yangfei@192.168.1.17 "mkdir -p /home/yangfei/mqtt-vue-dashboard/logs"
+echo   目录创建完成
+
+echo.
+echo 步骤3: 构建前端项目...
+cd mqtt-vue-dashboard
+if exist node_modules (
+    echo   前端依赖已存在,跳过安装
+) else (
+    echo   安装前端依赖...
+    npm install
+    if %errorlevel% neq 0 (
+        echo   前端依赖安装失败
+        exit /b 1
+    )
+)
+
+echo   构建前端...
+npm run build
+if %errorlevel% neq 0 (
+    echo   前端构建失败
+    exit /b 1
+)
+
+echo.
+echo 步骤4: 上传前端文件到服务器...
+if exist dist (
+    scp -r dist yangfei@192.168.1.17:/home/yangfei/mqtt-vue-dashboard/frontend/
+    if %errorlevel% equ 0 (
+        echo   前端文件上传成功
+    ) else (
+        echo   前端文件上传失败
+        exit /b 1
+    )
+) else (
+    echo   前端构建目录不存在
+    exit /b 1
+)
+
+echo.
+echo 步骤5: 构建后端项目...
+cd server
+if exist node_modules (
+    echo   后端依赖已存在,跳过安装
+) else (
+    echo   安装后端依赖...
+    npm install
+    if %errorlevel% neq 0 (
+        echo   后端依赖安装失败
+        exit /b 1
+    )
+)
+
+echo   构建后端...
+npm run build
+if %errorlevel% neq 0 (
+    echo   后端构建失败
+    exit /b 1
+)
+
+echo.
+echo 步骤6: 上传后端文件到服务器...
+if exist dist (
+    scp -r dist yangfei@192.168.1.17:/home/yangfei/mqtt-vue-dashboard/backend/
+    echo   后端代码上传成功
+)
+
+if exist package.json (
+    scp package.json yangfei@192.168.1.17:/home/yangfei/mqtt-vue-dashboard/backend/
+    echo   配置文件上传成功
+)
+
+if exist package-lock.json (
+    scp package-lock.json yangfei@192.168.1.17:/home/yangfei/mqtt-vue-dashboard/backend/
+    echo   依赖文件上传成功
+)
+
+echo.
+echo 步骤7: 在服务器上安装依赖...
+ssh yangfei@192.168.1.17 "cd /home/yangfei/mqtt-vue-dashboard/backend && npm install --production"
+echo   服务器依赖安装完成
+
+echo.
+echo 步骤8: 启动后端服务...
+ssh yangfei@192.168.1.17 "cd /home/yangfei/mqtt-vue-dashboard/backend && nohup npm start > ../logs/backend.log 2>&1 &"
+echo   后端服务启动完成
+
+echo.
+echo 步骤9: 验证部署结果...
+ssh yangfei@192.168.1.17 "ls -la /home/yangfei/mqtt-vue-dashboard/frontend/dist/" > nul 2>&1
+if %errorlevel% equ 0 (
+    echo   前端文件验证成功
+) else (
+    echo   前端文件验证失败
+)
+
+ssh yangfei@192.168.1.17 "ps aux | grep 'node.*backend' | grep -v grep" > nul 2>&1
+if %errorlevel% equ 0 (
+    echo   后端服务运行正常
+) else (
+    echo   后端服务未运行
+)
+
+echo.
+echo ========================================
+echo   部署完成!
+echo ========================================
+echo.
+echo 访问信息:
+echo   后端API: http://192.168.1.17:3002
+echo   前端应用: http://192.168.1.17
+echo.
+echo 查看日志:
+echo   ssh yangfei@192.168.1.17 'tail -f /home/yangfei/mqtt-vue-dashboard/logs/backend.log'
+echo.
+pause

+ 24 - 0
mqtt-vue-dashboard/.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 13 - 0
mqtt-vue-dashboard/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>MQTT 数据监控平台</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 2333 - 0
mqtt-vue-dashboard/package-lock.json

@@ -0,0 +1,2333 @@
+{
+  "name": "mqtt-vue-dashboard",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "mqtt-vue-dashboard",
+      "version": "0.0.0",
+      "dependencies": {
+        "@ant-design/icons-vue": "^7.0.1",
+        "ant-design-vue": "^4.2.6",
+        "axios": "^1.15.0",
+        "dayjs": "^1.11.20",
+        "echarts": "^6.0.0",
+        "echarts-for-vue": "^1.4.1",
+        "file-saver": "^2.0.5",
+        "pinia": "^3.0.4",
+        "socket.io-client": "^4.8.3",
+        "vue": "^3.5.32",
+        "vue-router": "^4.6.4",
+        "xlsx": "^0.18.5"
+      },
+      "devDependencies": {
+        "@types/file-saver": "^2.0.7",
+        "@vitejs/plugin-vue": "^6.0.6",
+        "less": "^4.6.4",
+        "typescript": "~6.0.2",
+        "vite": "^8.0.4"
+      }
+    },
+    "node_modules/@ant-design/colors": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
+      "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
+      "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/icons-vue": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
+      "integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-svg": "^4.2.1"
+      },
+      "peerDependencies": {
+        "vue": ">=3.0.3"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+      "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@emnapi/core": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+      "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.2.1",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+      "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+      "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+      "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+      "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@tybys/wasm-util": "^0.10.1"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "peerDependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.124.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
+      "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-arm64": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-x64": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-freebsd-x64": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
+      "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
+      "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
+      "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
+      "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
+      "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
+      "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
+      "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
+      "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-wasm32-wasi": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
+      "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "1.9.2",
+        "@emnapi/runtime": "1.9.2",
+        "@napi-rs/wasm-runtime": "^1.1.3"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
+      "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
+      "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
+      "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@simonwep/pickr": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.8.2.tgz",
+      "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js": "^3.15.1",
+        "nanopop": "^2.1.0"
+      }
+    },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+      "license": "MIT"
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/echarts": {
+      "version": "4.9.22",
+      "resolved": "https://registry.npmjs.org/@types/echarts/-/echarts-4.9.22.tgz",
+      "integrity": "sha512-7Fo6XdWpoi8jxkwP7BARUOM7riq8bMhmsCtSG8gzUcJmFhLo387tihoBYS/y5j7jl3PENT5RxeWZdN9RiwO7HQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/zrender": "*"
+      }
+    },
+    "node_modules/@types/file-saver": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
+      "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/zrender": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/@types/zrender/-/zrender-4.0.6.tgz",
+      "integrity": "sha512-1jZ9bJn2BsfmYFPBHtl5o3uV+ILejAtGrDcYSpT4qaVKEI/0YY+arw3XHU04Ebd8Nca3SQ7uNcLaqiL+tTFVMg==",
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.6",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz",
+      "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "1.0.0-rc.13"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vitejs/plugin-vue/node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.13",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz",
+      "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
+      "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/shared": "3.5.32",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
+      "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
+      "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@vue/compiler-core": "3.5.32",
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/compiler-ssr": "3.5.32",
+        "@vue/shared": "3.5.32",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.8",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
+      "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+      "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.9"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+      "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.9",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+      "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
+      "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
+      "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.32",
+        "@vue/shared": "3.5.32"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
+      "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.32",
+        "@vue/runtime-core": "3.5.32",
+        "@vue/shared": "3.5.32",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
+      "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.32",
+        "@vue/shared": "3.5.32"
+      },
+      "peerDependencies": {
+        "vue": "3.5.32"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
+      "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
+      "license": "MIT"
+    },
+    "node_modules/adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/ant-design-vue": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
+      "integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-vue": "^7.0.0",
+        "@babel/runtime": "^7.10.5",
+        "@ctrl/tinycolor": "^3.5.0",
+        "@emotion/hash": "^0.9.0",
+        "@emotion/unitless": "^0.8.0",
+        "@simonwep/pickr": "~1.8.0",
+        "array-tree-filter": "^2.1.0",
+        "async-validator": "^4.0.0",
+        "csstype": "^3.1.1",
+        "dayjs": "^1.10.5",
+        "dom-align": "^1.12.1",
+        "dom-scroll-into-view": "^2.0.0",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.15",
+        "resize-observer-polyfill": "^1.5.1",
+        "scroll-into-view-if-needed": "^2.2.25",
+        "shallow-equal": "^1.0.0",
+        "stylis": "^4.1.3",
+        "throttle-debounce": "^5.0.0",
+        "vue-types": "^3.0.0",
+        "warning": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design-vue"
+      },
+      "peerDependencies": {
+        "vue": ">=3.2.0"
+      }
+    },
+    "node_modules/array-tree-filter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
+      "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
+      "license": "MIT"
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
+      "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
+      "license": "MIT"
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/core-js": {
+      "version": "3.49.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
+      "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "license": "Apache-2.0",
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.20",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+      "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dom-align": {
+      "version": "1.12.4",
+      "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz",
+      "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
+      "license": "MIT"
+    },
+    "node_modules/dom-scroll-into-view": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
+      "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
+      "license": "MIT"
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/echarts": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
+      "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "6.0.0"
+      }
+    },
+    "node_modules/echarts-for-vue": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/echarts-for-vue/-/echarts-for-vue-1.4.1.tgz",
+      "integrity": "sha512-o1VKvX5lCo5Nb8B5EEr6ztt+hRi4W9RnKn7wIk+l3fZ0S2yVXanqZckgBfO7v8vSS+xOJq/Df5flrNUQIRhn/Q==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/echarts": "^4.9.7"
+      },
+      "peerDependencies": {
+        "echarts": ">=3",
+        "vue": ">=2"
+      }
+    },
+    "node_modules/echarts/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+      "license": "0BSD"
+    },
+    "node_modules/engine.io-client": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
+      "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.18.3",
+        "xmlhttprequest-ssl": "~2.1.1"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/errno": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+      "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "prr": "~1.0.1"
+      },
+      "bin": {
+        "errno": "cli.js"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
+      "license": "MIT"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC",
+      "optional": true
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/image-size": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+      "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "bin": {
+        "image-size": "bin/image-size.js"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
+      "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/less": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/less/-/less-4.6.4.tgz",
+      "integrity": "sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "copy-anything": "^3.0.5",
+        "parse-node-version": "^1.0.1"
+      },
+      "bin": {
+        "lessc": "bin/lessc"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "errno": "^0.1.1",
+        "graceful-fs": "^4.1.2",
+        "image-size": "~0.5.0",
+        "make-dir": "^2.1.0",
+        "mime": "^1.4.1",
+        "needle": "^3.1.0",
+        "source-map": "~0.6.0"
+      }
+    },
+    "node_modules/less/node_modules/copy-anything": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
+      "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^4.1.8"
+      },
+      "engines": {
+        "node": ">=12.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/less/node_modules/is-what": {
+      "version": "4.1.16",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+      "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.13"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+      "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+      "dev": true,
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.32.0",
+        "lightningcss-darwin-arm64": "1.32.0",
+        "lightningcss-darwin-x64": "1.32.0",
+        "lightningcss-freebsd-x64": "1.32.0",
+        "lightningcss-linux-arm-gnueabihf": "1.32.0",
+        "lightningcss-linux-arm64-gnu": "1.32.0",
+        "lightningcss-linux-arm64-musl": "1.32.0",
+        "lightningcss-linux-x64-gnu": "1.32.0",
+        "lightningcss-linux-x64-musl": "1.32.0",
+        "lightningcss-win32-arm64-msvc": "1.32.0",
+        "lightningcss-win32-x64-msvc": "1.32.0"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+      "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+      "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+      "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-freebsd-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+      "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+      "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+      "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+      "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+      "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+      "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+      "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+      "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/make-dir": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+      "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "pify": "^4.0.1",
+        "semver": "^5.6.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/nanopop": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.4.2.tgz",
+      "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
+      "license": "MIT"
+    },
+    "node_modules/needle": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/needle/-/needle-3.5.0.tgz",
+      "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "iconv-lite": "^0.6.3",
+        "sax": "^1.2.4"
+      },
+      "bin": {
+        "needle": "bin/needle"
+      },
+      "engines": {
+        "node": ">= 4.4.x"
+      }
+    },
+    "node_modules/parse-node-version": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
+      "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
+      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.7"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.5.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.10",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+      "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rolldown": {
+      "version": "1.0.0-rc.15",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
+      "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.124.0",
+        "@rolldown/pluginutils": "1.0.0-rc.15"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.0.0-rc.15",
+        "@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
+        "@rolldown/binding-darwin-x64": "1.0.0-rc.15",
+        "@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
+        "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
+        "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
+        "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
+        "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
+        "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
+        "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
+        "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
+        "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/sax": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
+      "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "optional": true,
+      "engines": {
+        "node": ">=11.0.0"
+      }
+    },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
+      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
+    "node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "dev": true,
+      "license": "ISC",
+      "optional": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/shallow-equal": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
+      "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
+      "license": "MIT"
+    },
+    "node_modules/socket.io-client": {
+      "version": "4.8.3",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
+      "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1",
+        "engine.io-client": "~6.6.1",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
+      "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "frac": "~1.1.2"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/stylis": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
+      "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+      "license": "MIT"
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.16",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+      "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "dev": true,
+      "license": "0BSD",
+      "optional": true
+    },
+    "node_modules/typescript": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
+      "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/vite": {
+      "version": "8.0.8",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
+      "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.32.0",
+        "picomatch": "^4.0.4",
+        "postcss": "^8.5.8",
+        "rolldown": "1.0.0-rc.15",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.1.0",
+        "esbuild": "^0.27.0 || ^0.28.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "@vitejs/devtools": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.32",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
+      "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.32",
+        "@vue/compiler-sfc": "3.5.32",
+        "@vue/runtime-dom": "3.5.32",
+        "@vue/server-renderer": "3.5.32",
+        "@vue/shared": "3.5.32"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-router/node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/vue-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/vue-types/-/vue-types-3.0.2.tgz",
+      "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "3.0.1"
+      },
+      "engines": {
+        "node": ">=10.15.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "node_modules/wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "bin": {
+        "xlsx": "bin/xlsx.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/xmlhttprequest-ssl": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+      "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/zrender": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
+      "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
+    },
+    "node_modules/zrender/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+      "license": "0BSD"
+    }
+  }
+}

+ 32 - 0
mqtt-vue-dashboard/package.json

@@ -0,0 +1,32 @@
+{
+  "name": "mqtt-vue-dashboard",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "@types/file-saver": "^2.0.7",
+    "@vitejs/plugin-vue": "^6.0.6",
+    "less": "^4.6.4",
+    "typescript": "~6.0.2",
+    "vite": "^8.0.4"
+  },
+  "dependencies": {
+    "@ant-design/icons-vue": "^7.0.1",
+    "ant-design-vue": "^4.2.6",
+    "axios": "^1.15.0",
+    "dayjs": "^1.11.20",
+    "echarts": "^6.0.0",
+    "echarts-for-vue": "^1.4.1",
+    "file-saver": "^2.0.5",
+    "pinia": "^3.0.4",
+    "socket.io-client": "^4.8.3",
+    "vue": "^3.5.32",
+    "vue-router": "^4.6.4",
+    "xlsx": "^0.18.5"
+  }
+}

File diff suppressed because it is too large
+ 0 - 0
mqtt-vue-dashboard/public/favicon.svg


+ 24 - 0
mqtt-vue-dashboard/public/icons.svg

@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <symbol id="bluesky-icon" viewBox="0 0 16 17">
+    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
+    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
+  </symbol>
+  <symbol id="discord-icon" viewBox="0 0 20 19">
+    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
+  </symbol>
+  <symbol id="documentation-icon" viewBox="0 0 21 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
+  </symbol>
+  <symbol id="github-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
+  </symbol>
+  <symbol id="social-icon" viewBox="0 0 20 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
+  </symbol>
+  <symbol id="x-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
+  </symbol>
+</svg>

+ 13 - 0
mqtt-vue-dashboard/server/.env

@@ -0,0 +1,13 @@
+DB_HOST=192.168.1.17
+DB_PORT=3306
+DB_USER=root
+DB_PASSWORD=123
+DB_NAME=mqtt_vue_dashboard
+
+MQTT_BROKER_PORT=1883
+MQTT_ALLOW_ANONYMOUS=true
+
+JWT_SECRET=mqtt_dashboard_jwt_secret_key_2024
+PORT=3002
+
+NODE_ENV=production

+ 13 - 0
mqtt-vue-dashboard/server/.env.production

@@ -0,0 +1,13 @@
+DB_HOST=192.168.1.17
+DB_PORT=3306
+DB_USER=root
+DB_PASSWORD=123
+DB_NAME=mqtt_vue_dashboard
+
+MQTT_BROKER_PORT=1883
+MQTT_ALLOW_ANONYMOUS=true
+
+JWT_SECRET=mqtt_dashboard_jwt_secret_key_2024
+PORT=3002
+
+NODE_ENV=production

+ 384 - 0
mqtt-vue-dashboard/server/database/init.sql

@@ -0,0 +1,384 @@
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+CREATE DATABASE IF NOT EXISTS `mqtt_vue_dashboard` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE `mqtt_vue_dashboard`;
+
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users` (
+  `id` varchar(36) NOT NULL COMMENT '用户ID',
+  `username` varchar(50) NOT NULL COMMENT '用户名',
+  `password` varchar(255) NOT NULL COMMENT '密码(加密)',
+  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
+  `role` enum('admin','user','viewer') NOT NULL DEFAULT 'user',
+  `created_at` timestamp NOT NULL DEFAULT current_timestamp COMMENT '创建时间',
+  `updated_at` timestamp NOT NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `username` (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
+
+INSERT INTO `users` (`id`, `username`, `password`, `email`, `role`) VALUES
+(UUID(), 'admin', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'admin@example.com', 'admin');
+
+DROP TABLE IF EXISTS `pages`;
+CREATE TABLE `pages` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `name` varchar(50) NOT NULL COMMENT '页面名称',
+  `path` varchar(100) NOT NULL COMMENT '页面路径',
+  `description` varchar(200) DEFAULT NULL COMMENT '页面描述',
+  `created_at` timestamp NULL DEFAULT current_timestamp,
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_path` (`path`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+INSERT INTO `pages` (`name`, `path`, `description`) VALUES
+('仪表板', '/dashboard', '系统概览和数据统计'),
+('设备管理', '/devices', '设备列表和状态监控'),
+('房间管理', '/rooms', '房间和设备绑定'),
+('OTA升级', '/ota', '固件管理和OTA升级'),
+('连接管理', '/connections', 'MQTT连接监控'),
+('消息管理', '/messages', 'MQTT消息查看'),
+('传感器数据', '/sensor-data', '传感器数据可视化'),
+('客户端认证', '/client-auth', 'MQTT客户端认证管理'),
+('访问控制', '/client-acl', 'MQTT客户端ACL管理'),
+('认证日志', '/auth-log', '认证操作日志'),
+('系统日志', '/system-log', '系统运行日志'),
+('系统设置', '/settings', '系统配置管理');
+
+DROP TABLE IF EXISTS `user_permissions`;
+CREATE TABLE `user_permissions` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `user_id` varchar(36) NOT NULL,
+  `page_id` int NOT NULL,
+  `created_at` timestamp NULL DEFAULT current_timestamp,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_page` (`user_id`, `page_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+DROP TABLE IF EXISTS `devices`;
+CREATE TABLE `devices` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '设备ID',
+  `clientid` varchar(100) NOT NULL COMMENT 'MQTT客户端ID',
+  `device_name` varchar(100) DEFAULT NULL COMMENT '设备名称',
+  `username` varchar(100) DEFAULT NULL,
+  `firmware_version` varchar(50) DEFAULT NULL COMMENT '固件版本',
+  `device_ip_port` varchar(100) DEFAULT NULL COMMENT '设备IP:端口',
+  `last_ip_port` varchar(100) DEFAULT NULL COMMENT '最后连接的IP:端口',
+  `status` enum('online','offline','unknown') DEFAULT 'unknown' COMMENT '设备状态',
+  `last_event_time` datetime DEFAULT NULL COMMENT '最后事件时间',
+  `last_online_time` datetime DEFAULT NULL COMMENT '最后上线时间',
+  `last_offline_time` datetime DEFAULT NULL COMMENT '最后下线时间',
+  `online_duration` int DEFAULT 0 COMMENT '累计在线时长(秒)',
+  `connect_count` int DEFAULT 0 COMMENT '累计连接次数',
+  `rssi` int DEFAULT NULL COMMENT 'WiFi信号强度(dBm)',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '设备创建时间',
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP COMMENT '设备更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `clientid` (`clientid`),
+  KEY `idx_status` (`status`),
+  KEY `idx_ip_port` (`device_ip_port`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备管理表';
+
+DROP TABLE IF EXISTS `rooms`;
+CREATE TABLE `rooms` (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT '房间唯一标识ID',
+  `name` varchar(100) NOT NULL COMMENT '房间名称',
+  `floor_id` int NOT NULL COMMENT '所在楼层ID',
+  `room_number` varchar(20) NOT NULL COMMENT '房间编号',
+  `room_type` varchar(50) NOT NULL COMMENT '房间类型',
+  `area` decimal(8,2) DEFAULT NULL COMMENT '房间面积(平方米)',
+  `description` text DEFAULT NULL COMMENT '房间描述',
+  `status` enum('active','inactive','maintenance') DEFAULT 'active' COMMENT '房间状态',
+  `orientation` varchar(20) DEFAULT '东' COMMENT '房间朝向',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '创建时间',
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_floor_id` (`floor_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间表';
+
+DROP TABLE IF EXISTS `room_devices`;
+CREATE TABLE `room_devices` (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT '设备唯一标识ID',
+  `name` varchar(100) NOT NULL COMMENT '设备名称',
+  `type` varchar(50) NOT NULL COMMENT '设备类型',
+  `model` varchar(100) DEFAULT NULL COMMENT '设备型号',
+  `room_id` int NOT NULL COMMENT '所属房间ID',
+  `status` enum('online','offline','error') DEFAULT 'offline' COMMENT '设备状态',
+  `last_seen` timestamp NULL DEFAULT NULL COMMENT '最后在线时间',
+  `properties` longtext DEFAULT NULL COMMENT '设备属性JSON',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '创建时间',
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_room_id` (`room_id`),
+  KEY `idx_status` (`status`),
+  CONSTRAINT `room_devices_ibfk_1` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间设备表';
+
+DROP TABLE IF EXISTS `device_bindings`;
+CREATE TABLE `device_bindings` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `device_clientid` varchar(100) NOT NULL COMMENT '设备客户端ID',
+  `room_id` int NOT NULL COMMENT '房间ID',
+  `device_name` varchar(255) DEFAULT NULL COMMENT '在房间中的设备显示名称',
+  `device_type` varchar(100) DEFAULT NULL COMMENT '在房间中的设备类型',
+  `properties` text DEFAULT NULL COMMENT 'JSON格式额外属性',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '创建时间',
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `unique_device_binding` (`device_clientid`),
+  KEY `idx_room_id` (`room_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备与房间绑定关系表';
+
+DROP TABLE IF EXISTS `mqtt_messages`;
+CREATE TABLE `mqtt_messages` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键',
+  `clientid` varchar(100) NOT NULL COMMENT '客户端ID',
+  `topic` varchar(512) NOT NULL COMMENT '消息主题',
+  `payload` text DEFAULT NULL COMMENT '消息内容',
+  `qos` tinyint NOT NULL DEFAULT 0 COMMENT 'QoS等级',
+  `retain` tinyint(1) NOT NULL DEFAULT 0 COMMENT '保留标志',
+  `message_id` varchar(64) DEFAULT NULL COMMENT 'MQTT消息ID',
+  `message_type` enum('publish','subscribe','unsubscribe') NOT NULL DEFAULT 'publish' COMMENT '消息类型',
+  `timestamp` bigint NOT NULL COMMENT '消息时间戳(毫秒)',
+  `node` varchar(100) NOT NULL DEFAULT '' COMMENT '节点',
+  `username` varchar(100) DEFAULT NULL COMMENT '用户名',
+  `proto_ver` tinyint NOT NULL DEFAULT 4 COMMENT 'MQTT协议版本',
+  `payload_format` varchar(50) DEFAULT 'text' COMMENT '消息格式',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '记录创建时间',
+  `message_time` datetime NOT NULL DEFAULT current_timestamp COMMENT '消息时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_clientid` (`clientid`),
+  KEY `idx_topic` (`topic`(255)),
+  KEY `idx_timestamp` (`timestamp`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MQTT消息存储表';
+
+DROP TABLE IF EXISTS `client_connections`;
+CREATE TABLE `client_connections` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键',
+  `username` varchar(100) DEFAULT NULL COMMENT '客户端用户名',
+  `clientid` varchar(100) NOT NULL COMMENT '客户端ID',
+  `event` varchar(50) NOT NULL COMMENT '事件类型',
+  `timestamp` datetime DEFAULT NULL COMMENT '事件时间',
+  `connected_at` datetime DEFAULT NULL COMMENT '连接时间',
+  `node` varchar(100) NOT NULL DEFAULT '' COMMENT '节点',
+  `peername` varchar(100) DEFAULT NULL COMMENT '客户端地址',
+  `sockname` varchar(100) DEFAULT NULL COMMENT '服务端地址',
+  `proto_name` varchar(20) NOT NULL DEFAULT 'MQTT' COMMENT '协议名称',
+  `proto_ver` int NOT NULL DEFAULT 4 COMMENT '协议版本',
+  `keepalive` int NOT NULL DEFAULT 60 COMMENT '心跳间隔(秒)',
+  `clean_start` tinyint(1) DEFAULT 1 COMMENT '是否清洁会话',
+  `reason` varchar(50) DEFAULT NULL COMMENT '断开原因',
+  `connection_duration` int DEFAULT NULL COMMENT '连接持续时间(秒)',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '记录创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_clientid` (`clientid`),
+  KEY `idx_timestamp` (`timestamp`),
+  KEY `idx_event` (`event`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户端连接事件表';
+
+DROP TABLE IF EXISTS `client_auth`;
+CREATE TABLE `client_auth` (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT '客户端认证记录ID',
+  `username` varchar(255) NOT NULL COMMENT '客户端用户名',
+  `clientid` varchar(255) NOT NULL COMMENT '客户端ID',
+  `password_hash` varchar(255) NOT NULL COMMENT '密码哈希值',
+  `salt` varchar(255) DEFAULT NULL,
+  `status` enum('enabled','disabled') DEFAULT 'enabled' COMMENT '客户端状态',
+  `device_type` varchar(100) DEFAULT 'unknown' COMMENT '设备类型',
+  `auth_method` enum('password','token','certificate','external') NOT NULL DEFAULT 'password' COMMENT '认证方法',
+  `description` text DEFAULT NULL COMMENT '客户端描述',
+  `is_superuser` tinyint(1) DEFAULT 0 COMMENT '是否为超级用户',
+  `use_salt` tinyint(1) DEFAULT 1 COMMENT '是否使用盐值加密',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '创建时间',
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `last_login_at` timestamp NULL DEFAULT NULL COMMENT '最后登录时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `unique_username_clientid` (`username`, `clientid`),
+  KEY `idx_username` (`username`),
+  KEY `idx_clientid` (`clientid`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户端认证表';
+
+DROP TABLE IF EXISTS `client_acl`;
+CREATE TABLE `client_acl` (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `username` varchar(100) NOT NULL COMMENT '用户名',
+  `topic` varchar(255) NOT NULL COMMENT 'MQTT主题',
+  `action` enum('publish','subscribe','pubsub') NOT NULL COMMENT '操作类型',
+  `permission` enum('allow','deny') NOT NULL DEFAULT 'allow' COMMENT '权限类型',
+  `priority` int DEFAULT 0 COMMENT '权限优先级',
+  `description` text DEFAULT NULL COMMENT '权限规则描述',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '创建时间',
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_topic` (`topic`),
+  KEY `idx_action` (`action`),
+  KEY `idx_permission` (`permission`),
+  KEY `idx_username` (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户端访问控制列表';
+
+DROP TABLE IF EXISTS `auth_log`;
+CREATE TABLE `auth_log` (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `clientid` varchar(255) NOT NULL COMMENT '客户端ID',
+  `username` varchar(255) NOT NULL COMMENT '用户名',
+  `ip_address` varchar(45) NOT NULL COMMENT '客户端IP地址',
+  `operation_type` enum('connect','publish','subscribe','disconnect') NOT NULL COMMENT '操作类型',
+  `auth_method` varchar(50) DEFAULT NULL COMMENT '认证方法',
+  `result` enum('success','failure') NOT NULL COMMENT '认证结果',
+  `reason` varchar(255) DEFAULT NULL COMMENT '认证失败原因',
+  `topic` varchar(255) DEFAULT NULL COMMENT 'MQTT主题',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '日志记录时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_clientid` (`clientid`),
+  KEY `idx_username` (`username`),
+  KEY `idx_operation_type` (`operation_type`),
+  KEY `idx_result` (`result`),
+  KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='认证日志表';
+
+DROP TABLE IF EXISTS `sensor_data`;
+CREATE TABLE `sensor_data` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `device_id` varchar(100) NOT NULL COMMENT '设备ID',
+  `topic` varchar(200) NOT NULL COMMENT 'MQTT主题',
+  `data_type` varchar(50) DEFAULT NULL COMMENT '数据类型',
+  `value` text DEFAULT NULL COMMENT '原始数据',
+  `timestamp` datetime NOT NULL COMMENT '数据时间戳',
+  `created_at` timestamp NULL DEFAULT current_timestamp,
+  PRIMARY KEY (`id`),
+  KEY `idx_device_timestamp` (`device_id`, `timestamp`),
+  KEY `idx_topic` (`topic`),
+  KEY `idx_data_type` (`data_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='传感器数据表';
+
+DROP TABLE IF EXISTS `firmware_files`;
+CREATE TABLE `firmware_files` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `version` varchar(50) NOT NULL,
+  `filename` varchar(255) NOT NULL,
+  `filepath` varchar(255) NOT NULL,
+  `filesize` bigint NOT NULL,
+  `md5sum` varchar(32) NOT NULL,
+  `description` text DEFAULT NULL,
+  `status` enum('active','inactive') DEFAULT 'active',
+  `created_by` varchar(50) DEFAULT NULL,
+  `created_at` timestamp NULL DEFAULT current_timestamp,
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `version` (`version`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='固件文件表';
+
+DROP TABLE IF EXISTS `ota_tasks`;
+CREATE TABLE `ota_tasks` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `device_id` varchar(255) NOT NULL,
+  `firmware_id` int NOT NULL,
+  `status` enum('pending','downloading','installing','success','failed') DEFAULT 'pending',
+  `progress` int DEFAULT 0,
+  `error_message` text DEFAULT NULL,
+  `start_time` timestamp NULL DEFAULT NULL,
+  `end_time` timestamp NULL DEFAULT NULL,
+  `created_at` timestamp NULL DEFAULT current_timestamp,
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_device_id` (`device_id`),
+  KEY `idx_firmware_id` (`firmware_id`),
+  KEY `idx_status` (`status`),
+  CONSTRAINT `ota_tasks_ibfk_1` FOREIGN KEY (`firmware_id`) REFERENCES `firmware_files` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OTA升级任务表';
+
+DROP TABLE IF EXISTS `system_logs`;
+CREATE TABLE `system_logs` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `level` enum('info','warn','error','debug') NOT NULL COMMENT '日志级别',
+  `message` text NOT NULL COMMENT '日志消息',
+  `source` varchar(255) NOT NULL COMMENT '日志来源',
+  `module` varchar(255) DEFAULT NULL COMMENT '模块名称',
+  `user_id` int DEFAULT NULL COMMENT '用户ID',
+  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
+  `ip_address` varchar(45) DEFAULT NULL COMMENT 'IP地址',
+  `details` text DEFAULT NULL COMMENT '详细信息',
+  `created_at` datetime DEFAULT current_timestamp COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_level` (`level`),
+  KEY `idx_source` (`source`),
+  KEY `idx_module` (`module`),
+  KEY `idx_created_at` (`created_at`),
+  KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统运行日志表';
+
+DROP TABLE IF EXISTS `wifi_configurations`;
+CREATE TABLE `wifi_configurations` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `device_clientid` varchar(255) NOT NULL COMMENT '设备客户端ID',
+  `ssid` varchar(32) NOT NULL COMMENT 'WiFi SSID',
+  `password` varchar(64) NOT NULL COMMENT 'WiFi密码',
+  `status` enum('pending','sent','applied','failed') DEFAULT 'sent' COMMENT '配置状态',
+  `sent_at` timestamp NULL DEFAULT NULL COMMENT '发送时间',
+  `applied_at` timestamp NULL DEFAULT NULL COMMENT '应用时间',
+  `created_at` timestamp NULL DEFAULT current_timestamp COMMENT '创建时间',
+  `updated_at` timestamp NULL DEFAULT current_timestamp ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_device_clientid` (`device_clientid`),
+  KEY `idx_status` (`status`),
+  KEY `idx_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备WiFi配置表';
+
+CREATE OR REPLACE VIEW `vw_client_connections` AS
+SELECT `clientid`, `username`, `peername`, `connected_at`, `timestamp` AS `disconnected_at`,
+       `reason`, `timestamp`, `event`, `node`, `sockname`, `proto_name`, `proto_ver`
+FROM `client_connections`;
+
+CREATE OR REPLACE VIEW `vw_device_logs` AS
+SELECT
+  NULL as id,
+  cc.clientid,
+  CASE
+    WHEN cc.event = 'client.connected' THEN 'connect'
+    WHEN cc.event = 'client.disconnected' THEN 'disconnect'
+    ELSE 'unknown'
+  END as event_type,
+  cc.timestamp as event_time,
+  NULL as topic,
+  NULL as payload,
+  NULL as qos,
+  cc.username,
+  cc.peername,
+  cc.proto_ver,
+  cc.node,
+  cc.reason as details,
+  cc.timestamp as created_at
+FROM client_connections cc
+WHERE cc.event IN ('client.connected', 'client.disconnected')
+
+UNION ALL
+
+SELECT
+  mm.id,
+  mm.clientid,
+  CASE
+    WHEN mm.message_type = 'publish' THEN 'publish'
+    WHEN mm.message_type = 'subscribe' THEN 'subscribe'
+    WHEN mm.message_type = 'unsubscribe' THEN 'unsubscribe'
+    ELSE mm.message_type
+  END as event_type,
+  mm.message_time as event_time,
+  mm.topic,
+  mm.payload,
+  mm.qos,
+  mm.username,
+  NULL as peername,
+  mm.proto_ver,
+  mm.node,
+  NULL as details,
+  mm.created_at
+FROM mqtt_messages mm
+WHERE mm.message_type IN ('publish', 'subscribe', 'unsubscribe')
+
+ORDER BY event_time DESC;
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 30 - 0
mqtt-vue-dashboard/server/ecosystem.config.js

@@ -0,0 +1,30 @@
+module.exports = {
+  apps: [
+    {
+      // 进程名称(自定义,好记:mqtt后端)
+      name: "mqtt-dashboard-backend",
+      // 后端入口文件(通用:app.js/index.js/server.js,99%的后端都是这三个,有其他名字改这里即可)
+      script: "./dist/index.js",
+      // 运行模式:fork 单进程 ✅后端推荐,MQTT服务用单进程足够,资源占用极低
+      exec_mode: "fork",
+      instances: 1,
+      // 运行环境:生产环境 production ✅后端必须配置
+      env: { 
+        NODE_ENV: "production",
+        TZ: "Asia/Shanghai"  // 设置时区为北京时间
+      },
+      
+      // ======== 后端核心守护配置(重中之重,全部开启) ========
+      autorestart: true,        // 进程崩溃/报错/被杀掉 → 自动重启 ✅必开
+      max_memory_restart: "200M", // 内存占用超200M自动重启,释放内存,防止内存泄漏 ✅后端必开
+      restart_delay: 500,       // 崩溃后0.5秒重启,快速恢复服务
+      min_uptime: "3s",         // 进程运行至少3秒才算稳定,避免无限重启
+      
+      // ======== 后端日志配置(排查BUG必备,MQTT消息/报错全留存) ========
+      log_date_format: "YYYY-MM-DD HH:mm:ss", // 日志带时间戳,方便定位问题
+      out_file: "./logs/pm2-out.log",         // 正常运行日志(比如MQTT连接日志)
+      error_file: "./logs/pm2-error.log",     // 错误日志(比如端口占用、代码报错)
+      merge_logs: true,                       // 合并日志,方便查看
+    }
+  ]
+};

+ 10 - 0
mqtt-vue-dashboard/server/migrations/add_rssi_to_devices.sql

@@ -0,0 +1,10 @@
+-- 添加 rssi 字段到 devices 表
+-- 用于存储设备的 WiFi 信号强度 (dBm)
+-- RSSI 值范围通常为 -30 (极好) 到 -90 (极差)
+
+ALTER TABLE `devices` 
+ADD COLUMN `rssi` INT NULL DEFAULT NULL COMMENT 'WiFi信号强度(dBm), 范围-30到-90, 越接近0信号越强' AFTER `connect_count`;
+
+-- 添加索引以便按信号强度查询
+ALTER TABLE `devices` 
+ADD INDEX `idx_rssi` (`rssi`);

+ 47 - 0
mqtt-vue-dashboard/server/migrations/create_device_logs_view.sql

@@ -0,0 +1,47 @@
+CREATE OR REPLACE VIEW vw_device_logs AS
+SELECT 
+  NULL as id,
+  cc.clientid,
+  CASE 
+    WHEN cc.event = 'client.connected' THEN 'connect'
+    WHEN cc.event = 'client.disconnected' THEN 'disconnect'
+    ELSE 'unknown'
+  END as event_type,
+  cc.timestamp as event_time,
+  NULL as topic,
+  NULL as payload,
+  NULL as qos,
+  cc.username,
+  cc.peername,
+  cc.proto_ver,
+  cc.node,
+  cc.reason as details,
+  cc.timestamp as created_at
+FROM vw_client_connections cc
+WHERE cc.event IN ('client.connected', 'client.disconnected')
+
+UNION ALL
+
+SELECT 
+  mm.id,
+  mm.clientid,
+  CASE 
+    WHEN mm.message_type = 'publish' THEN 'publish'
+    WHEN mm.message_type = 'subscribe' THEN 'subscribe'
+    WHEN mm.message_type = 'unsubscribe' THEN 'unsubscribe'
+    ELSE mm.message_type
+  END as event_type,
+  mm.message_time as event_time,
+  mm.topic,
+  mm.payload,
+  mm.qos,
+  mm.username,
+  NULL as peername,
+  mm.proto_ver,
+  mm.node,
+  NULL as details,
+  mm.created_at
+FROM mqtt_messages mm
+WHERE mm.message_type IN ('publish', 'subscribe', 'unsubscribe')
+
+ORDER BY event_time DESC;

+ 18 - 0
mqtt-vue-dashboard/server/migrations/create_system_logs_table.sql

@@ -0,0 +1,18 @@
+-- 创建系统日志表
+CREATE TABLE IF NOT EXISTS system_logs (
+  id INT PRIMARY KEY AUTO_INCREMENT,
+  level ENUM('info', 'warn', 'error', 'debug') NOT NULL COMMENT '日志级别',
+  message TEXT NOT NULL COMMENT '日志消息',
+  source VARCHAR(255) NOT NULL COMMENT '日志来源',
+  module VARCHAR(255) COMMENT '模块名称',
+  user_id INT COMMENT '用户ID',
+  username VARCHAR(255) COMMENT '用户名',
+  ip_address VARCHAR(45) COMMENT 'IP地址',
+  details TEXT COMMENT '详细信息',
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  INDEX idx_level (level),
+  INDEX idx_source (source),
+  INDEX idx_module (module),
+  INDEX idx_created_at (created_at),
+  INDEX idx_user_id (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统运行日志表';

+ 15 - 0
mqtt-vue-dashboard/server/migrations/create_wifi_configurations_table.sql

@@ -0,0 +1,15 @@
+-- WiFi配置表
+CREATE TABLE IF NOT EXISTS wifi_configurations (
+  id INT AUTO_INCREMENT PRIMARY KEY,
+  device_clientid VARCHAR(255) NOT NULL COMMENT '设备客户端ID',
+  ssid VARCHAR(32) NOT NULL COMMENT 'WiFi SSID',
+  password VARCHAR(64) NOT NULL COMMENT 'WiFi密码',
+  status ENUM('pending', 'sent', 'applied', 'failed') DEFAULT 'sent' COMMENT '配置状态:pending-待发送,sent-已发送,applied-已应用,failed-失败',
+  sent_at TIMESTAMP NULL COMMENT '发送时间',
+  applied_at TIMESTAMP NULL COMMENT '应用时间',
+  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  INDEX idx_device_clientid (device_clientid),
+  INDEX idx_status (status),
+  INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备WiFi配置表';

+ 3351 - 0
mqtt-vue-dashboard/server/package-lock.json

@@ -0,0 +1,3351 @@
+{
+  "name": "mqtt-dashboard-server",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "mqtt-dashboard-server",
+      "version": "1.0.0",
+      "dependencies": {
+        "aedes": "^0.51.3",
+        "axios": "^1.13.2",
+        "bcryptjs": "^3.0.3",
+        "compression": "^1.8.1",
+        "cors": "^2.8.5",
+        "cross-env": "^10.1.0",
+        "dotenv": "^17.2.3",
+        "express": "^5.1.0",
+        "express-rate-limit": "^8.2.1",
+        "express-validator": "^7.3.1",
+        "helmet": "^8.1.0",
+        "jsonwebtoken": "^9.0.2",
+        "moment": "^2.30.1",
+        "morgan": "^1.10.1",
+        "multer": "^2.0.2",
+        "mysql2": "^3.15.3",
+        "socket.io": "^4.8.1"
+      },
+      "devDependencies": {
+        "@types/bcryptjs": "^2.4.6",
+        "@types/cors": "^2.8.19",
+        "@types/express": "^5.0.5",
+        "@types/jsonwebtoken": "^9.0.10",
+        "@types/morgan": "^1.9.10",
+        "@types/multer": "^2.0.0",
+        "@types/node": "^24.10.0",
+        "nodemon": "^3.1.10",
+        "ts-node": "^10.9.2",
+        "typescript": "^5.9.3"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+      "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "0.3.9"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@epic-web/invariant": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
+      "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.0.3",
+        "@jridgewell/sourcemap-codec": "^1.4.10"
+      }
+    },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+      "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node10": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
+      "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node12": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+      "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node14": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node16": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+      "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/bcryptjs": {
+      "version": "2.4.6",
+      "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
+      "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/body-parser": {
+      "version": "1.19.6",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+      "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.38",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+      "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/cors": {
+      "version": "2.8.19",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+      "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/express": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+      "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^5.0.0",
+        "@types/serve-static": "^2"
+      }
+    },
+    "node_modules/@types/express-serve-static-core": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
+      "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*",
+        "@types/send": "*"
+      }
+    },
+    "node_modules/@types/http-errors": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+      "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/jsonwebtoken": {
+      "version": "9.0.10",
+      "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
+      "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/ms": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/morgan": {
+      "version": "1.9.10",
+      "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz",
+      "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+      "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/multer": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
+      "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/express": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "24.12.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
+      "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.16.0"
+      }
+    },
+    "node_modules/@types/qs": {
+      "version": "6.15.0",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
+      "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/range-parser": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/readable-stream": {
+      "version": "4.0.23",
+      "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz",
+      "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/serve-static": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+      "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/http-errors": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/ws": {
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+      "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/abort-controller": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+      "license": "MIT",
+      "dependencies": {
+        "event-target-shim": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=6.5"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/accepts/node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.16.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-walk": {
+      "version": "8.3.5",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+      "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.11.0"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/aedes": {
+      "version": "0.51.3",
+      "resolved": "https://registry.npmjs.org/aedes/-/aedes-0.51.3.tgz",
+      "integrity": "sha512-aQfiI9w3RbqnowNCdcGMmCtxBFXN9bhJFcuZm24U5/NU06V3MCl42jWK2GUnu8rOypR2Ahi/aEcgq3w7CMcycg==",
+      "license": "MIT",
+      "dependencies": {
+        "aedes-packet": "^3.0.0",
+        "aedes-persistence": "^9.1.2",
+        "end-of-stream": "^1.4.4",
+        "fastfall": "^1.5.1",
+        "fastparallel": "^2.4.1",
+        "fastseries": "^2.0.0",
+        "hyperid": "^3.2.0",
+        "mqemitter": "^6.0.0",
+        "mqtt-packet": "^9.0.0",
+        "retimer": "^4.0.0",
+        "reusify": "^1.0.4",
+        "uuid": "^10.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/aedes"
+      }
+    },
+    "node_modules/aedes-packet": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz",
+      "integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==",
+      "license": "MIT",
+      "dependencies": {
+        "mqtt-packet": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/aedes-packet/node_modules/bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "node_modules/aedes-packet/node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "node_modules/aedes-packet/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/aedes-packet/node_modules/mqtt-packet": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz",
+      "integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bl": "^4.0.2",
+        "debug": "^4.1.1",
+        "process-nextick-args": "^2.0.1"
+      }
+    },
+    "node_modules/aedes-packet/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/aedes-packet/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/aedes-persistence": {
+      "version": "9.1.2",
+      "resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-9.1.2.tgz",
+      "integrity": "sha512-2Wlr5pwIK0eQOkiTwb8ZF6C20s8UPUlnsJ4kXYePZ3JlQl0NbBA176mzM8wY294BJ5wybpNc9P5XEQxqadRNcQ==",
+      "license": "MIT",
+      "dependencies": {
+        "aedes-packet": "^3.0.0",
+        "qlobber": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/aedes/node_modules/uuid": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
+      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/append-field": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+      "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+      "license": "MIT"
+    },
+    "node_modules/arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/aws-ssl-profiles": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+      "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/axios": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
+      "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "license": "MIT",
+      "engines": {
+        "node": "^4.5.0 || >= 5.9"
+      }
+    },
+    "node_modules/basic-auth": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+      "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.1.2"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/basic-auth/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "license": "MIT"
+    },
+    "node_modules/bcryptjs": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
+      "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
+      "license": "BSD-3-Clause",
+      "bin": {
+        "bcrypt": "bin/bcrypt"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/bl": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
+      "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/readable-stream": "^4.0.0",
+        "buffer": "^6.0.3",
+        "inherits": "^2.0.4",
+        "readable-stream": "^4.2.0"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+      "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.3",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.7.0",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.1",
+        "raw-body": "^3.0.1",
+        "type-is": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/body-parser/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/body-parser/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+      "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/buffer": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.2.1"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "license": "MIT"
+    },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/compressible": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+      "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": ">= 1.43.0 < 2"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/compression": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+      "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "compressible": "~2.0.18",
+        "debug": "2.6.9",
+        "negotiator": "~0.6.4",
+        "on-headers": "~1.1.0",
+        "safe-buffer": "5.2.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/concat-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+      "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+      "engines": [
+        "node >= 6.0"
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.0.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "node_modules/concat-stream/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+      "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/cors": {
+      "version": "2.8.6",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+      "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-env": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
+      "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
+      "license": "MIT",
+      "dependencies": {
+        "@epic-web/invariant": "^1.0.0",
+        "cross-spawn": "^7.0.6"
+      },
+      "bin": {
+        "cross-env": "dist/bin/cross-env.js",
+        "cross-env-shell": "dist/bin/cross-env-shell.js"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/denque": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+      "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/diff": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+      "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "17.4.2",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+      "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/engine.io": {
+      "version": "6.6.6",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz",
+      "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "@types/ws": "^8.5.12",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.7.2",
+        "cors": "~2.8.5",
+        "debug": "~4.4.1",
+        "engine.io-parser": "~5.2.1",
+        "ws": "~8.18.3"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+      "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/engine.io/node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/engine.io/node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/engine.io/node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/event-target-shim": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.x"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+      "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.1",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express-rate-limit": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
+      "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
+      "license": "MIT",
+      "dependencies": {
+        "ip-address": "10.1.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/express-rate-limit"
+      },
+      "peerDependencies": {
+        "express": ">= 4.11"
+      }
+    },
+    "node_modules/express-validator": {
+      "version": "7.3.2",
+      "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz",
+      "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==",
+      "license": "MIT",
+      "dependencies": {
+        "lodash": "^4.18.1",
+        "validator": "~13.15.23"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      }
+    },
+    "node_modules/express/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/express/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/fastfall": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz",
+      "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==",
+      "license": "MIT",
+      "dependencies": {
+        "reusify": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/fastparallel": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz",
+      "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4",
+        "xtend": "^4.0.2"
+      }
+    },
+    "node_modules/fastseries": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz",
+      "integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ==",
+      "license": "ISC"
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+      "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/finalhandler/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/finalhandler/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/form-data/node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/form-data/node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/generate-function": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+      "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-property": "^1.0.2"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/helmet": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+      "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/hyperid": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz",
+      "integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "^5.2.1",
+        "uuid": "^8.3.2",
+        "uuid-parse": "^1.1.0"
+      }
+    },
+    "node_modules/hyperid/node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "node_modules/hyperid/node_modules/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+      "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/ignore-by-default": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+      "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ip-address": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+      "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-property": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+      "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+      "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+      "license": "MIT",
+      "dependencies": {
+        "jws": "^4.0.1",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/jsonwebtoken/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+      "license": "MIT"
+    },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/lru.min": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+      "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+      "license": "MIT",
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=1.30.0",
+        "node": ">=8.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wellwelwel"
+      }
+    },
+    "node_modules/make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+      "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/morgan": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
+      "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
+      "license": "MIT",
+      "dependencies": {
+        "basic-auth": "~2.0.1",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-finished": "~2.3.0",
+        "on-headers": "~1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/morgan/node_modules/on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/mqemitter": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-6.0.2.tgz",
+      "integrity": "sha512-8RGlznQx/Nb1xC3xKUFXHWov7pn7JdH++YVwlr6SLT6k3ft1h+ImGqZdVudbdKruFckIq9wheq9s4hgCivJDow==",
+      "license": "ISC",
+      "dependencies": {
+        "fastparallel": "^2.4.1",
+        "qlobber": "^8.0.1"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/mqemitter/node_modules/qlobber": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz",
+      "integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      }
+    },
+    "node_modules/mqtt-packet": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
+      "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
+      "license": "MIT",
+      "dependencies": {
+        "bl": "^6.0.8",
+        "debug": "^4.3.4",
+        "process-nextick-args": "^2.0.1"
+      }
+    },
+    "node_modules/mqtt-packet/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/mqtt-packet/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/multer": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
+      "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
+      "license": "MIT",
+      "dependencies": {
+        "append-field": "^1.0.0",
+        "busboy": "^1.6.0",
+        "concat-stream": "^2.0.0",
+        "type-is": "^1.6.18"
+      },
+      "engines": {
+        "node": ">= 10.16.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/multer/node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/multer/node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/multer/node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/multer/node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mysql2": {
+      "version": "3.22.0",
+      "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.0.tgz",
+      "integrity": "sha512-4jaJYBObj7FhD3lnZhqX1yDMuZN4mQNz+IolDySDXT7fbozMBpeGQNcuWXKUqo4ahkAEfkjUHPjnwuDI0/6VKw==",
+      "license": "MIT",
+      "dependencies": {
+        "aws-ssl-profiles": "^1.1.2",
+        "denque": "^2.1.0",
+        "generate-function": "^2.3.1",
+        "iconv-lite": "^0.7.2",
+        "long": "^5.3.2",
+        "lru.min": "^1.1.4",
+        "named-placeholders": "^1.1.6",
+        "sql-escaper": "^1.3.3"
+      },
+      "engines": {
+        "node": ">= 8.0"
+      },
+      "peerDependencies": {
+        "@types/node": ">= 8"
+      }
+    },
+    "node_modules/named-placeholders": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+      "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+      "license": "MIT",
+      "dependencies": {
+        "lru.min": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.4",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+      "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/nodemon": {
+      "version": "3.1.14",
+      "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+      "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^3.5.2",
+        "debug": "^4",
+        "ignore-by-default": "^1.0.1",
+        "minimatch": "^10.2.1",
+        "pstree.remy": "^1.1.8",
+        "semver": "^7.5.3",
+        "simple-update-notifier": "^2.0.0",
+        "supports-color": "^5.5.0",
+        "touch": "^3.1.0",
+        "undefsafe": "^2.0.5"
+      },
+      "bin": {
+        "nodemon": "bin/nodemon.js"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/nodemon"
+      }
+    },
+    "node_modules/nodemon/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/nodemon/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/on-headers": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+      "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.4.2",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+      "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/pstree.remy": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+      "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/qlobber": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-7.0.1.tgz",
+      "integrity": "sha512-FsFg9lMuMEFNKmTO9nV7tlyPhx8BmskPPjH2akWycuYVTtWaVwhW5yCHLJQ6Q+3mvw5cFX2vMfW2l9z2SiYAbg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.15.1",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+      "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+      "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.7.0",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+      "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+      "license": "MIT",
+      "dependencies": {
+        "abort-controller": "^3.0.0",
+        "buffer": "^6.0.3",
+        "events": "^3.3.0",
+        "process": "^0.11.10",
+        "string_decoder": "^1.3.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/retimer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/retimer/-/retimer-4.0.0.tgz",
+      "integrity": "sha512-fZIVtvbOsQsxNSDhpdPOX4lx5Ss2ni+S72AUBitARpFhtA3UzrAjQ6gDtypB2/+l7L+1VQgAgpvAKY66mElH0w==",
+      "license": "MIT",
+      "dependencies": {
+        "worker-timers": "^7.0.75"
+      }
+    },
+    "node_modules/retimer/node_modules/fast-unique-numbers": {
+      "version": "8.0.13",
+      "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz",
+      "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.23.8",
+        "tslib": "^2.6.2"
+      },
+      "engines": {
+        "node": ">=16.1.0"
+      }
+    },
+    "node_modules/retimer/node_modules/worker-timers": {
+      "version": "7.1.8",
+      "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz",
+      "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.5",
+        "tslib": "^2.6.2",
+        "worker-timers-broker": "^6.1.8",
+        "worker-timers-worker": "^7.0.71"
+      }
+    },
+    "node_modules/retimer/node_modules/worker-timers-broker": {
+      "version": "6.1.8",
+      "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz",
+      "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.5",
+        "fast-unique-numbers": "^8.0.13",
+        "tslib": "^2.6.2",
+        "worker-timers-worker": "^7.0.71"
+      }
+    },
+    "node_modules/retimer/node_modules/worker-timers-worker": {
+      "version": "7.0.71",
+      "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz",
+      "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.5",
+        "tslib": "^2.6.2"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/router/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/router/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.3",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.1",
+        "mime-types": "^3.0.2",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/send/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+      "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/simple-update-notifier": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+      "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^7.5.3"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/socket.io": {
+      "version": "4.8.3",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
+      "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "cors": "~2.8.5",
+        "debug": "~4.4.1",
+        "engine.io": "~6.6.0",
+        "socket.io-adapter": "~2.5.2",
+        "socket.io-parser": "~4.2.4"
+      },
+      "engines": {
+        "node": ">=10.2.0"
+      }
+    },
+    "node_modules/socket.io-adapter": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
+      "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "~4.4.1",
+        "ws": "~8.18.3"
+      }
+    },
+    "node_modules/socket.io-adapter/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-adapter/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/socket.io-adapter/node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
+      "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
+      "license": "MIT",
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.4.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io-parser/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/socket.io/node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/socket.io/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/socket.io/node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/socket.io/node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/socket.io/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/socket.io/node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/sql-escaper": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
+      "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
+      "license": "MIT",
+      "engines": {
+        "bun": ">=1.0.0",
+        "deno": ">=2.0.0",
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/touch": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+      "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "nodetouch": "bin/nodetouch.js"
+      }
+    },
+    "node_modules/ts-node": {
+      "version": "10.9.2",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@cspotcode/source-map-support": "^0.8.0",
+        "@tsconfig/node10": "^1.0.7",
+        "@tsconfig/node12": "^1.0.7",
+        "@tsconfig/node14": "^1.0.0",
+        "@tsconfig/node16": "^1.0.2",
+        "acorn": "^8.4.1",
+        "acorn-walk": "^8.1.1",
+        "arg": "^4.1.0",
+        "create-require": "^1.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "v8-compile-cache-lib": "^3.0.1",
+        "yn": "3.1.1"
+      },
+      "bin": {
+        "ts-node": "dist/bin.js",
+        "ts-node-cwd": "dist/bin-cwd.js",
+        "ts-node-esm": "dist/bin-esm.js",
+        "ts-node-script": "dist/bin-script.js",
+        "ts-node-transpile-only": "dist/bin-transpile.js",
+        "ts-script": "dist/bin-script-deprecated.js"
+      },
+      "peerDependencies": {
+        "@swc/core": ">=1.2.50",
+        "@swc/wasm": ">=1.2.50",
+        "@types/node": "*",
+        "typescript": ">=2.7"
+      },
+      "peerDependenciesMeta": {
+        "@swc/core": {
+          "optional": true
+        },
+        "@swc/wasm": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/type-is": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+      "license": "MIT"
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undefsafe": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+      "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+      "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+      "license": "MIT"
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/uuid-parse": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz",
+      "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==",
+      "license": "MIT"
+    },
+    "node_modules/v8-compile-cache-lib": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/validator": {
+      "version": "13.15.35",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz",
+      "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    }
+  }
+}

+ 42 - 0
mqtt-vue-dashboard/server/package.json

@@ -0,0 +1,42 @@
+{
+  "name": "mqtt-dashboard-server",
+  "version": "1.0.0",
+  "main": "dist/index.js",
+  "scripts": {
+    "build": "tsc",
+    "start": "cross-env NODE_ENV=production node --max-old-space-size=2048 dist/index.js",
+    "dev": "nodemon --exec ts-node src/index.ts"
+  },
+  "type": "commonjs",
+  "dependencies": {
+    "aedes": "^0.51.3",
+    "axios": "^1.13.2",
+    "bcryptjs": "^3.0.3",
+    "compression": "^1.8.1",
+    "cors": "^2.8.5",
+    "cross-env": "^10.1.0",
+    "dotenv": "^17.2.3",
+    "express": "^5.1.0",
+    "express-rate-limit": "^8.2.1",
+    "express-validator": "^7.3.1",
+    "helmet": "^8.1.0",
+    "jsonwebtoken": "^9.0.2",
+    "moment": "^2.30.1",
+    "morgan": "^1.10.1",
+    "multer": "^2.0.2",
+    "mysql2": "^3.15.3",
+    "socket.io": "^4.8.1"
+  },
+  "devDependencies": {
+    "@types/bcryptjs": "^2.4.6",
+    "@types/cors": "^2.8.19",
+    "@types/express": "^5.0.5",
+    "@types/jsonwebtoken": "^9.0.10",
+    "@types/morgan": "^1.9.10",
+    "@types/multer": "^2.0.0",
+    "@types/node": "^24.10.0",
+    "nodemon": "^3.1.10",
+    "ts-node": "^10.9.2",
+    "typescript": "^5.9.3"
+  }
+}

+ 68 - 0
mqtt-vue-dashboard/server/src/config/database.ts

@@ -0,0 +1,68 @@
+import mysql from 'mysql2/promise';
+
+let pool: mysql.Pool | null = null;
+
+const getDbConfig = () => ({
+  host: process.env.DB_HOST || 'localhost',
+  port: Number(process.env.DB_PORT) || 3306,
+  user: process.env.DB_USER || 'root',
+  password: process.env.DB_PASSWORD || '',
+  database: process.env.DB_NAME || 'mqtt_vue_dashboard',
+  charset: 'utf8mb4',
+  waitForConnections: true,
+  connectionLimit: 10,
+  queueLimit: 0,
+});
+
+const getPool = (): mysql.Pool => {
+  if (!pool) {
+    const dbConfig = getDbConfig();
+    console.log('创建数据库连接池:', {
+      host: dbConfig.host,
+      port: dbConfig.port,
+      user: dbConfig.user,
+      database: dbConfig.database,
+      env_DB_HOST: process.env.DB_HOST,
+      env_DB_PORT: process.env.DB_PORT,
+      env_DB_USER: process.env.DB_USER,
+      env_DB_NAME: process.env.DB_NAME
+    });
+    pool = mysql.createPool(dbConfig);
+  }
+  return pool;
+};
+
+export const testConnection = async (): Promise<boolean> => {
+  try {
+    const connection = await getPool().getConnection();
+    await connection.ping();
+    connection.release();
+    console.log('数据库连接成功');
+    return true;
+  } catch (error) {
+    console.error('数据库连接失败:', error);
+    return false;
+  }
+};
+
+export const executeQuery = async (query: string, params?: any[]): Promise<any> => {
+  try {
+    const [rows] = await getPool().execute(query, params);
+    return rows;
+  } catch (error) {
+    console.error('查询执行失败:', error);
+    throw error;
+  }
+};
+
+export const getConnection = () => {
+  return getPool();
+};
+
+export const config = {
+  testConnection,
+  getConnection,
+  executeQuery
+};
+
+export default getPool;

+ 480 - 0
mqtt-vue-dashboard/server/src/controllers/authController.ts

@@ -0,0 +1,480 @@
+import { Request, Response } from 'express';
+import jwt from 'jsonwebtoken';
+import bcrypt from 'bcryptjs';
+import { AppError } from '../middleware/errorHandler';
+import { UserModel } from '../models/user';
+import { LoggerService } from '../services/loggerService';
+
+/**
+ * 认证控制器
+ * 处理用户登录、注册、获取当前用户信息等功能
+ */
+export class AuthController {
+  /**
+   * 用户登录
+   */
+  static async login(req: Request, res: Response): Promise<void> {
+    try {
+      const { username, password } = req.body;
+
+      // 验证输入
+      if (!username || !password) {
+        throw new AppError('用户名和密码不能为空', 400);
+      }
+
+      // 查询用户
+      const user = await UserModel.getByUsername(username);
+      if (!user) {
+        // 记录登录失败日志
+        LoggerService.warn('用户登录失败 - 用户不存在', {
+          source: 'auth',
+          module: 'login',
+          details: JSON.stringify({
+            username,
+            ip: req.ip,
+            userAgent: req.get('user-agent')
+          })
+        }).catch(err => {
+          console.error('登录失败日志写入失败:', err);
+        });
+        throw new AppError('用户名或密码错误', 401);
+      }
+
+      // 验证密码
+      const isPasswordValid = await bcrypt.compare(password, user.password);
+      if (!isPasswordValid) {
+        // 记录登录失败日志
+        LoggerService.warn('用户登录失败 - 密码错误', {
+          source: 'auth',
+          module: 'login',
+          details: JSON.stringify({
+            username,
+            userId: user.id,
+            ip: req.ip,
+            userAgent: req.get('user-agent')
+          })
+        }).catch(err => {
+          console.error('登录失败日志写入失败:', err);
+        });
+        throw new AppError('用户名或密码错误', 401);
+      }
+
+      const token = jwt.sign(
+        { id: user.id, username: user.username, role: user.role },
+        process.env.JWT_SECRET || 'mqtt_dashboard_jwt_secret_key_2024',
+        { expiresIn: '7d' }
+      );
+
+      // 记录登录成功日志
+      LoggerService.info('用户登录成功', {
+        source: 'auth',
+        module: 'login',
+        details: JSON.stringify({
+          userId: user.id,
+          username: user.username,
+          role: user.role,
+          ip: req.ip,
+          userAgent: req.get('user-agent')
+        })
+      }).catch(err => {
+        console.error('登录成功日志写入失败:', err);
+      });
+
+      // 返回用户信息和令牌
+      res.status(200).json({
+        success: true,
+        message: '登录成功',
+        data: {
+          user: {
+            id: user.id,
+            username: user.username,
+            role: user.role,
+            created_at: user.created_at
+          },
+          token
+        }
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 获取当前用户信息
+   */
+  static async getCurrentUser(req: Request, res: Response): Promise<void> {
+    try {
+      // 从请求对象中获取用户ID(由authenticateToken中间件设置)
+      const userId = (req as any).user?.id;
+      if (!userId) {
+        throw new AppError('用户未登录', 401);
+      }
+
+      // 查询用户信息
+      const user = await UserModel.getById(userId);
+      if (!user) {
+        throw new AppError('用户不存在', 404);
+      }
+
+      // 返回用户信息
+      res.status(200).json({
+        success: true,
+        message: '获取用户信息成功',
+        data: {
+          id: user.id,
+          username: user.username,
+          role: user.role,
+          created_at: user.created_at
+        }
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 用户注册(可选功能,根据需要启用)
+   */
+  static async register(req: Request, res: Response): Promise<void> {
+    try {
+      const { username, password, role = 'user' } = req.body;
+
+      // 验证输入
+      if (!username || !password) {
+        throw new AppError('用户名和密码不能为空', 400);
+      }
+
+      // 检查用户名是否已存在
+      const existingUser = await UserModel.getByUsername(username);
+      if (existingUser) {
+        // 记录注册失败日志
+        LoggerService.warn('用户注册失败 - 用户名已存在', {
+          source: 'auth',
+          module: 'register',
+          details: JSON.stringify({
+            username,
+            ip: req.ip,
+            userAgent: req.get('user-agent')
+          })
+        }).catch(err => {
+          console.error('注册失败日志写入失败:', err);
+        });
+        throw new AppError('用户名已存在', 400);
+      }
+
+      // 创建用户
+      const user = await UserModel.create({
+        username,
+        password,
+        role
+      });
+
+      const token = jwt.sign(
+        { id: user.id, username: user.username, role: user.role },
+        process.env.JWT_SECRET || 'mqtt_dashboard_jwt_secret_key_2024',
+        { expiresIn: '7d' }
+      );
+
+      // 记录注册成功日志
+      LoggerService.info('用户注册成功', {
+        source: 'auth',
+        module: 'register',
+        details: JSON.stringify({
+          userId: user.id,
+          username: user.username,
+          role: user.role,
+          ip: req.ip,
+          userAgent: req.get('user-agent')
+        })
+      }).catch(err => {
+        console.error('注册成功日志写入失败:', err);
+      });
+
+      // 返回用户信息和令牌
+      res.status(201).json({
+        success: true,
+        message: '注册成功',
+        data: {
+          user: {
+            id: user.id,
+            username: user.username,
+            role: user.role,
+            created_at: user.created_at
+          },
+          token
+        }
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 刷新token
+   */
+  static async refreshToken(req: Request, res: Response): Promise<void> {
+    try {
+      // 从请求对象中获取用户ID(由authenticateToken中间件设置)
+      const userId = (req as any).user?.id;
+      if (!userId) {
+        throw new AppError('用户未登录', 401);
+      }
+
+      // 查询用户信息
+      const user = await UserModel.getById(userId);
+      if (!user) {
+        throw new AppError('用户不存在', 404);
+      }
+
+      const token = jwt.sign(
+        { id: user.id, username: user.username, role: user.role },
+        process.env.JWT_SECRET || 'mqtt_dashboard_jwt_secret_key_2024',
+        { expiresIn: '7d' }
+      );
+
+      // 返回新令牌
+      res.status(200).json({
+        success: true,
+        message: '令牌刷新成功',
+        data: {
+          token
+        }
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 修改密码
+   */
+  static async changePassword(req: Request, res: Response): Promise<void> {
+    try {
+      // 从请求对象中获取用户ID
+      const userId = (req as any).user?.id;
+      if (!userId) {
+        throw new AppError('用户未登录', 401);
+      }
+
+      const { oldPassword, newPassword } = req.body;
+
+      // 验证输入
+      if (!oldPassword || !newPassword) {
+        throw new AppError('旧密码和新密码不能为空', 400);
+      }
+
+      // 查询用户
+      const user = await UserModel.getById(userId);
+      if (!user) {
+        throw new AppError('用户不存在', 404);
+      }
+
+      // 验证旧密码
+      const isPasswordValid = await bcrypt.compare(oldPassword, user.password);
+      if (!isPasswordValid) {
+        // 记录修改密码失败日志
+        LoggerService.warn('修改密码失败 - 旧密码错误', {
+          source: 'auth',
+          module: 'change_password',
+          details: JSON.stringify({
+            userId,
+            username: user.username,
+            ip: req.ip,
+            userAgent: req.get('user-agent')
+          })
+        }).catch(err => {
+          console.error('修改密码失败日志写入失败:', err);
+        });
+        throw new AppError('旧密码错误', 401);
+      }
+
+      // 更新密码
+      await UserModel.updatePassword(userId, newPassword);
+
+      // 记录修改密码成功日志
+      LoggerService.info('用户修改密码成功', {
+        source: 'auth',
+        module: 'change_password',
+        details: JSON.stringify({
+          userId,
+          username: user.username,
+          ip: req.ip,
+          userAgent: req.get('user-agent')
+        })
+      }).catch(err => {
+        console.error('修改密码成功日志写入失败:', err);
+      });
+
+      res.status(200).json({
+        success: true,
+        message: '密码修改成功'
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 获取用户列表(仅管理员)
+   */
+  static async getUsers(req: Request, res: Response): Promise<void> {
+    try {
+      // 从请求对象中获取当前用户
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以访问', 403);
+      }
+
+      const users = await UserModel.getAll();
+      
+      // 移除密码字段
+      const sanitizedUsers = users.map(user => {
+        const { password, ...rest } = user;
+        return rest;
+      });
+
+      res.status(200).json({
+        success: true,
+        message: '获取用户列表成功',
+        data: sanitizedUsers
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 创建新用户(仅管理员)
+   */
+  static async createUser(req: Request, res: Response): Promise<void> {
+    try {
+      // 从请求对象中获取当前用户
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以创建用户', 403);
+      }
+
+      const { username, password, role, email } = req.body;
+
+      // 验证输入
+      if (!username || !password || !role) {
+        throw new AppError('用户名、密码和角色不能为空', 400);
+      }
+
+      // 检查用户名是否已存在
+      const existingUser = await UserModel.getByUsername(username);
+      if (existingUser) {
+        throw new AppError('用户名已存在', 400);
+      }
+
+      // 创建用户
+      const user = await UserModel.create({ username, password, role, email });
+      
+      // 移除密码字段
+      const { password: _, ...sanitizedUser } = user;
+
+      res.status(201).json({
+        success: true,
+        message: '用户创建成功',
+        data: sanitizedUser
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 更新用户(仅管理员)
+   */
+  static async updateUser(req: Request, res: Response): Promise<void> {
+    try {
+      // 从请求对象中获取当前用户
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以更新用户', 403);
+      }
+
+      const { id } = req.params;
+      const { username, role, email } = req.body;
+
+      // 验证输入
+      if (!id) {
+        throw new AppError('用户ID不能为空', 400);
+      }
+
+      // 检查用户是否存在
+      const existingUser = await UserModel.getById(id);
+      if (!existingUser) {
+        throw new AppError('用户不存在', 404);
+      }
+
+      // 更新用户
+      const updatedUser = await UserModel.update(id, { username, role, email });
+      
+      if (!updatedUser) {
+        throw new AppError('用户更新失败', 500);
+      }
+      
+      // 移除密码字段
+      const { password: _, ...sanitizedUser } = updatedUser;
+
+      res.status(200).json({
+        success: true,
+        message: '用户更新成功',
+        data: sanitizedUser
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 删除用户(仅管理员)
+   */
+  static async deleteUser(req: Request, res: Response): Promise<void> {
+    try {
+      // 从请求对象中获取当前用户
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以删除用户', 403);
+      }
+
+      const { id } = req.params;
+
+      // 验证输入
+      if (!id) {
+        throw new AppError('用户ID不能为空', 400);
+      }
+
+      // 检查用户是否存在
+      const existingUser = await UserModel.getById(id);
+      if (!existingUser) {
+        throw new AppError('用户不存在', 404);
+      }
+
+      // 只禁止删除默认的超级管理员用户(username为'admin')
+      if (existingUser.username === 'admin') {
+        throw new AppError('不允许删除超级管理员用户', 400);
+      }
+
+      // 不允许删除自己
+      if (existingUser.id === currentUser.id) {
+        throw new AppError('不允许删除当前登录用户', 400);
+      }
+
+      // 删除用户
+      const success = await UserModel.delete(Number(id));
+
+      if (!success) {
+        throw new AppError('用户删除失败', 500);
+      }
+
+      res.status(200).json({
+        success: true,
+        message: '用户删除成功'
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+}

+ 533 - 0
mqtt-vue-dashboard/server/src/controllers/authLogController.ts

@@ -0,0 +1,533 @@
+import { Request, Response } from 'express';
+import { AuthLogModel } from '../models/authLog';
+import { toString } from '../utils/helpers';
+
+export class AuthLogController {
+  // 获取所有认证日志
+  static async getAllAuthLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const authLogs = await AuthLogModel.getAll(limit, offset);
+      const total = await AuthLogModel.getCount();
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取认证日志列表失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取认证日志列表失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据ID获取认证日志
+  static async getAuthLogById(req: Request, res: Response): Promise<void> {
+    try {
+      const { id } = req.params;
+      
+      if (!id || isNaN(Number(id))) {
+        res.status(400).json({
+          success: false,
+          message: '无效的ID'
+        });
+        return;
+      }
+      
+      const authLog = await AuthLogModel.getById(Number(id));
+      
+      if (!authLog) {
+        res.status(404).json({
+          success: false,
+          message: '认证日志不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: authLog
+      });
+    } catch (error) {
+      console.error('获取认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据客户端ID获取认证日志
+  static async getAuthLogsByClientId(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const clientidStr = toString(clientid);
+      
+      if (!clientidStr) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID不能为空'
+        });
+        return;
+      }
+      
+      const authLogs = await AuthLogModel.getByClientid(clientidStr, limit, offset);
+      const total = await AuthLogModel.getCountByClientid(clientidStr);
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据客户端ID获取认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据客户端ID获取认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据用户名获取认证日志
+  static async getAuthLogsByUsername(req: Request, res: Response): Promise<void> {
+    try {
+      const { username } = req.params;
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const usernameStr = toString(username);
+      
+      if (!usernameStr) {
+        res.status(400).json({
+          success: false,
+          message: '用户名不能为空'
+        });
+        return;
+      }
+      
+      const authLogs = await AuthLogModel.getByUsername(usernameStr, limit, offset);
+      const total = await AuthLogModel.getCountByUsername(usernameStr);
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据用户名获取认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据用户名获取认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据IP地址获取认证日志
+  static async getAuthLogsByIpAddress(req: Request, res: Response): Promise<void> {
+    try {
+      const { ipAddress } = req.params;
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const ipAddressStr = toString(ipAddress);
+      
+      if (!ipAddressStr) {
+        res.status(400).json({
+          success: false,
+          message: 'IP地址不能为空'
+        });
+        return;
+      }
+      
+      const authLogs = await AuthLogModel.getByIpAddress(ipAddressStr, limit, offset);
+      const total = await AuthLogModel.getCountByIpAddress(ipAddressStr);
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据IP地址获取认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据IP地址获取认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据操作类型获取认证日志
+  static async getAuthLogsByOperationType(req: Request, res: Response): Promise<void> {
+    try {
+      const { operationType } = req.params;
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const operationTypeStr = toString(operationType);
+      
+      if (!operationTypeStr) {
+        res.status(400).json({
+          success: false,
+          message: '操作类型不能为空'
+        });
+        return;
+      }
+      
+      if (!['connect', 'publish', 'subscribe', 'disconnect'].includes(operationTypeStr)) {
+        res.status(400).json({
+          success: false,
+          message: '操作类型必须是connect、publish、subscribe或disconnect之一'
+        });
+        return;
+      }
+      
+      const authLogs = await AuthLogModel.getByOperationType(operationTypeStr, limit, offset);
+      const total = await AuthLogModel.getCountByOperationType(operationTypeStr);
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据操作类型获取认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据操作类型获取认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据结果获取认证日志
+  static async getAuthLogsByResult(req: Request, res: Response): Promise<void> {
+    try {
+      const result = toString(req.params.result);
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      if (!result) {
+        res.status(400).json({
+          success: false,
+          message: '结果不能为空'
+        });
+        return;
+      }
+      
+      // 验证结果
+      if (!['success', 'failure'].includes(result)) {
+        res.status(400).json({
+          success: false,
+          message: '结果必须是success或failure之一'
+        });
+        return;
+      }
+      
+      const authLogs = await AuthLogModel.getByResult(result, limit, offset);
+      const total = await AuthLogModel.getCountByResult(result);
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据结果获取认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据结果获取认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据时间范围获取认证日志
+  static async getAuthLogsByTimeRange(req: Request, res: Response): Promise<void> {
+    try {
+      const { start_time, end_time } = req.query;
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      if (!start_time || !end_time) {
+        res.status(400).json({
+          success: false,
+          message: '开始时间和结束时间不能为空'
+        });
+        return;
+      }
+      
+      const startDate = new Date(start_time as string);
+      const endDate = new Date(end_time as string);
+      
+      if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
+        res.status(400).json({
+          success: false,
+          message: '无效的时间格式'
+        });
+        return;
+      }
+      
+      if (startDate >= endDate) {
+        res.status(400).json({
+          success: false,
+          message: '开始时间必须早于结束时间'
+        });
+        return;
+      }
+      
+      const authLogs = await AuthLogModel.getByTimeRange(startDate, endDate, limit, offset);
+      const total = await AuthLogModel.getCountByTimeRange(startDate, endDate);
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据时间范围获取认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据时间范围获取认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据多条件查询认证日志
+  static async getAuthLogsByMultipleConditions(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid, username, ip_address, operation_type, result, start_time, end_time } = req.query;
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      // 构建查询条件
+      const conditions: any = {};
+      
+      // 修复:检查参数是否存在且不为空字符串
+      if (clientid !== undefined && clientid !== '') conditions.clientid = clientid as string;
+      if (username !== undefined && username !== '') conditions.username = username as string;
+      if (ip_address !== undefined && ip_address !== '') conditions.ip_address = ip_address as string;
+      if (operation_type !== undefined && operation_type !== '') {
+        if (!['connect', 'publish', 'subscribe', 'disconnect'].includes(operation_type as string)) {
+          res.status(400).json({
+            success: false,
+            message: '操作类型必须是connect、publish、subscribe或disconnect之一'
+          });
+          return;
+        }
+        conditions.operation_type = operation_type as string;
+      }
+      if (result !== undefined && result !== '') {
+        if (!['success', 'failure'].includes(result as string)) {
+          res.status(400).json({
+            success: false,
+            message: '结果必须是success或failure之一'
+          });
+          return;
+        }
+        conditions.result = result as string;
+      }
+      
+      let startDate, endDate;
+      if (start_time && end_time) {
+        startDate = new Date(start_time as string);
+        endDate = new Date(end_time as string);
+        
+        if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
+          res.status(400).json({
+            success: false,
+            message: '无效的时间格式'
+          });
+          return;
+        }
+        
+        if (startDate >= endDate) {
+          res.status(400).json({
+            success: false,
+            message: '开始时间必须早于结束时间'
+          });
+          return;
+        }
+      }
+      
+      // 指定需要模糊查询的字段
+      const fuzzyFields = ['clientid', 'username', 'ip_address'];
+      
+      const authLogs = await AuthLogModel.getByMultipleConditions(
+        conditions, 
+        startDate, 
+        endDate, 
+        limit, 
+        offset,
+        fuzzyFields
+      );
+      const total = await AuthLogModel.getCountByMultipleConditions(
+        conditions, 
+        startDate, 
+        endDate,
+        fuzzyFields
+      );
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据多条件查询认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据多条件查询认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取认证日志统计信息
+  static async getAuthLogStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await AuthLogModel.getFullStats();
+      
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取认证日志统计信息成功'
+      });
+    } catch (error) {
+      console.error('获取认证日志统计信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取认证日志统计信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取最近认证日志
+  static async getRecentAuthLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const limit = Number(req.query.limit) || 10;
+      
+      if (limit > 100) {
+        res.status(400).json({
+          success: false,
+          message: '限制数量不能超过100'
+        });
+        return;
+      }
+      
+      const authLogs = await AuthLogModel.getRecent(limit);
+      
+      res.status(200).json({
+        success: true,
+        data: authLogs,
+        message: '获取最近认证日志成功'
+      });
+    } catch (error) {
+      console.error('获取最近认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取最近认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 清理旧认证日志
+  static async cleanupOldAuthLogs(req: Request, res: Response): Promise<void> {
+    try {
+      // 支持从查询参数或请求体中获取days参数
+      const { days } = req.query;
+      let daysToDelete = days;
+      
+      // 如果查询参数中没有days,尝试从请求体中获取
+      if (!daysToDelete && req.body && req.body.days) {
+        daysToDelete = req.body.days;
+      }
+      
+      if (!daysToDelete || isNaN(Number(daysToDelete)) || Number(daysToDelete) < 1) {
+        res.status(400).json({
+          success: false,
+          message: '请提供有效的天数(大于0的数字)'
+        });
+        return;
+      }
+      
+      const deletedCount = await AuthLogModel.cleanupOldLogs(Number(daysToDelete));
+      
+      res.status(200).json({
+        success: true,
+        data: { deletedCount },
+        message: `成功清理${deletedCount}条${daysToDelete}天前的认证日志`
+      });
+    } catch (error) {
+      console.error('清理旧认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '清理旧认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+}

+ 441 - 0
mqtt-vue-dashboard/server/src/controllers/clientAclController.ts

@@ -0,0 +1,441 @@
+import { Request, Response } from 'express';
+import { ClientAclModel } from '../models/clientAcl';
+import { toString } from '../utils/helpers';
+
+export class ClientAclController {
+  // 获取所有客户端授权规则
+  static async getAllClientAcl(req: Request, res: Response): Promise<void> {
+    try {
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const clientAcls = await ClientAclModel.getAll(limit, offset);
+      const total = await ClientAclModel.getCount();
+      
+      res.status(200).json({
+        success: true,
+        data: clientAcls,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取客户端授权规则列表失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取客户端授权规则列表失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据ID获取客户端授权规则
+  static async getClientAclById(req: Request, res: Response): Promise<void> {
+    try {
+      const { id } = req.params;
+      
+      if (!id || isNaN(Number(id))) {
+        res.status(400).json({
+          success: false,
+          message: '无效的ID'
+        });
+        return;
+      }
+      
+      const clientAcl = await ClientAclModel.getById(Number(id));
+      
+      if (!clientAcl) {
+        res.status(404).json({
+          success: false,
+          message: '客户端授权规则不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: clientAcl
+      });
+    } catch (error) {
+      console.error('获取客户端授权规则失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取客户端授权规则失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据用户名获取授权规则
+  static async getClientAclByUsername(req: Request, res: Response): Promise<void> {
+    try {
+      const { username } = req.params;
+      
+      const usernameStr = toString(username);
+      
+      if (!usernameStr) {
+        res.status(400).json({
+          success: false,
+          message: '用户名不能为空'
+        });
+        return;
+      }
+      
+      const clientAcls = await ClientAclModel.getByUsername(usernameStr);
+      
+      res.status(200).json({
+        success: true,
+        data: clientAcls
+      });
+    } catch (error) {
+      console.error('根据用户名获取授权规则失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据用户名获取授权规则失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据主题获取授权规则
+  static async getClientAclByTopic(req: Request, res: Response): Promise<void> {
+    try {
+      const { topic } = req.params;
+      
+      const topicStr = toString(topic);
+      
+      if (!topicStr) {
+        res.status(400).json({
+          success: false,
+          message: '主题不能为空'
+        });
+        return;
+      }
+      
+      const clientAcls = await ClientAclModel.getByTopic(topicStr);
+      
+      res.status(200).json({
+        success: true,
+        data: clientAcls
+      });
+    } catch (error) {
+      console.error('根据主题获取授权规则失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据主题获取授权规则失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 创建客户端授权规则
+  static async createClientAcl(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid, username, topic, action, permission, priority, description } = req.body;
+      
+      if (!username || !topic || !action || !permission) {
+        res.status(400).json({
+          success: false,
+          message: '用户名、主题、操作和权限不能为空'
+        });
+        return;
+      }
+      
+      // 验证action和permission的值
+      if (!['publish', 'subscribe', 'pubsub'].includes(action)) {
+        res.status(400).json({
+          success: false,
+          message: '操作必须是publish、subscribe或pubsub之一'
+        });
+        return;
+      }
+      
+      if (!['allow', 'deny'].includes(permission)) {
+        res.status(400).json({
+          success: false,
+          message: '权限必须是allow或deny之一'
+        });
+        return;
+      }
+      
+      // 创建客户端授权规则
+      const newClientAcl = await ClientAclModel.create({
+        clientid: clientid || null,
+        username,
+        topic,
+        action,
+        permission,
+        priority: priority || 0,
+        description: description || null
+      });
+      
+      res.status(201).json({
+        success: true,
+        data: newClientAcl,
+        message: '客户端授权规则创建成功'
+      });
+    } catch (error) {
+      console.error('创建客户端授权规则失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '创建客户端授权规则失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 更新客户端授权规则
+  static async updateClientAcl(req: Request, res: Response): Promise<void> {
+    try {
+      const { id } = req.params;
+      const { username, topic, action, permission, priority, description } = req.body;
+      
+      if (!id || isNaN(Number(id))) {
+        res.status(400).json({
+          success: false,
+          message: '无效的ID'
+        });
+        return;
+      }
+      
+      // 检查客户端授权规则是否存在
+      const existingClientAcl = await ClientAclModel.getById(Number(id));
+      if (!existingClientAcl) {
+        res.status(404).json({
+          success: false,
+          message: '客户端授权规则不存在'
+        });
+        return;
+      }
+      
+      // 验证action和permission的值
+      if (action && !['publish', 'subscribe', 'pubsub'].includes(action)) {
+        res.status(400).json({
+          success: false,
+          message: '操作必须是publish、subscribe或pubsub之一'
+        });
+        return;
+      }
+      
+      if (permission && !['allow', 'deny'].includes(permission)) {
+        res.status(400).json({
+          success: false,
+          message: '权限必须是allow或deny之一'
+        });
+        return;
+      }
+      
+      // 更新客户端授权规则
+      const updatedClientAcl = await ClientAclModel.update(Number(id), {
+        username,
+        topic,
+        action,
+        permission,
+        priority,
+        description
+      });
+      
+      res.status(200).json({
+        success: true,
+        data: updatedClientAcl,
+        message: '客户端授权规则更新成功'
+      });
+    } catch (error) {
+      console.error('更新客户端授权规则失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '更新客户端授权规则失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 删除客户端授权规则
+  static async deleteClientAcl(req: Request, res: Response): Promise<void> {
+    try {
+      const { id } = req.params;
+      
+      if (!id || isNaN(Number(id))) {
+        res.status(400).json({
+          success: false,
+          message: '无效的ID'
+        });
+        return;
+      }
+      
+      // 检查客户端授权规则是否存在
+      const existingClientAcl = await ClientAclModel.getById(Number(id));
+      if (!existingClientAcl) {
+        res.status(404).json({
+          success: false,
+          message: '客户端授权规则不存在'
+        });
+        return;
+      }
+      
+      // 删除客户端授权规则
+      await ClientAclModel.delete(Number(id));
+      
+      res.status(200).json({
+        success: true,
+        message: '客户端授权规则删除成功'
+      });
+    } catch (error) {
+      console.error('删除客户端授权规则失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '删除客户端授权规则失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 批量删除客户端授权规则
+  static async deleteMultipleClientAcl(req: Request, res: Response): Promise<void> {
+    try {
+      const { ids } = req.body;
+      
+      if (!ids || !Array.isArray(ids) || ids.length === 0) {
+        res.status(400).json({
+          success: false,
+          message: '请提供有效的ID列表'
+        });
+        return;
+      }
+      
+      // 验证所有ID是否为数字
+      const validIds = ids.filter(id => !isNaN(Number(id)));
+      if (validIds.length !== ids.length) {
+        res.status(400).json({
+          success: false,
+          message: 'ID列表包含无效的ID'
+        });
+        return;
+      }
+      
+      // 批量删除客户端授权规则
+      await ClientAclModel.deleteMultiple(validIds.map(id => Number(id)));
+      
+      res.status(200).json({
+        success: true,
+        message: `成功删除${validIds.length}条客户端授权规则`
+      });
+    } catch (error) {
+      console.error('批量删除客户端授权规则失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '批量删除客户端授权规则失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据用户名和操作类型获取授权规则
+  static async getClientAclByUsernameAndAction(req: Request, res: Response): Promise<void> {
+    try {
+      const { username, action } = req.params;
+      
+      const usernameStr = toString(username);
+      const actionStr = toString(action);
+      
+      if (!usernameStr || !actionStr) {
+        res.status(400).json({
+          success: false,
+          message: '用户名和操作类型不能为空'
+        });
+        return;
+      }
+      
+      if (!['publish', 'subscribe', 'pubsub'].includes(actionStr)) {
+        res.status(400).json({
+          success: false,
+          message: '操作必须是publish、subscribe或pubsub之一'
+        });
+        return;
+      }
+      
+      const clientAcls = await ClientAclModel.getByUsernameAndAction(usernameStr, actionStr);
+      
+      res.status(200).json({
+        success: true,
+        data: clientAcls
+      });
+    } catch (error) {
+      console.error('根据用户名和操作类型获取授权规则失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据用户名和操作类型获取授权规则失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 检查用户是否有权限访问特定主题
+  static async checkUserPermission(req: Request, res: Response): Promise<void> {
+    try {
+      const { username, topic, action } = req.body;
+      
+      if (!username || !topic || !action) {
+        res.status(400).json({
+          success: false,
+          message: '用户名、主题和操作类型不能为空'
+        });
+        return;
+      }
+      
+      // 验证action的值
+      if (!['publish', 'subscribe'].includes(action)) {
+        res.status(400).json({
+          success: false,
+          message: '操作必须是publish或subscribe之一'
+        });
+        return;
+      }
+      
+      // 检查用户权限
+      const hasPermission = await ClientAclModel.checkPermission(username, topic, action);
+      
+      res.status(200).json({
+        success: true,
+        data: {
+          username,
+          topic,
+          action,
+          hasPermission
+        },
+        message: `用户${hasPermission ? '有' : '没有'}权限${action === 'publish' ? '发布到' : '订阅'}主题${topic}`
+      });
+    } catch (error) {
+      console.error('检查用户权限失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '检查用户权限失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取客户端授权统计信息
+  static async getClientAclStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await ClientAclModel.getPermissionStats();
+      
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取客户端授权统计信息成功'
+      });
+    } catch (error) {
+      console.error('获取客户端授权统计信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取客户端授权统计信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+}

+ 1276 - 0
mqtt-vue-dashboard/server/src/controllers/clientAuthController.ts

@@ -0,0 +1,1276 @@
+import { Request, Response } from 'express';
+import { ClientAuthModel, ClientAuth, AuthMethod, AuthPolicy, ClientToken, ClientCertificate } from '../models/clientAuth';
+import { AuthLogModel } from '../models/authLog';
+import { toString } from '../utils/helpers';
+
+export class ClientAuthController {
+  // 获取所有客户端认证信息
+  static async getAllClientAuth(req: Request, res: Response): Promise<void> {
+    try {
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const clientAuths = await ClientAuthModel.getAll(limit, offset);
+      const total = await ClientAuthModel.getCount();
+      
+      res.status(200).json({
+        success: true,
+        data: clientAuths,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取客户端认证列表失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取客户端认证列表失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据ID获取客户端认证信息
+  static async getClientAuthById(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      
+      if (!id || isNaN(Number(id))) {
+        res.status(400).json({
+          success: false,
+          message: '无效的ID'
+        });
+        return;
+      }
+      
+      const clientAuth = await ClientAuthModel.getById(Number(id));
+      
+      if (!clientAuth) {
+        res.status(404).json({
+          success: false,
+          message: '客户端认证信息不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: clientAuth
+      });
+    } catch (error) {
+      console.error('获取客户端认证信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取客户端认证信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据用户名获取客户端认证信息
+  static async getClientAuthByUsername(req: Request, res: Response): Promise<void> {
+    try {
+      const { username } = req.params;
+      
+      const usernameStr = toString(username);
+      
+      if (!usernameStr) {
+        res.status(400).json({
+          success: false,
+          message: '用户名不能为空'
+        });
+        return;
+      }
+      
+      const clientAuth = await ClientAuthModel.getByUsername(usernameStr);
+      
+      if (!clientAuth) {
+        res.status(404).json({
+          success: false,
+          message: '客户端认证信息不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: clientAuth
+      });
+    } catch (error) {
+      console.error('根据用户名获取客户端认证信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据用户名获取客户端认证信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据客户端ID获取客户端认证信息
+  static async getClientAuthByClientId(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      
+      const clientidStr = toString(clientid);
+      
+      if (!clientidStr) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID不能为空'
+        });
+        return;
+      }
+      
+      const clientAuth = await ClientAuthModel.getByClientId(clientidStr);
+      
+      if (!clientAuth) {
+        res.status(404).json({
+          success: false,
+          message: '客户端认证信息不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: clientAuth
+      });
+    } catch (error) {
+      console.error('根据客户端ID获取客户端认证信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据客户端ID获取客户端认证信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 创建客户端认证信息
+  static async createClientAuth(req: Request, res: Response): Promise<void> {
+    try {
+      const { 
+        username, 
+        clientid, 
+        password, 
+        device_type, 
+        description, 
+        is_superuser, 
+        use_salt,
+        auth_method,
+        auth_expiry,
+        allowed_ip_ranges,
+        allowed_time_ranges,
+        auth_policy_id
+      } = req.body;
+      
+      // 验证必填字段
+      if (!username || !clientid || !password) {
+        res.status(400).json({
+          success: false,
+          message: '用户名、客户端ID和密码不能为空'
+        });
+        return;
+      }
+      
+      // 检查用户名和客户端ID是否已存在
+      const existingByUsername = await ClientAuthModel.getByUsername(username);
+      if (existingByUsername) {
+        res.status(400).json({
+          success: false,
+          message: '用户名已存在'
+        });
+        return;
+      }
+      
+      const existingByClientId = await ClientAuthModel.getByClientId(clientid);
+      if (existingByClientId) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID已存在'
+        });
+        return;
+      }
+      
+      // 创建客户端认证信息
+      const shouldUseSalt = use_salt !== undefined ? Boolean(use_salt) : true; // 默认为true
+      
+      // 生成盐值和密码哈希
+      const salt = shouldUseSalt ? ClientAuthModel.generateSalt() : '';
+      const passwordHash = ClientAuthModel.generatePasswordHash(password, salt, shouldUseSalt);
+      
+      const newClientAuth = await ClientAuthModel.create({
+        username,
+        clientid,
+        password_hash: passwordHash,
+        salt: salt, // 对于不加盐的情况,salt为空字符串
+        status: 'enabled',
+        device_type: device_type || null,
+        description: description || null,
+        is_superuser: is_superuser || false,
+        use_salt: shouldUseSalt,
+        auth_method: auth_method || 'password',
+        auth_expiry: auth_expiry ? new Date(auth_expiry) : null,
+        allowed_ip_ranges: allowed_ip_ranges ? JSON.stringify(allowed_ip_ranges) : null,
+        allowed_time_ranges: allowed_time_ranges ? JSON.stringify(allowed_time_ranges) : null,
+        auth_policy_id: auth_policy_id || null
+      });
+      
+      // 记录创建日志
+      await ClientAuthModel.logAuthEvent(
+        newClientAuth.clientid,
+        newClientAuth.username,
+        'connect', // 使用数据库允许的值
+        'success',
+        'Client authentication created',
+        req.ip,
+        undefined,
+        auth_method,
+        auth_policy_id
+      );
+      
+      res.status(201).json({
+        success: true,
+        data: newClientAuth,
+        message: '客户端认证信息创建成功'
+      });
+    } catch (error) {
+      console.error('创建客户端认证信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '创建客户端认证信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 更新客户端认证信息
+  static async updateClientAuth(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      const { 
+        username, 
+        clientid, 
+        password, 
+        device_type, 
+        description, 
+        status, 
+        is_superuser, 
+        use_salt,
+        auth_method,
+        auth_expiry,
+        allowed_ip_ranges,
+        allowed_time_ranges,
+        auth_policy_id
+      } = req.body;
+      
+      if (!id || isNaN(Number(id))) {
+        res.status(400).json({
+          success: false,
+          message: '无效的ID'
+        });
+        return;
+      }
+      
+      // 检查客户端认证信息是否存在
+      const existingClientAuth = await ClientAuthModel.getById(Number(id));
+      if (!existingClientAuth) {
+        res.status(404).json({
+          success: false,
+          message: '客户端认证信息不存在'
+        });
+        return;
+      }
+      
+      // 如果更新了用户名或客户端ID,检查是否与其他记录冲突
+      if (username && username !== existingClientAuth.username) {
+        const existingByUsername = await ClientAuthModel.getByUsername(username);
+        if (existingByUsername) {
+          res.status(400).json({
+            success: false,
+            message: '用户名已存在'
+          });
+          return;
+        }
+      }
+      
+      if (clientid && clientid !== existingClientAuth.clientid) {
+        const existingByClientId = await ClientAuthModel.getByClientId(clientid);
+        if (existingByClientId) {
+          res.status(400).json({
+            success: false,
+            message: '客户端ID已存在'
+          });
+          return;
+        }
+      }
+      
+      // 更新客户端认证信息
+      const updateData: any = {};
+      
+      if (username !== undefined && username !== existingClientAuth.username) {
+        updateData.username = username;
+      }
+      
+      if (clientid !== undefined && clientid !== existingClientAuth.clientid) {
+        updateData.clientid = clientid;
+      }
+      
+      if (password !== undefined) {
+        // 如果提供了新密码,根据use_salt参数决定是否使用盐值
+        const shouldUseSalt = use_salt !== undefined ? Boolean(use_salt) : existingClientAuth.use_salt;
+        const salt = shouldUseSalt ? ClientAuthModel.generateSalt() : '';
+        const passwordHash = ClientAuthModel.generatePasswordHash(password, salt, shouldUseSalt);
+        updateData.password_hash = passwordHash;
+        updateData.salt = salt; // 对于不加盐的情况,salt为空字符串
+      }
+      
+      if (device_type !== undefined) {
+        updateData.device_type = device_type;
+      }
+      
+      if (description !== undefined) {
+        updateData.description = description;
+      }
+      
+      if (status !== undefined) {
+        updateData.status = status;
+      }
+      
+      if (is_superuser !== undefined) {
+        updateData.is_superuser = is_superuser;
+      }
+      
+      if (use_salt !== undefined) {
+        updateData.use_salt = Boolean(use_salt);
+        
+        // 如果修改了use_salt但没有提供新密码,需要更新salt字段
+        if (password === undefined) {
+          const shouldUseSalt = Boolean(use_salt);
+          const salt = shouldUseSalt ? ClientAuthModel.generateSalt() : '';
+          updateData.salt = salt;
+        }
+      }
+      
+      if (auth_method !== undefined) {
+        updateData.auth_method = auth_method;
+      }
+      
+      if (auth_expiry !== undefined) {
+        updateData.auth_expiry = auth_expiry ? new Date(auth_expiry) : null;
+      }
+      
+      if (allowed_ip_ranges !== undefined) {
+        updateData.allowed_ip_ranges = allowed_ip_ranges ? JSON.stringify(allowed_ip_ranges) : null;
+      }
+      
+      if (allowed_time_ranges !== undefined) {
+        updateData.allowed_time_ranges = allowed_time_ranges ? JSON.stringify(allowed_time_ranges) : null;
+      }
+      
+      if (auth_policy_id !== undefined) {
+        updateData.auth_policy_id = auth_policy_id;
+      }
+      
+      const updatedClientAuth = await ClientAuthModel.update(Number(id), updateData);
+      
+      if (!updatedClientAuth) {
+        res.status(500).json({
+          success: false,
+          message: '更新客户端认证信息失败'
+        });
+        return;
+      }
+      
+      // 记录更新日志
+      await ClientAuthModel.logAuthEvent(
+        updatedClientAuth.clientid,
+        updatedClientAuth.username,
+        'connect', // 使用数据库允许的值
+        'success',
+        'Client authentication updated',
+        req.ip,
+        undefined,
+        updatedClientAuth.auth_method,
+        updatedClientAuth.auth_policy_id || undefined
+      );
+      
+      res.status(200).json({
+        success: true,
+        data: updatedClientAuth,
+        message: '客户端认证信息更新成功'
+      });
+    } catch (error) {
+      console.error('更新客户端认证信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '更新客户端认证信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 删除客户端认证信息
+  static async deleteClientAuth(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      
+      if (!id || isNaN(Number(id))) {
+        res.status(400).json({
+          success: false,
+          message: '无效的ID'
+        });
+        return;
+      }
+      
+      // 检查客户端认证信息是否存在
+      const existingClientAuth = await ClientAuthModel.getById(Number(id));
+      if (!existingClientAuth) {
+        res.status(404).json({
+          success: false,
+          message: '客户端认证信息不存在'
+        });
+        return;
+      }
+      
+      // 删除客户端认证信息
+      await ClientAuthModel.delete(Number(id));
+      
+      // 记录删除日志
+      await ClientAuthModel.logAuthEvent(
+        existingClientAuth.clientid,
+        existingClientAuth.username,
+        'connect', // 使用数据库允许的值
+        'success',
+        'Client authentication deleted',
+        req.ip
+      );
+      
+      res.status(200).json({
+        success: true,
+        message: '客户端认证信息删除成功'
+      });
+    } catch (error) {
+      console.error('删除客户端认证信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '删除客户端认证信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 验证客户端认证信息
+  static async verifyClientAuth(req: Request, res: Response): Promise<void> {
+    try {
+      const { username, clientid, password } = req.body;
+      
+      if (!username || !clientid || !password) {
+        res.status(400).json({
+          success: false,
+          message: '用户名、客户端ID和密码不能为空'
+        });
+        return;
+      }
+      
+      const startTime = Date.now();
+      const isValid = await ClientAuthModel.verifyClient(username, clientid, password);
+      const executionTime = Date.now() - startTime;
+      
+      // 记录认证日志
+      await ClientAuthModel.logAuthEvent(
+        clientid,
+        username,
+        'connect', // 修改为数据库允许的值
+        isValid ? 'success' : 'failure',
+        isValid ? undefined : 'Invalid credentials',
+        req.ip,
+        undefined,
+        'password',
+        undefined,
+        executionTime
+      );
+      
+      if (isValid) {
+        res.status(200).json({
+          success: true,
+          message: '客户端认证信息验证成功'
+        });
+      } else {
+        res.status(401).json({
+          success: false,
+          message: '客户端认证信息验证失败'
+        });
+      }
+    } catch (error) {
+      console.error('验证客户端认证信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '验证客户端认证信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // MQTT Password-Based HTTP认证接口
+  static async mqttPasswordAuth(req: Request, res: Response): Promise<void> {
+    try {
+      const { username, clientid, password } = req.body;
+      
+      if (!username || !password) {
+        res.status(200).json({
+          result: false,
+          reason: '用户名和密码不能为空'
+        });
+        return;
+      }
+      
+      // 直接尝试常规密码验证
+      const clientAuth = await ClientAuthModel.getByUsername(username);
+      
+      if (!clientAuth) {
+        res.status(200).json({
+          result: false,
+          reason: '用户不存在'
+        });
+        return;
+      }
+      
+      if (clientAuth.status !== 'enabled') {
+        res.status(200).json({
+          result: false,
+          reason: '用户已被禁用'
+        });
+        return;
+      }
+      
+      // 常规密码验证
+      const useSalt = clientAuth.use_salt !== undefined ? clientAuth.use_salt : true;
+      const isValidPassword = ClientAuthModel.verifyPassword(password, clientAuth.salt, clientAuth.password_hash, useSalt);
+      
+      if (!isValidPassword) {
+        res.status(200).json({
+          result: false,
+          reason: '密码无效'
+        });
+        return;
+      }
+      
+      // 记录认证日志
+      await ClientAuthModel.logAuthEvent(
+        clientAuth.clientid,
+        username,
+        'connect',
+        'success',
+        '常规密码认证成功',
+        req.ip
+      );
+      
+      // 返回MQTT Broker期望的格式
+      res.status(200).json({
+        result: true,
+        is_superuser: clientAuth.is_superuser === true,
+        acl: [] // 这里可以根据需要添加ACL规则
+      });
+    } catch (error) {
+      console.error('MQTT密码认证失败:', error);
+      res.status(200).json({
+        result: false,
+        reason: '认证服务内部错误'
+      });
+    }
+  }
+
+  // 获取客户端认证统计信息
+  static async getClientAuthStats(req: Request, res: Response): Promise<void> {
+    try {
+      const statusStats = await ClientAuthModel.getStatusStats();
+      const deviceTypeStats = await ClientAuthModel.getDeviceTypeStats();
+      
+      res.status(200).json({
+        success: true,
+        data: {
+          status: statusStats,
+          deviceType: deviceTypeStats
+        },
+        message: '获取客户端认证统计信息成功'
+      });
+    } catch (error) {
+      console.error('获取客户端认证统计信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取客户端认证统计信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 认证方法相关接口
+
+  // 获取所有认证方法
+  static async getAuthMethods(req: Request, res: Response): Promise<void> {
+    try {
+      const methods = await ClientAuthModel.getAuthMethods();
+      
+      res.status(200).json({
+        success: true,
+        data: methods
+      });
+    } catch (error) {
+      console.error('获取认证方法失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取认证方法失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据ID获取认证方法
+  static async getAuthMethodById(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      const method = await ClientAuthModel.getAuthMethodById(parseInt(id));
+      
+      if (!method) {
+        res.status(404).json({
+          success: false,
+          message: '认证方法不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: method
+      });
+    } catch (error) {
+      console.error('获取认证方法失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取认证方法失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 创建认证方法
+  static async createAuthMethod(req: Request, res: Response): Promise<void> {
+    try {
+      const { method_name, method_type, config, is_active } = req.body;
+      
+      // 验证必填字段
+      if (!method_name || !method_type || !config) {
+        res.status(400).json({
+          success: false,
+          message: '方法名称、类型和配置为必填项'
+        });
+        return;
+      }
+      
+      // 检查方法名称是否已存在
+      const existingMethod = await ClientAuthModel.getAuthMethodByName(method_name);
+      if (existingMethod) {
+        res.status(400).json({
+          success: false,
+          message: '认证方法名称已存在'
+        });
+        return;
+      }
+      
+      // 创建认证方法
+      const authMethodData: Omit<AuthMethod, 'id' | 'created_at' | 'updated_at'> = {
+        method_name,
+        method_type,
+        config: JSON.stringify(config),
+        is_active: is_active !== undefined ? is_active : true
+      };
+      
+      const newMethod = await ClientAuthModel.createAuthMethod(authMethodData);
+      
+      res.status(201).json({
+        success: true,
+        message: '认证方法创建成功',
+        data: newMethod
+      });
+    } catch (error) {
+      console.error('创建认证方法失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '创建认证方法失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 更新认证方法
+  static async updateAuthMethod(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      const updateData = req.body;
+      
+      // 处理JSON字段
+      if (updateData.config) {
+        updateData.config = JSON.stringify(updateData.config);
+      }
+      
+      const updatedMethod = await ClientAuthModel.updateAuthMethod(parseInt(id), updateData);
+      
+      if (!updatedMethod) {
+        res.status(404).json({
+          success: false,
+          message: '认证方法不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        message: '认证方法更新成功',
+        data: updatedMethod
+      });
+    } catch (error) {
+      console.error('更新认证方法失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '更新认证方法失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 删除认证方法
+  static async deleteAuthMethod(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      
+      const success = await ClientAuthModel.deleteAuthMethod(parseInt(id));
+      
+      if (!success) {
+        res.status(404).json({
+          success: false,
+          message: '认证方法不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        message: '认证方法删除成功'
+      });
+    } catch (error) {
+      console.error('删除认证方法失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '删除认证方法失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 认证策略相关接口
+
+  // 获取所有认证策略
+  static async getAuthPolicies(req: Request, res: Response): Promise<void> {
+    try {
+      const policies = await ClientAuthModel.getAuthPolicies();
+      
+      res.status(200).json({
+        success: true,
+        data: policies
+      });
+    } catch (error) {
+      console.error('获取认证策略失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取认证策略失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据ID获取认证策略
+  static async getAuthPolicyById(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      const policy = await ClientAuthModel.getAuthPolicyById(parseInt(id));
+      
+      if (!policy) {
+        res.status(404).json({
+          success: false,
+          message: '认证策略不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: policy
+      });
+    } catch (error) {
+      console.error('获取认证策略失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取认证策略失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 创建认证策略
+  static async createAuthPolicy(req: Request, res: Response): Promise<void> {
+    try {
+      const { policy_name, priority, conditions, actions, is_active, description } = req.body;
+      
+      // 验证必填字段
+      if (!policy_name || priority === undefined || !conditions || !actions) {
+        res.status(400).json({
+          success: false,
+          message: '策略名称、优先级、条件和操作为必填项'
+        });
+        return;
+      }
+      
+      // 创建认证策略
+      const authPolicyData: Omit<AuthPolicy, 'id' | 'created_at' | 'updated_at'> = {
+        policy_name,
+        priority,
+        conditions: JSON.stringify(conditions),
+        actions: JSON.stringify(actions),
+        is_active: is_active !== undefined ? is_active : true,
+        description
+      };
+      
+      const newPolicy = await ClientAuthModel.createAuthPolicy(authPolicyData);
+      
+      res.status(201).json({
+        success: true,
+        message: '认证策略创建成功',
+        data: newPolicy
+      });
+    } catch (error) {
+      console.error('创建认证策略失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '创建认证策略失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 更新认证策略
+  static async updateAuthPolicy(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      const updateData = req.body;
+      
+      // 处理JSON字段
+      if (updateData.conditions) {
+        updateData.conditions = JSON.stringify(updateData.conditions);
+      }
+      
+      if (updateData.actions) {
+        updateData.actions = JSON.stringify(updateData.actions);
+      }
+      
+      const updatedPolicy = await ClientAuthModel.updateAuthPolicy(parseInt(id), updateData);
+      
+      if (!updatedPolicy) {
+        res.status(404).json({
+          success: false,
+          message: '认证策略不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        message: '认证策略更新成功',
+        data: updatedPolicy
+      });
+    } catch (error) {
+      console.error('更新认证策略失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '更新认证策略失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 删除认证策略
+  static async deleteAuthPolicy(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      
+      const success = await ClientAuthModel.deleteAuthPolicy(parseInt(id));
+      
+      if (!success) {
+        res.status(404).json({
+          success: false,
+          message: '认证策略不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        message: '认证策略删除成功'
+      });
+    } catch (error) {
+      console.error('删除认证策略失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '删除认证策略失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 客户端令牌相关接口
+
+  // 获取客户端令牌
+  static async getClientTokens(req: Request, res: Response): Promise<void> {
+    try {
+      const clientid = toString(req.params.clientid);
+      
+      if (!clientid) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID为必填项'
+        });
+        return;
+      }
+      
+      const tokens = await ClientAuthModel.getClientTokens(clientid);
+      
+      res.status(200).json({
+        success: true,
+        data: tokens
+      });
+    } catch (error) {
+      console.error('获取客户端令牌失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取客户端令牌失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 创建客户端令牌
+  static async createClientToken(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid, token_type, token_value, expires_at } = req.body;
+      
+      // 验证必填字段
+      if (!clientid || !token_type || !token_value || !expires_at) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID、令牌类型、令牌值和过期时间为必填项'
+        });
+        return;
+      }
+      
+      // 创建客户端令牌
+      const clientTokenData: Omit<ClientToken, 'id' | 'created_at' | 'updated_at'> = {
+        clientid,
+        token_type,
+        token_value,
+        expires_at: new Date(expires_at),
+        status: 'active'
+      };
+      
+      const newToken = await ClientAuthModel.createClientToken(clientTokenData);
+      
+      // 记录令牌创建日志
+      await ClientAuthModel.logAuthEvent(
+        clientid,
+        '',
+        'connect', // 修改为数据库允许的值
+        'success',
+        'Client token created',
+        req.ip,
+        undefined,
+        token_type,
+        undefined
+      );
+      
+      res.status(201).json({
+        success: true,
+        message: '客户端令牌创建成功',
+        data: newToken
+      });
+    } catch (error) {
+      console.error('创建客户端令牌失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '创建客户端令牌失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 更新客户端令牌
+  static async updateClientToken(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      const updateData = req.body;
+      
+      if (updateData.expires_at) {
+        updateData.expires_at = new Date(updateData.expires_at);
+      }
+      
+      const updatedToken = await ClientAuthModel.updateClientToken(parseInt(id), updateData);
+      
+      if (!updatedToken) {
+        res.status(404).json({
+          success: false,
+          message: '客户端令牌不存在'
+        });
+        return;
+      }
+      
+      // 记录令牌更新日志
+      await ClientAuthModel.logAuthEvent(
+        updatedToken.clientid,
+        '',
+        'connect', // 修改为数据库允许的值
+        'success',
+        'Client token updated',
+        req.ip,
+        undefined,
+        updatedToken.token_type,
+        undefined
+      );
+      
+      res.status(200).json({
+        success: true,
+        message: '客户端令牌更新成功',
+        data: updatedToken
+      });
+    } catch (error) {
+      console.error('更新客户端令牌失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '更新客户端令牌失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 删除客户端令牌
+  static async deleteClientToken(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      
+      const tokens = await ClientAuthModel.getClientTokens('');
+      const token = tokens.find(t => t.id === parseInt(id));
+      
+      if (!token) {
+        res.status(404).json({
+          success: false,
+          message: '客户端令牌不存在'
+        });
+        return;
+      }
+      
+      const success = await ClientAuthModel.deleteClientToken(parseInt(id));
+      
+      if (!success) {
+        res.status(404).json({
+          success: false,
+          message: '客户端令牌不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        message: '客户端令牌删除成功'
+      });
+    } catch (error) {
+      console.error('删除客户端令牌失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '删除客户端令牌失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 客户端证书相关接口
+
+  // 获取客户端证书
+  static async getClientCertificates(req: Request, res: Response): Promise<void> {
+    try {
+      const clientid = toString(req.params.clientid);
+      
+      if (!clientid) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID为必填项'
+        });
+        return;
+      }
+      
+      const certificates = await ClientAuthModel.getClientCertificates(clientid);
+      
+      res.status(200).json({
+        success: true,
+        data: certificates
+      });
+    } catch (error) {
+      console.error('获取客户端证书失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取客户端证书失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 创建客户端证书
+  static async createClientCertificate(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid, certificate_pem, fingerprint, expires_at } = req.body;
+      
+      // 验证必填字段
+      if (!clientid || !certificate_pem || !fingerprint || !expires_at) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID、证书PEM、指纹和过期时间为必填项'
+        });
+        return;
+      }
+      
+      // 创建客户端证书
+      const clientCertificateData: Omit<ClientCertificate, 'id' | 'created_at' | 'updated_at'> = {
+        clientid,
+        certificate_pem,
+        fingerprint,
+        expires_at: new Date(expires_at),
+        status: 'active'
+      };
+      
+      const newCertificate = await ClientAuthModel.createClientCertificate(clientCertificateData);
+      
+      // 记录证书创建日志
+      await ClientAuthModel.logAuthEvent(
+        clientid,
+        '',
+        'connect', // 修改为数据库允许的值
+        'success',
+        'Client certificate created',
+        req.ip,
+        undefined,
+        'certificate',
+        undefined
+      );
+      
+      res.status(201).json({
+        success: true,
+        message: '客户端证书创建成功',
+        data: newCertificate
+      });
+    } catch (error) {
+      console.error('创建客户端证书失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '创建客户端证书失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 更新客户端证书
+  static async updateClientCertificate(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      const updateData = req.body;
+      
+      if (updateData.expires_at) {
+        updateData.expires_at = new Date(updateData.expires_at);
+      }
+      
+      const updatedCertificate = await ClientAuthModel.updateClientCertificate(parseInt(id), updateData);
+      
+      if (!updatedCertificate) {
+        res.status(404).json({
+          success: false,
+          message: '客户端证书不存在'
+        });
+        return;
+      }
+      
+      // 记录证书更新日志
+      await ClientAuthModel.logAuthEvent(
+        updatedCertificate.clientid,
+        '',
+        'connect', // 修改为数据库允许的值
+        'success',
+        'Client certificate updated',
+        req.ip,
+        undefined,
+        'certificate',
+        undefined
+      );
+      
+      res.status(200).json({
+        success: true,
+        message: '客户端证书更新成功',
+        data: updatedCertificate
+      });
+    } catch (error) {
+      console.error('更新客户端证书失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '更新客户端证书失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 删除客户端证书
+  static async deleteClientCertificate(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      
+      const certificates = await ClientAuthModel.getClientCertificates('');
+      const certificate = certificates.find(c => c.id === parseInt(id));
+      
+      if (!certificate) {
+        res.status(404).json({
+          success: false,
+          message: '客户端证书不存在'
+        });
+        return;
+      }
+      
+      const success = await ClientAuthModel.deleteClientCertificate(parseInt(id));
+      
+      if (!success) {
+        res.status(404).json({
+          success: false,
+          message: '客户端证书不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        message: '客户端证书删除成功'
+      });
+    } catch (error) {
+      console.error('删除客户端证书失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '删除客户端证书失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+}

+ 201 - 0
mqtt-vue-dashboard/server/src/controllers/clientConnectionController.ts

@@ -0,0 +1,201 @@
+import { Request, Response } from 'express';
+import { ClientConnectionModel } from '../models/clientConnection';
+import { toString } from '../utils/helpers';
+
+export class ClientConnectionController {
+  // 获取所有连接记录
+  static async getAllConnections(req: Request, res: Response): Promise<void> {
+    try {
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const clientid = toString(req.query.clientid);
+      const event = toString(req.query.event);
+      const startDate = toString(req.query.startDate);
+      const endDate = toString(req.query.endDate);
+      
+      const connections = await ClientConnectionModel.getAllWithFilters(
+        limit, 
+        offset, 
+        { clientid, event, startDate, endDate }
+      );
+      const total = await ClientConnectionModel.getCountWithFilters(
+        { clientid, event, startDate, endDate }
+      );
+      
+      res.status(200).json({
+        success: true,
+        data: connections,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取连接记录失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取连接记录失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据客户端ID获取连接记录
+  static async getConnectionsByClientId(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      const limit = Number(req.query.limit) || 50;
+      
+      const clientidStr = toString(clientid);
+      
+      if (!clientidStr) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID不能为空'
+        });
+        return;
+      }
+      
+      const connections = await ClientConnectionModel.getByClientId(clientidStr, limit);
+      
+      res.status(200).json({
+        success: true,
+        data: connections
+      });
+    } catch (error) {
+      console.error('根据客户端ID获取连接记录失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据客户端ID获取连接记录失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据事件类型获取连接记录
+  static async getConnectionsByEvent(req: Request, res: Response): Promise<void> {
+    try {
+      const { event } = req.params;
+      const limit = Number(req.query.limit) || 50;
+      
+      const eventStr = toString(event);
+      
+      if (!eventStr || !['connected', 'disconnected'].includes(eventStr)) {
+        res.status(400).json({
+          success: false,
+          message: '无效的事件类型'
+        });
+        return;
+      }
+      
+      const connections = await ClientConnectionModel.getByEvent(eventStr, limit);
+      
+      res.status(200).json({
+        success: true,
+        data: connections
+      });
+    } catch (error) {
+      console.error('根据事件类型获取连接记录失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据事件类型获取连接记录失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取指定时间范围内的连接记录
+  static async getConnectionsByTimeRange(req: Request, res: Response): Promise<void> {
+    try {
+      const { startTime, endTime } = req.query;
+      
+      if (!startTime || !endTime) {
+        res.status(400).json({
+          success: false,
+          message: '开始时间和结束时间不能为空'
+        });
+        return;
+      }
+      
+      const start = new Date(startTime as string);
+      const end = new Date(endTime as string);
+      
+      if (isNaN(start.getTime()) || isNaN(end.getTime())) {
+        res.status(400).json({
+          success: false,
+          message: '无效的时间格式'
+        });
+        return;
+      }
+      
+      const connections = await ClientConnectionModel.getByTimeRange(start, end);
+      
+      res.status(200).json({
+        success: true,
+        data: connections
+      });
+    } catch (error) {
+      console.error('根据时间范围获取连接记录失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据时间范围获取连接记录失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取连接事件统计
+  static async getConnectionStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await ClientConnectionModel.getEventStats();
+      
+      res.status(200).json({
+        success: true,
+        data: stats
+      });
+    } catch (error) {
+      console.error('获取连接事件统计失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取连接事件统计失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 获取每日连接统计
+   */
+  static async getDailyConnectionStats(req: Request, res: Response): Promise<void> {
+    try {
+      const days = req.query.days ? parseInt(req.query.days as string) : 7;
+      
+      if (days < 1 || days > 30) {
+        res.status(400).json({
+          success: false,
+          message: '天数必须在1到30之间'
+        });
+        return;
+      }
+      
+      const stats = await ClientConnectionModel.getDailyStats(days);
+      
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取每日连接统计成功'
+      });
+    } catch (error) {
+      console.error('获取每日连接统计失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取每日连接统计失败',
+        error: error instanceof Error ? error.message : String(error)
+      });
+    }
+  }
+}

+ 213 - 0
mqtt-vue-dashboard/server/src/controllers/dashboardController.ts

@@ -0,0 +1,213 @@
+import { Request, Response } from 'express';
+import { DeviceModel } from '../models/device';
+import { ClientConnectionModel } from '../models/clientConnection';
+import { MqttMessageModel } from '../models/mqttMessage';
+
+export class DashboardController {
+  // 获取仪表板概览数据
+  static async getOverview(req: Request, res: Response): Promise<void> {
+    try {
+      // 并行获取各项统计数据
+      const [
+        deviceCount,
+        deviceStats,
+        connectionCount,
+        connectionStats,
+        connectionEventStats,
+        messageCount,
+        messageTypeStats,
+        recentConnections,
+        recentMessages
+      ] = await Promise.all([
+        DeviceModel.getCount(),
+        DeviceModel.getStatusStats(),
+        ClientConnectionModel.getCount(),
+        ClientConnectionModel.getConnectionStats(),
+        ClientConnectionModel.getEventStats(),
+        MqttMessageModel.getCount(),
+        MqttMessageModel.getTypeStats(),
+        ClientConnectionModel.getAll(10),
+        MqttMessageModel.getAll(10)
+      ]);
+
+      // 处理设备状态统计
+      const deviceStatusCounts = {
+        online: 0,
+        offline: 0,
+        unknown: 0
+      };
+      
+      deviceStats.forEach(stat => {
+        deviceStatusCounts[stat.status as keyof typeof deviceStatusCounts] = stat.count;
+      });
+
+      // 处理连接事件统计
+      const connectionEventCounts = {
+        connected: 0,
+        disconnected: 0
+      };
+      
+      connectionStats.forEach(stat => {
+        if (stat.connection_type in connectionEventCounts) {
+          connectionEventCounts[stat.connection_type as keyof typeof connectionEventCounts] = stat.count;
+        }
+      });
+
+      // 处理消息类型统计
+      const messageTypeCounts = {
+        publish: 0,
+        subscribe: 0,
+        unsubscribe: 0
+      };
+      
+      messageTypeStats.forEach(stat => {
+        messageTypeCounts[stat.message_type as keyof typeof messageTypeCounts] = stat.count;
+      });
+
+      // 获取每日连接统计(最近7天)
+      const dailyStats = await ClientConnectionModel.getDailyStats(7);
+      
+      // 获取每小时消息统计(最近24小时)
+      const hourlyStats = await MqttMessageModel.getHourlyStats(24);
+
+      res.status(200).json({
+        success: true,
+        data: {
+          summary: {
+            totalDevices: deviceCount,
+            onlineDevices: deviceStatusCounts.online,
+            totalConnections: connectionCount,
+            totalMessages: messageCount
+          },
+          deviceStats: deviceStatusCounts,
+          connectionStats: connectionEventCounts,
+          messageStats: messageTypeCounts,
+          dailyStats: dailyStats || [],
+          hourlyStats: hourlyStats || [],
+          recentConnections: recentConnections || [],
+          recentMessages: recentMessages || []
+        }
+      });
+    } catch (error) {
+      console.error('获取仪表板概览数据失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取仪表板概览数据失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取图表数据
+  static async getChartData(req: Request, res: Response): Promise<void> {
+    try {
+      const { type, period } = req.query;
+      
+      let data;
+      
+      switch (type) {
+        case 'connections':
+          const days = Number(period) || 7;
+          data = await ClientConnectionModel.getDailyStats(days);
+          break;
+          
+        case 'messages':
+          const hours = Number(period) || 24;
+          data = await MqttMessageModel.getHourlyStats(hours);
+          break;
+          
+        case 'deviceStatus':
+          data = await DeviceModel.getStatusStats();
+          break;
+          
+        case 'messageTypes':
+          data = await MqttMessageModel.getTypeStats();
+          break;
+          
+        case 'qosLevels':
+          data = await MqttMessageModel.getQosStats();
+          break;
+          
+        case 'popularTopics':
+          const limit = Number(period) || 10;
+          data = await MqttMessageModel.getPopularTopics(limit);
+          break;
+          
+        case 'activeClients':
+          const clientLimit = Number(period) || 10;
+          data = await MqttMessageModel.getActiveClients(clientLimit);
+          break;
+          
+        default:
+          res.status(400).json({
+            success: false,
+            message: '无效的图表类型'
+          });
+          return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data
+      });
+    } catch (error) {
+      console.error('获取图表数据失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取图表数据失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 获取设备统计信息
+   */
+  static async getDeviceStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await DeviceModel.getDeviceStats();
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取设备统计成功'
+      });
+    } catch (error) {
+      res.status(500).json({
+        success: false,
+        message: '获取设备统计失败',
+        error: error instanceof Error ? error.message : String(error)
+      });
+    }
+  }
+
+  static async getSystemInfo(req: Request, res: Response): Promise<void> {
+    try {
+      const uptimeSeconds = process.uptime();
+      const days = Math.floor(uptimeSeconds / 86400);
+      const hours = Math.floor((uptimeSeconds % 86400) / 3600);
+      const minutes = Math.floor((uptimeSeconds % 3600) / 60);
+
+      let uptimeStr = '';
+      if (days > 0) uptimeStr += `${days}天 `;
+      if (hours > 0 || days > 0) uptimeStr += `${hours}小时 `;
+      uptimeStr += `${minutes}分钟`;
+
+      res.status(200).json({
+        success: true,
+        data: {
+          nodeVersion: process.version.replace('v', ''),
+          uptime: uptimeStr,
+          platform: process.platform,
+          arch: process.arch,
+          memoryUsage: process.memoryUsage()
+        }
+      });
+    } catch (error) {
+      res.status(500).json({
+        success: false,
+        message: '获取系统信息失败',
+        error: error instanceof Error ? error.message : String(error)
+      });
+    }
+  }
+}

+ 270 - 0
mqtt-vue-dashboard/server/src/controllers/deviceBindingController.ts

@@ -0,0 +1,270 @@
+import { Request, Response } from 'express';
+import { DeviceBindingModel, DeviceBinding } from '../models/deviceBinding';
+import { DeviceModel } from '../models/device';
+import { toString } from '../utils/helpers';
+
+export class DeviceBindingController {
+  // 获取所有设备绑定关系
+  static async getAllBindings(req: Request, res: Response): Promise<void> {
+    try {
+      const bindings = await DeviceBindingModel.getAll();
+      res.json({
+        success: true,
+        data: bindings,
+        message: '获取设备绑定关系成功'
+      });
+    } catch (error: any) {
+      console.error('获取设备绑定关系失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备绑定关系失败',
+        error: error.message
+      });
+    }
+  }
+
+  // 根据房间ID获取绑定的设备
+  static async getDevicesByRoom(req: Request, res: Response): Promise<void> {
+    try {
+      const { roomId } = req.params;
+      
+      const roomIdStr = toString(roomId);
+      
+      if (!roomIdStr || isNaN(parseInt(roomIdStr))) {
+        res.status(400).json({
+          success: false,
+          message: '房间ID无效'
+        });
+        return;
+      }
+
+      const devices = await DeviceBindingModel.getByRoomId(parseInt(roomIdStr));
+      res.json({
+        success: true,
+        data: devices,
+        message: '获取房间设备成功'
+      });
+    } catch (error: any) {
+      console.error('获取房间设备失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取房间设备失败',
+        error: error.message
+      });
+    }
+  }
+
+  // 获取可绑定的设备(未绑定到任何房间的设备)
+  static async getAvailableDevices(req: Request, res: Response): Promise<void> {
+    try {
+      const devices = await DeviceBindingModel.getAvailableDevices();
+      res.json({
+        success: true,
+        data: devices,
+        message: '获取可绑定设备成功'
+      });
+    } catch (error: any) {
+      console.error('获取可绑定设备失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取可绑定设备失败',
+        error: error.message
+      });
+    }
+  }
+
+  // 获取所有设备及其绑定状态
+  static async getAllDevicesWithBindingStatus(req: Request, res: Response): Promise<void> {
+    try {
+      // 解析请求参数
+      const page = parseInt(toString(req.query.page)) || 1;
+      const pageSize = parseInt(toString(req.query.pageSize)) || 10;
+      const status = toString(req.query.status);
+      const room_id = req.query.room_id ? parseInt(toString(req.query.room_id)) : undefined;
+      const search = toString(req.query.search);
+
+      // 构建筛选条件
+      const filters = {
+        ...(status && { status }),
+        ...(room_id && { room_id }),
+        ...(search && { search })
+      };
+
+      // 调用模型层获取数据
+      const { devices, total } = await DeviceBindingModel.getAllDevicesWithBindingStatus(page, pageSize, filters);
+
+      res.json({
+        success: true,
+        data: {
+          devices,
+          pagination: {
+            current: page,
+            pageSize,
+            total
+          }
+        },
+        message: '获取设备绑定状态成功'
+      });
+    } catch (error: any) {
+      console.error('获取设备绑定状态失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备绑定状态失败',
+        error: error.message
+      });
+    }
+  }
+
+  // 绑定设备到房间
+  static async bindDevice(req: Request, res: Response): Promise<void> {
+    try {
+      const { device_clientid, room_id, device_name, device_type, properties } = req.body;
+
+      // 验证必填字段
+      if (!device_clientid || !room_id) {
+        res.status(400).json({
+          success: false,
+          message: '设备客户端ID和房间ID为必填项'
+        });
+        return;
+      }
+
+      // 验证设备是否存在
+      const device = await DeviceModel.getByClientId(device_clientid);
+      if (!device) {
+        res.status(404).json({
+          success: false,
+          message: '设备不存在'
+        });
+        return;
+      }
+
+      // 创建绑定关系
+      const binding = await DeviceBindingModel.bindDevice(
+        device_clientid,
+        room_id
+      );
+
+      res.status(201).json({
+        success: true,
+        data: binding,
+        message: '设备绑定成功'
+      });
+    } catch (error: any) {
+      console.error('设备绑定失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '设备绑定失败',
+        error: error.message
+      });
+    }
+  }
+
+  // 解绑设备
+  static async unbindDevice(req: Request, res: Response): Promise<void> {
+    try {
+      const { deviceClientId } = req.params;
+      
+      const deviceClientIdStr = toString(deviceClientId);
+
+      if (!deviceClientIdStr) {
+        res.status(400).json({
+          success: false,
+          message: '设备客户端ID为必填项'
+        });
+        return;
+      }
+
+      const success = await DeviceBindingModel.unbindDevice(deviceClientIdStr);
+
+      if (!success) {
+        res.status(404).json({
+          success: false,
+          message: '设备绑定关系不存在'
+        });
+        return;
+      }
+
+      res.json({
+        success: true,
+        message: '设备解绑成功'
+      });
+    } catch (error: any) {
+      console.error('设备解绑失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '设备解绑失败',
+        error: error.message
+      });
+    }
+  }
+
+  // 更新设备绑定关系
+  static async updateBinding(req: Request, res: Response): Promise<void> {
+    try {
+      const { id } = req.params;
+      const { room_id, device_name, device_type, properties } = req.body;
+
+      const idStr = toString(id);
+      
+      if (!idStr || isNaN(parseInt(idStr))) {
+        res.status(400).json({
+          success: false,
+          message: '绑定ID无效'
+        });
+        return;
+      }
+
+      const binding = await DeviceBindingModel.updateBinding(parseInt(idStr), {
+        room_id,
+        device_name,
+        device_type,
+        properties
+      });
+
+      res.json({
+        success: true,
+        data: binding,
+        message: '设备绑定更新成功'
+      });
+    } catch (error: any) {
+      console.error('更新设备绑定失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '更新设备绑定失败',
+        error: error.message
+      });
+    }
+  }
+
+  // 获取房间设备详情(合并devices表和绑定关系)
+  static async getRoomDevicesWithDetails(req: Request, res: Response): Promise<void> {
+    try {
+      const { roomId } = req.params;
+      
+      const roomIdStr = toString(roomId);
+      
+      if (!roomIdStr || isNaN(parseInt(roomIdStr))) {
+        res.status(400).json({
+          success: false,
+          message: '房间ID无效'
+        });
+        return;
+      }
+
+      const devices = await DeviceBindingModel.getRoomDevicesWithDetails(parseInt(roomIdStr));
+      res.json({
+        success: true,
+        data: devices,
+        message: '获取房间设备详情成功'
+      });
+    } catch (error: any) {
+      console.error('获取房间设备详情失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取房间设备详情失败',
+        error: error.message
+      });
+    }
+  }
+}

+ 832 - 0
mqtt-vue-dashboard/server/src/controllers/deviceController.ts

@@ -0,0 +1,832 @@
+import { Request, Response } from 'express';
+import { DeviceModel } from '../models/device';
+import { DeviceBindingModel } from '../models/deviceBinding';
+import { SensorDataModel } from '../models/sensorData';
+import { WiFiConfigModel } from '../models/wifiConfig';
+import { LoggerService } from '../services/loggerService';
+import { MqttBrokerService } from '../services/mqttBrokerService';
+import { toString } from '../utils/helpers';
+
+export class DeviceController {
+  // 获取所有设备
+  static async getAllDevices(req: Request, res: Response): Promise<void> {
+    try {
+      const page = Number(toString(req.query.page)) || 1;
+      const limit = Number(toString(req.query.limit)) || 20;
+      const offset = (page - 1) * limit;
+      
+      const status = toString(req.query.status);
+      const search = toString(req.query.search);
+      
+      let devices;
+      let total;
+      
+      if (status) {
+        // 根据状态筛选
+        devices = await DeviceModel.getByStatus(status);
+        total = await DeviceModel.getCountByStatus(status);
+      } else if (search) {
+        // 根据搜索关键词筛选
+        devices = await DeviceModel.getBySearch(search);
+        total = await DeviceModel.getCountBySearch(search);
+      } else {
+        // 获取所有设备
+        devices = await DeviceModel.getAll(limit, offset);
+        total = await DeviceModel.getCount();
+      }
+      
+      // 如果使用了筛选条件,需要手动应用分页
+      if (status || search) {
+        const startIndex = offset;
+        const endIndex = offset + limit;
+        devices = devices.slice(startIndex, endIndex);
+      }
+      
+      // 记录获取设备列表成功日志
+      LoggerService.info('获取设备列表成功', {
+        source: 'device',
+        module: 'get_all_devices',
+        details: JSON.stringify({
+          page,
+          limit,
+          total,
+          status: status || 'all',
+          search: search || 'none',
+          returnedCount: devices.length
+        })
+      }).catch(err => {
+        console.error('获取设备列表日志写入失败:', err);
+      });
+      
+      res.status(200).json({
+        success: true,
+        data: devices,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取设备列表失败:', error);
+      
+      // 记录获取设备列表失败日志
+      LoggerService.error('获取设备列表失败', {
+        source: 'device',
+        module: 'get_all_devices',
+        details: JSON.stringify({
+          error: error instanceof Error ? error.message : '未知错误',
+          page: toString(req.query.page),
+          limit: toString(req.query.limit),
+          status: toString(req.query.status),
+          search: toString(req.query.search)
+        })
+      }).catch(err => {
+        console.error('获取设备列表失败日志写入失败:', err);
+      });
+      
+      res.status(500).json({
+        success: false,
+        message: '获取设备列表失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据状态获取设备
+  static async getDevicesByStatus(req: Request, res: Response): Promise<void> {
+    try {
+      const { status } = req.params;
+      
+      const statusStr = toString(status);
+      
+      if (!statusStr || !['online', 'offline', 'unknown'].includes(statusStr)) {
+        res.status(400).json({
+          success: false,
+          message: '无效的设备状态'
+        });
+        return;
+      }
+      
+      const devices = await DeviceModel.getByStatus(statusStr);
+      
+      res.status(200).json({
+        success: true,
+        data: devices
+      });
+    } catch (error) {
+      console.error('根据状态获取设备失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据状态获取设备失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据客户端ID获取设备
+  static async getDeviceByClientId(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      
+      const clientidStr = toString(clientid);
+      
+      if (!clientidStr) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID不能为空'
+        });
+        return;
+      }
+      
+      const device = await DeviceModel.getByClientId(clientidStr);
+      
+      if (!device) {
+        res.status(404).json({
+          success: false,
+          message: '设备不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: device
+      });
+    } catch (error) {
+      console.error('根据客户端ID获取设备失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据客户端ID获取设备失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 获取设备状态统计
+   */
+  static async getDeviceStatusStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await DeviceModel.getStatusStats();
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取设备状态统计成功'
+      });
+    } catch (error) {
+      res.status(500).json({
+        success: false,
+        message: '获取设备状态统计失败',
+        error: error instanceof Error ? error.message : String(error)
+      });
+    }
+  }
+
+  /**
+   * 获取设备统计信息
+   */
+  static async getDeviceStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await DeviceModel.getDeviceStats();
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取设备统计成功'
+      });
+    } catch (error) {
+      res.status(500).json({
+        success: false,
+        message: '获取设备统计失败',
+        error: error instanceof Error ? error.message : String(error)
+      });
+    }
+  }
+
+  /**
+   * 根据客户端ID删除设备
+   */
+  static async deleteDeviceByClientId(req: Request, res: Response): Promise<void> {
+    const clientidStr = toString(req.params.clientid);
+    
+    try {
+      if (!clientidStr) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID不能为空'
+        });
+        return;
+      }
+      
+      const device = await DeviceModel.getByClientId(clientidStr);
+      if (!device) {
+        res.status(404).json({
+          success: false,
+          message: '设备不存在'
+        });
+        return;
+      }
+      
+      const success = await DeviceModel.deleteByClientId(clientidStr);
+      
+      if (success) {
+        LoggerService.info('删除设备成功', {
+          source: 'device',
+          module: 'delete_device',
+          details: JSON.stringify({
+            clientid: clientidStr,
+            deviceName: device.device_name,
+            status: device.status
+          })
+        }).catch(err => {
+          console.error('删除设备成功日志写入失败:', err);
+        });
+        
+        res.status(200).json({
+          success: true,
+          message: '设备删除成功'
+        });
+      } else {
+        LoggerService.error('删除设备失败', {
+          source: 'device',
+          module: 'delete_device',
+          details: JSON.stringify({
+            clientid: clientidStr,
+            reason: '数据库操作失败'
+          })
+        }).catch(err => {
+          console.error('删除设备失败日志写入失败:', err);
+        });
+        
+        res.status(500).json({
+          success: false,
+          message: '设备删除失败'
+        });
+      }
+    } catch (error) {
+      console.error('删除设备失败:', error);
+      
+      // 记录删除设备异常日志
+      LoggerService.error('删除设备异常', {
+        source: 'device',
+        module: 'delete_device',
+        details: JSON.stringify({
+          clientid: clientidStr,
+          error: error instanceof Error ? error.message : '未知错误'
+        })
+      }).catch(err => {
+        console.error('删除设备异常日志写入失败:', err);
+      });
+      
+      res.status(500).json({
+        success: false,
+        message: '删除设备失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 根据客户端ID更新设备信息
+   */
+  static async updateDeviceByClientId(req: Request, res: Response): Promise<void> {
+    const clientidStr = toString(req.params.clientid);
+    
+    try {
+      const updateData = req.body || {};
+      
+      if (!clientidStr) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID不能为空'
+        });
+        return;
+      }
+      
+      const device = await DeviceModel.getByClientId(clientidStr);
+      if (!device) {
+        res.status(404).json({
+          success: false,
+          message: '设备不存在'
+        });
+        return;
+      }
+      
+      // 如果请求中包含room_id,则处理设备绑定/解绑
+      if (updateData && 'room_id' in updateData) {
+        const { room_id } = updateData;
+        
+        if (room_id === null || room_id === undefined || room_id === '') {
+          await DeviceBindingModel.unbindDevice(clientidStr);
+          
+          LoggerService.info('设备解绑成功', {
+            source: 'device',
+            module: 'update_device',
+            details: JSON.stringify({
+              clientid: clientidStr,
+              deviceName: device.device_name,
+              action: 'unbind'
+            })
+          }).catch(err => {
+            console.error('设备解绑日志写入失败:', err);
+          });
+        } else {
+          await DeviceBindingModel.bindDevice(clientidStr, room_id);
+          
+          LoggerService.info('设备绑定成功', {
+            source: 'device',
+            module: 'update_device',
+            details: JSON.stringify({
+              clientid: clientidStr,
+              deviceName: device.device_name,
+              roomId: room_id,
+              action: 'bind'
+            })
+          }).catch(err => {
+            console.error('设备绑定日志写入失败:', err);
+          });
+        }
+        
+        delete updateData.room_id;
+      }
+      
+      if (updateData && Object.keys(updateData).length > 0) {
+        console.log('更新设备数据:', updateData);
+        const success = await DeviceModel.update(clientidStr, updateData);
+        console.log('更新结果:', success);
+        
+        if (!success) {
+          LoggerService.error('设备信息更新失败', {
+            source: 'device',
+            module: 'update_device',
+            details: JSON.stringify({
+              clientid: clientidStr,
+              updateData,
+              reason: '数据库操作失败'
+            })
+          }).catch(err => {
+            console.error('设备更新失败日志写入失败:', err);
+          });
+          
+          res.status(500).json({
+            success: false,
+            message: '设备更新失败'
+          });
+          return;
+        }
+        
+        LoggerService.info('设备信息更新成功', {
+          source: 'device',
+          module: 'update_device',
+          details: JSON.stringify({
+            clientid: clientidStr,
+            deviceName: device.device_name,
+            updateData
+          })
+        }).catch(err => {
+          console.error('设备更新成功日志写入失败:', err);
+        });
+      }
+      
+      res.status(200).json({
+        success: true,
+        message: '设备更新成功'
+      });
+    } catch (error) {
+      console.error('更新设备失败:', error);
+      
+      // 记录设备更新异常日志
+      LoggerService.error('设备更新异常', {
+        source: 'device',
+        module: 'update_device',
+        details: JSON.stringify({
+          clientid: clientidStr,
+          updateData: req.body,
+          error: error instanceof Error ? error.message : '未知错误'
+        })
+      }).catch(err => {
+        console.error('设备更新异常日志写入失败:', err);
+      });
+      
+      res.status(500).json({
+        success: false,
+        message: '更新设备失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 获取房间中的设备列表
+   */
+  static async getDevicesByRoomId(req: Request, res: Response): Promise<void> {
+    try {
+      const { room_id } = req.params;
+      
+      const roomIdStr = toString(room_id);
+      
+      if (!roomIdStr) {
+        res.status(400).json({
+          success: false,
+          message: '房间ID不能为空'
+        });
+        return;
+      }
+      
+      const devices = await DeviceBindingModel.getDevicesByRoomId(parseInt(roomIdStr));
+      
+      res.status(200).json({
+        success: true,
+        data: devices
+      });
+    } catch (error) {
+      console.error('获取房间设备失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取房间设备失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 获取未绑定的设备列表
+   */
+  static async getUnboundDevices(req: Request, res: Response): Promise<void> {
+    console.log('getUnboundDevices 方法被调用');
+    try {
+      console.log('正在调用 DeviceBindingModel.getUnboundDevices()');
+      const devices = await DeviceBindingModel.getUnboundDevices();
+      console.log('获取到的未绑定设备数量:', devices.length);
+      
+      res.status(200).json({
+        success: true,
+        data: devices
+      });
+    } catch (error) {
+      console.error('获取未绑定设备失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取未绑定设备失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 控制设备继电器开关
+   */
+  static async controlRelay(req: Request, res: Response): Promise<void> {
+    try {
+      const { deviceId, status } = req.body;
+      
+      console.log('接收到控制继电器请求:', { deviceId, status });
+      
+      if (!deviceId || !status) {
+        console.log('缺少设备ID或状态参数');
+        res.status(400).json({
+          success: false,
+          message: '设备ID和状态不能为空'
+        });
+        return;
+      }
+      
+      if (!['ON', 'OFF'].includes(status)) {
+        console.log('状态参数不正确:', status);
+        res.status(400).json({
+          success: false,
+          message: '状态必须是ON或OFF'
+        });
+        return;
+      }
+      
+      // 检查设备是否存在
+      const device = await DeviceModel.getByClientId(deviceId);
+      if (!device) {
+        console.log('设备不存在:', deviceId);
+        res.status(404).json({
+          success: false,
+          message: '设备不存在'
+        });
+        return;
+      }
+      
+      console.log('设备存在,准备连接MQTT:', deviceId);
+      
+      // 连接到MQTT服务器
+      const mqttBroker = MqttBrokerService.getInstance();
+      
+      const topic = `device/${deviceId}/relay/control`;
+      await mqttBroker.publish(topic, status);
+      
+      console.log('MQTT消息发布成功:', { deviceId, status });
+      
+      // 更新设备继电器状态到传感器数据表
+      await SensorDataModel.upsertByDeviceAndType(deviceId, 'relay', status, `device/${deviceId}/relay/control`);
+      
+      console.log('设备状态更新成功:', { deviceId, status });
+      
+      // 记录控制继电器成功日志
+      LoggerService.info('控制设备继电器成功', {
+        source: 'device',
+        module: 'control_relay',
+        details: JSON.stringify({
+          deviceId,
+          status,
+          topic
+        })
+      }).catch(err => {
+        console.error('控制继电器成功日志写入失败:', err);
+      });
+      
+      res.status(200).json({
+        success: true,
+        message: `设备 ${deviceId} 的继电器已${status === 'ON' ? '开启' : '关闭'}`,
+        data: {
+          deviceId,
+          status,
+          topic
+        }
+      });
+    } catch (error) {
+      console.error('控制设备继电器失败:', error);
+      
+      // 记录控制继电器失败日志
+      LoggerService.error('控制设备继电器失败', {
+        source: 'device',
+        module: 'control_relay',
+        details: JSON.stringify({
+          error: error instanceof Error ? error.message : '未知错误'
+        })
+      }).catch(err => {
+        console.error('控制继电器失败日志写入失败:', err);
+      });
+      
+      res.status(500).json({
+        success: false,
+        message: '控制设备继电器失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 配置设备WiFi
+   */
+  static async configureWiFi(req: Request, res: Response): Promise<void> {
+    try {
+      const { deviceId, ssid, password } = req.body;
+      
+      console.log('接收到WiFi配置请求:', { deviceId, ssid });
+      
+      if (!deviceId || !ssid || !password) {
+        console.log('缺少必要参数');
+        res.status(400).json({
+          success: false,
+          message: '设备ID、SSID和密码不能为空'
+        });
+        return;
+      }
+      
+      // 检查设备是否存在
+      const device = await DeviceModel.getByClientId(deviceId);
+      if (!device) {
+        console.log('设备不存在:', deviceId);
+        res.status(404).json({
+          success: false,
+          message: '设备不存在'
+        });
+        return;
+      }
+      
+      console.log('设备存在,准备连接MQTT:', deviceId);
+      
+      // 连接到MQTT服务器
+      const mqttBroker = MqttBrokerService.getInstance();
+      
+      const topic = `device/ESP32-${deviceId}/wifi/config`;
+      const message = `${ssid},${password}`;
+      await mqttBroker.publish(topic, message);
+      
+      console.log('WiFi配置消息发布成功:', { deviceId, ssid });
+      
+      // 保存WiFi配置到数据库
+      const wifiConfig = await WiFiConfigModel.create({
+        device_clientid: deviceId,
+        ssid,
+        password,
+        status: 'sent'
+      });
+      
+      console.log('WiFi配置已保存到数据库:', wifiConfig.id);
+      
+      // 记录WiFi配置成功日志
+      LoggerService.info('配置设备WiFi成功', {
+        source: 'device',
+        module: 'configure_wifi',
+        details: JSON.stringify({
+          deviceId,
+          ssid,
+          topic,
+          configId: wifiConfig.id
+        })
+      }).catch(err => {
+        console.error('配置WiFi成功日志写入失败:', err);
+      });
+      
+      res.status(200).json({
+        success: true,
+        message: `设备 ${deviceId} 的WiFi配置已发送`,
+        data: {
+          deviceId,
+          ssid,
+          topic,
+          configId: wifiConfig.id
+        }
+      });
+    } catch (error) {
+      console.error('配置设备WiFi失败:', error);
+      
+      // 记录配置WiFi失败日志
+      LoggerService.error('配置设备WiFi失败', {
+        source: 'device',
+        module: 'configure_wifi',
+        details: JSON.stringify({
+          error: error instanceof Error ? error.message : '未知错误'
+        })
+      }).catch(err => {
+        console.error('配置WiFi失败日志写入失败:', err);
+      });
+      
+      res.status(500).json({
+        success: false,
+        message: '配置设备WiFi失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 重启设备
+   */
+  static async restartDevice(req: Request, res: Response): Promise<void> {
+    try {
+      const { deviceId } = req.body;
+      
+      console.log('接收到设备重启请求:', { deviceId });
+      
+      if (!deviceId) {
+        console.log('缺少设备ID参数');
+        res.status(400).json({
+          success: false,
+          message: '设备ID不能为空'
+        });
+        return;
+      }
+      
+      // 检查设备是否存在
+      const device = await DeviceModel.getByClientId(deviceId);
+      if (!device) {
+        console.log('设备不存在:', deviceId);
+        res.status(404).json({
+          success: false,
+          message: '设备不存在'
+        });
+        return;
+      }
+      
+      console.log('设备存在,准备连接MQTT:', deviceId);
+      
+      // 连接到MQTT服务器
+      const mqttBroker = MqttBrokerService.getInstance();
+      
+      const topic = `device/ESP32-${deviceId}/system/status`;
+      const message = 'restarting';
+      await mqttBroker.publish(topic, message);
+      
+      console.log('重启通知消息发布成功:', { deviceId });
+      
+      // 记录重启设备成功日志
+      LoggerService.info('重启设备成功', {
+        source: 'device',
+        module: 'restart_device',
+        details: JSON.stringify({
+          deviceId,
+          topic
+        })
+      }).catch(err => {
+        console.error('重启设备成功日志写入失败:', err);
+      });
+      
+      res.status(200).json({
+        success: true,
+        message: `设备 ${deviceId} 的重启指令已发送,设备将在3秒后重启`,
+        data: {
+          deviceId,
+          topic
+        }
+      });
+    } catch (error) {
+      console.error('重启设备失败:', error);
+      
+      // 记录重启设备失败日志
+      LoggerService.error('重启设备失败', {
+        source: 'device',
+        module: 'restart_device',
+        details: JSON.stringify({
+          error: error instanceof Error ? error.message : '未知错误'
+        })
+      }).catch(err => {
+        console.error('重启设备失败日志写入失败:', err);
+      });
+      
+      res.status(500).json({
+        success: false,
+        message: '重启设备失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 查询设备WiFi信号强度
+   * 向设备发送查询指令,设备收到后会主动上报RSSI
+   */
+  static async queryRssi(req: Request, res: Response): Promise<void> {
+    try {
+      const deviceId = req.params.deviceId as string;
+      
+      if (!deviceId) {
+        res.status(400).json({
+          success: false,
+          message: '设备ID不能为空'
+        });
+        return;
+      }
+      
+      const device = await DeviceModel.getByClientId(deviceId);
+      if (!device) {
+        res.status(404).json({
+          success: false,
+          message: '设备不存在'
+        });
+        return;
+      }
+
+      if (device.status !== 'online') {
+        res.status(400).json({
+          success: false,
+          message: '设备不在线,无法查询WiFi信号强度'
+        });
+        return;
+      }
+      
+      const mqttBroker = MqttBrokerService.getInstance();
+      
+      const topic = `device/ESP32-${deviceId}/wifi/rssi/query`;
+      await mqttBroker.publish(topic, 'query');
+      
+      console.log('RSSI查询指令发送成功:', { deviceId, topic });
+      
+      LoggerService.info('查询设备RSSI', {
+        source: 'device',
+        module: 'query_rssi',
+        details: JSON.stringify({
+          deviceId,
+          topic
+        })
+      }).catch(err => {
+        console.error('RSSI查询日志写入失败:', err);
+      });
+      
+      res.status(200).json({
+        success: true,
+        message: `已向设备 ${deviceId} 发送RSSI查询指令`,
+        data: {
+          deviceId,
+          topic,
+          currentRssi: device.rssi
+        }
+      });
+    } catch (error) {
+      console.error('查询RSSI失败:', error);
+      
+      LoggerService.error('查询RSSI失败', {
+        source: 'device',
+        module: 'query_rssi',
+        details: JSON.stringify({
+          error: error instanceof Error ? error.message : '未知错误'
+        })
+      }).catch(err => {
+        console.error('RSSI查询失败日志写入失败:', err);
+      });
+      
+      res.status(500).json({
+        success: false,
+        message: '查询RSSI失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+}

+ 338 - 0
mqtt-vue-dashboard/server/src/controllers/deviceLogController.ts

@@ -0,0 +1,338 @@
+import { Request, Response } from 'express';
+import { DeviceLogModel } from '../models/deviceLog';
+import { DeviceModel } from '../models/device';
+import { AuthLogModel } from '../models/authLog';
+import { SystemLogModel } from '../models/systemLog';
+import { LoggerService } from '../services/loggerService';
+import { toString } from '../utils/helpers';
+
+export class DeviceLogController {
+  static async getDeviceLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const clientid = toString(req.params.clientid);
+      const page = Number(toString(req.query.page)) || 1;
+      const limit = Number(toString(req.query.limit)) || 50;
+      const offset = (page - 1) * limit;
+
+      const event_type = toString(req.query.event_type);
+      const start_time = toString(req.query.start_time);
+      const end_time = toString(req.query.end_time);
+
+      const filters: any = {};
+
+      if (event_type) {
+        filters.event_type = event_type;
+      }
+
+      if (start_time) {
+        filters.start_time = new Date(start_time);
+      }
+
+      if (end_time) {
+        filters.end_time = new Date(end_time);
+      }
+
+      const { logs, total } = await DeviceLogModel.getByClientId(
+        clientid,
+        filters,
+        limit,
+        offset
+      );
+
+      res.status(200).json({
+        success: true,
+        data: logs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取设备日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDeviceConnectLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      const limit = Number(toString(req.query.limit)) || 100;
+
+      const clientidStr = toString(clientid);
+      const logs = await DeviceLogModel.getConnectDisconnectLogs(clientidStr, limit);
+
+      res.status(200).json({
+        success: true,
+        data: logs
+      });
+    } catch (error) {
+      console.error('获取设备连接日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备连接日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDevicePublishLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      const limit = Number(toString(req.query.limit)) || 100;
+
+      const clientidStr = toString(clientid);
+      const logs = await DeviceLogModel.getPublishLogs(clientidStr, limit);
+
+      res.status(200).json({
+        success: true,
+        data: logs
+      });
+    } catch (error) {
+      console.error('获取设备发布日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备发布日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDeviceSubscribeLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      const limit = Number(toString(req.query.limit)) || 100;
+
+      const clientidStr = toString(clientid);
+      const logs = await DeviceLogModel.getSubscribeLogs(clientidStr, limit);
+
+      res.status(200).json({
+        success: true,
+        data: logs
+      });
+    } catch (error) {
+      console.error('获取设备订阅日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备订阅日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDeviceLogStats(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+
+      const clientidStr = toString(clientid);
+      const eventStats = await DeviceLogModel.getEventTypesStats(clientidStr);
+
+      res.status(200).json({
+        success: true,
+        data: eventStats
+      });
+    } catch (error) {
+      console.error('获取设备日志统计失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备日志统计失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDeviceDailyStats(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      const days = Number(toString(req.query.days)) || 7;
+
+      const clientidStr = toString(clientid);
+      const stats = await DeviceLogModel.getDailyStats(clientidStr, days);
+
+      res.status(200).json({
+        success: true,
+        data: stats
+      });
+    } catch (error) {
+      console.error('获取设备每日统计失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备每日统计失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDeviceAuthLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const clientid = toString(req.params.clientid);
+      const page = Number(toString(req.query.page)) || 1;
+      const limit = Number(toString(req.query.limit)) || 50;
+      const offset = (page - 1) * limit;
+
+      const operation_type = toString(req.query.operation_type);
+      const result = toString(req.query.result);
+
+      const filters: any = { clientid };
+
+      if (operation_type) {
+        filters.operation_type = operation_type;
+      }
+
+      if (result) {
+        filters.result = result;
+      }
+
+      const logs = await AuthLogModel.getByMultipleConditions(
+        filters,
+        undefined,
+        undefined,
+        limit,
+        offset
+      );
+
+      const total = await AuthLogModel.getCountByClientid(clientid);
+
+      res.status(200).json({
+        success: true,
+        data: logs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取设备认证日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备认证日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDeviceSystemLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+      const page = Number(toString(req.query.page)) || 1;
+      const limit = Number(toString(req.query.limit)) || 50;
+      const offset = (page - 1) * limit;
+
+      const level = toString(req.query.level);
+      const source = toString(req.query.source);
+
+      const filters: any = {};
+
+      if (level) {
+        filters.level = level;
+      }
+
+      if (source) {
+        filters.source = source;
+      }
+
+      const logs = await SystemLogModel.getByMultipleConditions(
+        filters,
+        undefined,
+        undefined,
+        limit,
+        offset,
+        ['message', 'source', 'module', 'details']
+      );
+
+      const total = await SystemLogModel.getCount();
+
+      res.status(200).json({
+        success: true,
+        data: logs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDeviceOverview(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+
+      const clientidStr = toString(clientid);
+      const [eventStats, authLogs, recentLogs] = await Promise.all([
+        DeviceLogModel.getEventTypesStats(clientidStr),
+        AuthLogModel.getByClientid(clientidStr, 10),
+        DeviceLogModel.getByClientId(clientidStr, undefined, 10, 0)
+      ]);
+
+      const authSuccess = authLogs.filter((log: any) => log.result === 'success').length;
+      const authFailure = authLogs.filter((log: any) => log.result === 'failure').length;
+
+      res.status(200).json({
+        success: true,
+        data: {
+          event_stats: eventStats,
+          auth_stats: {
+            success: authSuccess,
+            failure: authFailure,
+            total: authLogs.length
+          },
+          recent_logs: recentLogs.logs,
+          total_logs: recentLogs.total
+        }
+      });
+    } catch (error) {
+      console.error('获取设备概览失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备概览失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  static async getDeviceAuthStats(req: Request, res: Response): Promise<void> {
+    try {
+      const { clientid } = req.params;
+
+      const clientidStr = toString(clientid);
+      const authLogs = await AuthLogModel.getByClientid(clientidStr);
+      const success = authLogs.filter((log: any) => log.result === 'success').length;
+      const failure = authLogs.filter((log: any) => log.result === 'failure').length;
+
+      const operationStats = await AuthLogModel.getOperationTypeStats();
+
+      res.status(200).json({
+        success: true,
+        data: {
+          success,
+          failure,
+          total: authLogs.length,
+          operation_stats: operationStats
+        }
+      });
+    } catch (error) {
+      console.error('获取设备认证统计失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取设备认证统计失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+}

+ 89 - 0
mqtt-vue-dashboard/server/src/controllers/mqttBrokerController.ts

@@ -0,0 +1,89 @@
+import { Request, Response } from 'express';
+import { MqttBrokerService } from '../services/mqttBrokerService';
+
+export class MqttBrokerController {
+  static async getStatus(req: Request, res: Response) {
+    try {
+      const broker = MqttBrokerService.getInstance();
+      const clients = broker.getConnectedClients();
+      res.json({
+        success: true,
+        data: {
+          running: true,
+          connectedClients: clients.length,
+          port: parseInt(process.env.MQTT_BROKER_PORT || '1883', 10),
+          allowAnonymous: process.env.MQTT_ALLOW_ANONYMOUS === 'true'
+        }
+      });
+    } catch (error: any) {
+      res.status(500).json({
+        success: false,
+        message: error.message || '获取Broker状态失败'
+      });
+    }
+  }
+
+  static async getConnectedClients(req: Request, res: Response) {
+    try {
+      const broker = MqttBrokerService.getInstance();
+      const clients = broker.getConnectedClients();
+      res.json({
+        success: true,
+        data: clients
+      });
+    } catch (error: any) {
+      res.status(500).json({
+        success: false,
+        message: error.message || '获取连接客户端失败'
+      });
+    }
+  }
+
+  static async disconnectClient(req: Request, res: Response) {
+    try {
+      const clientId = req.params.clientId as string;
+      const broker = MqttBrokerService.getInstance();
+      const result = broker.disconnectClient(clientId);
+      if (result) {
+        res.json({
+          success: true,
+          message: `客户端 ${clientId} 已断开连接`
+        });
+      } else {
+        res.status(404).json({
+          success: false,
+          message: `客户端 ${clientId} 未找到`
+        });
+      }
+    } catch (error: any) {
+      res.status(500).json({
+        success: false,
+        message: error.message || '断开客户端失败'
+      });
+    }
+  }
+
+  static async publishMessage(req: Request, res: Response) {
+    try {
+      const { topic, payload, qos, retain } = req.body;
+      if (!topic || payload === undefined) {
+        res.status(400).json({
+          success: false,
+          message: '缺少必要参数: topic, payload'
+        });
+        return;
+      }
+      const broker = MqttBrokerService.getInstance();
+      await broker.publish(topic, typeof payload === 'object' ? JSON.stringify(payload) : String(payload), { qos: qos || 0, retain: retain || false });
+      res.json({
+        success: true,
+        message: '消息发布成功'
+      });
+    } catch (error: any) {
+      res.status(500).json({
+        success: false,
+        message: error.message || '发布消息失败'
+      });
+    }
+  }
+}

+ 380 - 0
mqtt-vue-dashboard/server/src/controllers/mqttMessageController.ts

@@ -0,0 +1,380 @@
+import { Request, Response } from 'express';
+import { MqttMessageModel } from '../models/mqttMessage';
+import { toString } from '../utils/helpers';
+
+export class MqttMessageController {
+  // 获取所有消息
+  static async getAllMessages(req: Request, res: Response): Promise<void> {
+    try {
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      const messages = await MqttMessageModel.getAll(limit, offset);
+      const total = await MqttMessageModel.getCount();
+      
+      res.status(200).json({
+        success: true,
+        data: messages,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取消息列表失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取消息列表失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据客户端ID获取消息
+  static async getMessagesByClientId(req: Request, res: Response): Promise<void> {
+    try {
+      const clientid = toString(req.params.clientid);
+      const limit = Number(req.query.limit) || 50;
+      
+      if (!clientid) {
+        res.status(400).json({
+          success: false,
+          message: '客户端ID不能为空'
+        });
+        return;
+      }
+      
+      const messages = await MqttMessageModel.getByClientId(clientid, limit);
+      
+      res.status(200).json({
+        success: true,
+        data: messages
+      });
+    } catch (error) {
+      console.error('根据客户端ID获取消息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据客户端ID获取消息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据主题获取消息
+  static async getMessagesByTopic(req: Request, res: Response): Promise<void> {
+    try {
+      const topic = toString(req.params.topic);
+      const limit = Number(req.query.limit) || 50;
+      
+      if (!topic) {
+        res.status(400).json({
+          success: false,
+          message: '主题不能为空'
+        });
+        return;
+      }
+      
+      const messages = await MqttMessageModel.getByTopic(topic, limit);
+      
+      res.status(200).json({
+        success: true,
+        data: messages
+      });
+    } catch (error) {
+      console.error('根据主题获取消息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据主题获取消息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据消息类型获取消息
+  static async getMessagesByType(req: Request, res: Response): Promise<void> {
+    try {
+      const type = toString(req.params.type);
+      const limit = Number(req.query.limit) || 50;
+      
+      if (!type || !['publish', 'subscribe', 'unsubscribe'].includes(type)) {
+        res.status(400).json({
+          success: false,
+          message: '无效的消息类型'
+        });
+        return;
+      }
+      
+      const messages = await MqttMessageModel.getByType(type, limit);
+      
+      res.status(200).json({
+        success: true,
+        data: messages
+      });
+    } catch (error) {
+      console.error('根据消息类型获取消息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据消息类型获取消息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取指定时间范围内的消息
+  static async getMessagesByTimeRange(req: Request, res: Response): Promise<void> {
+    try {
+      const { startTime, endTime } = req.query;
+      
+      if (!startTime || !endTime) {
+        res.status(400).json({
+          success: false,
+          message: '开始时间和结束时间不能为空'
+        });
+        return;
+      }
+      
+      const start = new Date(startTime as string);
+      const end = new Date(endTime as string);
+      
+      if (isNaN(start.getTime()) || isNaN(end.getTime())) {
+        res.status(400).json({
+          success: false,
+          message: '无效的时间格式'
+        });
+        return;
+      }
+      
+      const messages = await MqttMessageModel.getByTimeRange(start, end);
+      
+      res.status(200).json({
+        success: true,
+        data: messages
+      });
+    } catch (error) {
+      console.error('根据时间范围获取消息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据时间范围获取消息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 获取消息类型统计
+   */
+  static async getMessageTypeStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await MqttMessageModel.getTypeStats();
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取消息类型统计成功'
+      });
+    } catch (error) {
+      res.status(500).json({
+        success: false,
+        message: '获取消息类型统计失败',
+        error: error instanceof Error ? error.message : String(error)
+      });
+    }
+  }
+
+  // 获取QoS等级统计
+  static async getMessageQosStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await MqttMessageModel.getQosStats();
+      
+      res.status(200).json({
+        success: true,
+        data: stats
+      });
+    } catch (error) {
+      console.error('获取QoS等级统计失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取QoS等级统计失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  /**
+   * 获取消息大小统计
+   */
+  static async getMessageSizeStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await MqttMessageModel.getSizeStats();
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取消息大小统计成功'
+      });
+    } catch (error) {
+      res.status(500).json({
+        success: false,
+        message: '获取消息大小统计失败',
+        error: error instanceof Error ? error.message : String(error)
+      });
+    }
+  }
+
+  /**
+   * 获取每小时消息统计
+   */
+  static async getHourlyStats(req: Request, res: Response): Promise<void> {
+    try {
+      const hours = req.query.hours ? parseInt(req.query.hours as string) : 24;
+      const stats = await MqttMessageModel.getHourlyStats(hours);
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取每小时消息统计成功'
+      });
+    } catch (error) {
+      res.status(500).json({
+        success: false,
+        message: '获取每小时消息统计失败',
+        error: error instanceof Error ? error.message : String(error)
+      });
+    }
+  }
+
+  // 获取QoS等级统计
+  static async getQosStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await MqttMessageModel.getQosStats();
+      
+      res.status(200).json({
+        success: true,
+        data: stats
+      });
+    } catch (error) {
+      console.error('获取QoS等级统计失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取QoS等级统计失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取每小时消息统计
+  static async getHourlyMessageStats(req: Request, res: Response): Promise<void> {
+    try {
+      const hours = Number(req.query.hours) || 24;
+      
+      if (hours < 1 || hours > 168) { // 限制最大7天
+        res.status(400).json({
+          success: false,
+          message: '小时数必须在1到168之间'
+        });
+        return;
+      }
+      
+      const stats = await MqttMessageModel.getHourlyStats(hours);
+      
+      res.status(200).json({
+        success: true,
+        data: stats
+      });
+    } catch (error) {
+      console.error('获取每小时消息统计失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取每小时消息统计失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取热门主题
+  static async getPopularTopics(req: Request, res: Response): Promise<void> {
+    try {
+      const limit = Number(req.query.limit) || 10;
+      
+      if (limit < 1 || limit > 50) {
+        res.status(400).json({
+          success: false,
+          message: '限制数量必须在1到50之间'
+        });
+        return;
+      }
+      
+      const topics = await MqttMessageModel.getPopularTopics(limit);
+      
+      res.status(200).json({
+        success: true,
+        data: topics
+      });
+    } catch (error) {
+      console.error('获取热门主题失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取热门主题失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取活跃客户端
+  static async getActiveClients(req: Request, res: Response): Promise<void> {
+    try {
+      const limit = Number(req.query.limit) || 10;
+      
+      if (limit < 1 || limit > 50) {
+        res.status(400).json({
+          success: false,
+          message: '限制数量必须在1到50之间'
+        });
+        return;
+      }
+      
+      const clients = await MqttMessageModel.getActiveClients(limit);
+      
+      res.status(200).json({
+        success: true,
+        data: clients
+      });
+    } catch (error) {
+      console.error('获取活跃客户端失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取活跃客户端失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取消息热力图数据
+  static async getMessageHeatmapData(req: Request, res: Response): Promise<void> {
+    try {
+      const days = Number(req.query.days) || 7;
+      
+      if (days < 1 || days > 30) {
+        res.status(400).json({
+          success: false,
+          message: '天数必须在1到30之间'
+        });
+        return;
+      }
+      
+      const heatmapData = await MqttMessageModel.getHeatmapData(days);
+      
+      res.status(200).json({
+        success: true,
+        data: heatmapData
+      });
+    } catch (error) {
+      console.error('获取消息热力图数据失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取消息热力图数据失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+}

+ 897 - 0
mqtt-vue-dashboard/server/src/controllers/otaController.ts

@@ -0,0 +1,897 @@
+import { Request, Response } from 'express';
+import { FirmwareFileModel, FirmwareFile } from '../models/firmware';
+import { OTATaskModel, OTATask } from '../models/ota';
+import { DeviceModel } from '../models/device';
+import { generateMD5, verifyMD5, getFileSize } from '../utils/fileUtils';
+import fs from 'fs/promises';
+import path from 'path';
+import { MqttBrokerService } from '../services/mqttBrokerService';
+import { LoggerService } from '../services/loggerService';
+import { toString } from '../utils/helpers';
+import { SystemLogModel } from '../models/systemLog';
+
+// 扩展Request接口,添加file属性
+declare global {
+  namespace Express {
+    interface Request {
+      file?: any;
+    }
+  }
+}
+
+// 创建固件文件上传目录
+const firmwareUploadDir = '/home/yangfei/OTA/firmware';
+
+// 确保固件上传目录存在
+async function ensureUploadDirExists() {
+  try {
+    await fs.access(firmwareUploadDir);
+  } catch {
+    await fs.mkdir(firmwareUploadDir, { recursive: true });
+  }
+}
+
+// 初始化上传目录
+ensureUploadDirExists();
+
+export class OtaController {
+  private static async logOTAToSystemLog(
+    deviceId: string,
+    message: string,
+    level: 'info' | 'warn' | 'error' = 'info',
+    details?: any
+  ): Promise<void> {
+    try {
+      await SystemLogModel.create({
+        level,
+        message,
+        source: 'ota',
+        module: 'device_ota',
+        details: details ? JSON.stringify({ ...details, deviceId }) : JSON.stringify({ deviceId })
+      });
+    } catch (error) {
+      console.error('记录OTA系统日志失败:', error);
+    }
+  }
+
+  // 上传固件文件
+  static async uploadFirmware(req: Request, res: Response): Promise<void> {
+    try {
+      const file = req.file;
+      if (!file) {
+        res.status(400).json({ 
+          success: false, 
+          message: 'No file uploaded',
+          error: 'No file uploaded' 
+        });
+        return;
+      }
+
+      // 验证文件类型
+      if (!file.mimetype.startsWith('application/octet-stream')) {
+        res.status(400).json({ 
+          success: false, 
+          message: 'Invalid file type. Only binary files are allowed.',
+          error: 'Invalid file type. Only binary files are allowed.' 
+        });
+        return;
+      }
+
+      // 验证版本号格式
+      const version = req.body.version;
+      if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
+        res.status(400).json({ 
+          success: false, 
+          message: 'Invalid version format. Please use semantic versioning (e.g., 1.0.0).',
+          error: 'Invalid version format. Please use semantic versioning (e.g., 1.0.0).' 
+        });
+        return;
+      }
+
+      // 计算文件MD5
+      const md5sum = await generateMD5(file.path);
+
+      // 移动文件到上传目录
+      const newFilename = `firmware-${version}.bin`;
+      const newFilePath = path.join(firmwareUploadDir, newFilename);
+      await fs.rename(file.path, newFilePath);
+
+      // 创建固件文件记录
+      const firmwareData: Omit<FirmwareFile, 'id' | 'created_at' | 'updated_at'> = {
+        version,
+        filename: newFilename,
+        filepath: newFilePath,
+        filesize: file.size,
+        md5sum,
+        description: req.body.description || '',
+        status: req.body.status as 'active' | 'inactive' || 'active',
+        created_by: req.user?.id || 'system'
+      };
+
+      const firmware = await FirmwareFileModel.create(firmwareData);
+
+      // 记录固件上传成功日志
+      LoggerService.info('固件上传成功', {
+        source: 'ota',
+        module: 'upload_firmware',
+        details: JSON.stringify({
+          firmwareId: firmware.id,
+          version,
+          filename: newFilename,
+          filesize: file.size,
+          md5sum,
+          createdBy: req.user?.id || 'system'
+        })
+      }).catch(err => {
+        console.error('固件上传成功日志写入失败:', err);
+      });
+
+      res.status(201).json({ 
+        success: true, 
+        message: 'Firmware uploaded successfully', 
+        data: firmware 
+      });
+    } catch (error) {
+      console.error('Error uploading firmware:', error);
+      
+      // 记录固件上传失败日志
+      LoggerService.error('固件上传失败', {
+        source: 'ota',
+        module: 'upload_firmware',
+        details: JSON.stringify({
+          error: error instanceof Error ? error.message : '未知错误',
+          version: req.body.version,
+          filename: req.file?.originalname
+        })
+      }).catch(err => {
+        console.error('固件上传失败日志写入失败:', err);
+      });
+      
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to upload firmware',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 获取所有固件文件
+  static async getFirmwareFiles(req: Request, res: Response): Promise<void> {
+    try {
+      const firmwareFiles = await FirmwareFileModel.getAll();
+      res.status(200).json({
+        success: true,
+        message: '固件文件列表获取成功',
+        data: firmwareFiles
+      });
+    } catch (error) {
+      console.error('Error getting firmware files:', error);
+      res.status(500).json({
+        success: false,
+        message: 'Failed to get firmware files',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 删除固件文件
+  static async deleteFirmware(req: Request, res: Response): Promise<void> {
+    try {
+      const id = parseInt(toString(req.params.id));
+      const firmware = await FirmwareFileModel.getById(id);
+
+      if (!firmware) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'Firmware not found',
+          error: 'Firmware not found' 
+        });
+        return;
+      }
+
+      // 删除物理文件
+      try {
+        await fs.access(firmware.filepath);
+        await fs.unlink(firmware.filepath);
+      } catch (error) {
+        console.error('Error deleting firmware file:', error);
+        // 继续执行,即使文件删除失败也不影响数据库记录删除
+      }
+
+      // 删除数据库记录
+      await FirmwareFileModel.delete(id);
+
+      res.status(200).json({ 
+        success: true, 
+        message: 'Firmware deleted successfully' 
+      });
+    } catch (error) {
+      console.error('Error deleting firmware:', error);
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to delete firmware',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 为设备创建OTA升级任务
+  static async createOTATask(req: Request, res: Response): Promise<void> {
+    try {
+      const deviceId = toString(req.params.deviceId);
+      const firmwareId = parseInt(req.body.firmwareId);
+
+      // 验证设备是否存在
+      const device = await DeviceModel.getByClientId(deviceId);
+      if (!device) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'Device not found',
+          error: 'Device not found' 
+        });
+        return;
+      }
+
+      // 验证固件是否存在
+      const firmware = await FirmwareFileModel.getById(firmwareId);
+      if (!firmware) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'Firmware not found',
+          error: 'Firmware not found' 
+        });
+        return;
+      }
+
+      // 创建OTA任务
+      const taskData: Omit<OTATask, 'id' | 'created_at' | 'updated_at'> = {
+        device_id: deviceId,
+        firmware_id: firmwareId,
+        status: 'pending',
+        progress: 0,
+        start_time: new Date()
+      };
+
+      const task = await OTATaskModel.create(taskData);
+
+      await this.logOTAToSystemLog(
+        deviceId,
+        `OTA升级任务已创建,固件版本: ${firmware.version}`,
+        'info',
+        {
+          taskId: task.id,
+          firmwareId: firmware.id,
+          firmwareVersion: firmware.version,
+          status: 'pending'
+        }
+      );
+
+      // 如果设备在线,立即通过MQTT发送OTA升级指令
+      // 如果设备离线,任务会保持pending状态,等设备上线时自动执行
+      if (device.status === 'online') {
+        // 通过MQTT发送OTA升级指令
+        const mqttBroker = MqttBrokerService.getInstance();
+        // 使用OTA服务器URL构建固件下载地址
+        let otaServerUrl = process.env.OTA_SERVER_URL || process.env.BACKEND_URL || 'http://localhost:3002';
+        // 移除URL末尾的斜杠,避免双斜杠问题
+        otaServerUrl = otaServerUrl.replace(/\/$/, '');
+        console.log('OTA Server URL:', otaServerUrl);
+        console.log('Environment OTA_SERVER_URL:', process.env.OTA_SERVER_URL);
+        console.log('Environment BACKEND_URL:', process.env.BACKEND_URL);
+        const otaCommand = {
+          act: 'upgrade',
+          ver: firmware.version,
+          url: `${otaServerUrl}/api/ota/firmware/${firmware.id}`,
+          md5: firmware.md5sum,
+          tid: task.id
+        };
+        console.log('OTA升级指令:', JSON.stringify(otaCommand, null, 2));
+        console.log('MQTT Topic:', `device/${deviceId}/ota`);
+
+        mqttBroker.publish(`device/${deviceId}/ota`, JSON.stringify(otaCommand));
+
+        // 记录OTA任务创建成功日志
+        LoggerService.info('OTA升级任务创建成功', {
+          source: 'ota',
+          module: 'create_ota_task',
+          details: JSON.stringify({
+            taskId: task.id,
+            deviceId,
+            deviceName: device.device_name,
+            firmwareId: firmware.id,
+            firmwareVersion: firmware.version,
+            firmwareUrl: `${otaServerUrl}/api/ota/firmware/${firmware.id}`,
+            deviceStatus: 'online',
+            createdBy: req.user?.id || 'system'
+          })
+        }).catch(err => {
+          console.error('OTA任务创建成功日志写入失败:', err);
+        });
+      } else {
+        // 设备离线,记录离线任务创建日志
+        LoggerService.info('OTA升级任务已创建(设备离线,等待上线后执行)', {
+          source: 'ota',
+          module: 'create_ota_task',
+          details: JSON.stringify({
+            taskId: task.id,
+            deviceId,
+            deviceName: device.device_name,
+            firmwareId: firmware.id,
+            firmwareVersion: firmware.version,
+            deviceStatus: 'offline'
+          })
+        }).catch(err => {
+          console.error('OTA离线任务创建日志写入失败:', err);
+        });
+      }
+
+      res.status(201).json({ 
+        success: true, 
+        message: device.status === 'online' ? 'OTA task created successfully' : 'OTA task created (device offline, will execute when online)', 
+        data: task 
+      });
+    } catch (error) {
+      console.error('Error creating OTA task:', error);
+      
+      // 记录OTA任务创建失败日志
+      LoggerService.error('OTA升级任务创建失败', {
+        source: 'ota',
+        module: 'create_ota_task',
+        details: JSON.stringify({
+          deviceId: toString(req.params.deviceId),
+          firmwareId: req.body.firmwareId,
+          error: error instanceof Error ? error.message : '未知错误'
+        })
+      }).catch(err => {
+        console.error('OTA任务创建失败日志写入失败:', err);
+      });
+      
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to create OTA task',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 获取设备的OTA任务
+  static async getDeviceOTATasks(req: Request, res: Response): Promise<void> {
+    try {
+      const deviceId = toString(req.params.deviceId);
+      const tasks = await OTATaskModel.getByDeviceId(deviceId);
+      res.status(200).json({ 
+        success: true, 
+        message: 'Device OTA tasks retrieved successfully', 
+        data: tasks 
+      });
+    } catch (error) {
+      console.error('Error getting device OTA tasks:', error);
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to get device OTA tasks',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 获取所有OTA任务
+  static async getAllOTATasks(req: Request, res: Response): Promise<void> {
+    try {
+      const tasks = await OTATaskModel.getAll();
+      res.status(200).json({ 
+        success: true, 
+        message: 'All OTA tasks retrieved successfully', 
+        data: tasks 
+      });
+    } catch (error) {
+      console.error('Error getting all OTA tasks:', error);
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to get all OTA tasks',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 下载固件文件
+  static async downloadFirmware(req: Request, res: Response): Promise<void> {
+    try {
+      const id = parseInt(toString(req.params.id));
+      const firmware = await FirmwareFileModel.getById(id);
+
+      if (!firmware) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'Firmware not found',
+          error: '固件文件不存在' 
+        });
+        return;
+      }
+
+      try {
+        await fs.access(firmware.filepath);
+      } catch {
+        res.status(404).json({ 
+          success: false, 
+          message: 'Firmware file not found on server',
+          error: '服务器上不存在固件文件' 
+        });
+        return;
+      }
+
+      // 对于文件下载,我们直接使用res.download,不返回JSON格式
+      res.download(firmware.filepath, firmware.filename);
+    } catch (error) {
+      console.error('Error downloading firmware:', error);
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to download firmware',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 更新OTA任务状态
+  static async updateOTATaskStatus(req: Request, res: Response): Promise<void> {
+    try {
+      const taskId = parseInt(toString(req.params.taskId));
+      const { status, progress, error_message } = req.body;
+
+      // 验证任务是否存在
+      const task = await OTATaskModel.getById(taskId);
+      if (!task) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'OTA task not found',
+          error: 'OTA任务不存在' 
+        });
+        return;
+      }
+
+      // 更新任务状态和进度
+      if (status && progress !== undefined) {
+        await OTATaskModel.updateStatusAndProgress(taskId, status as any, progress);
+        
+        await this.logOTAToSystemLog(
+          task.device_id,
+          `OTA升级进度更新: ${progress}%, 状态: ${status}`,
+          'info',
+          {
+            taskId,
+            status,
+            progress
+          }
+        );
+      }
+
+      // 如果任务完成,更新结束时间和结果
+      if (status === 'success' || status === 'failed') {
+        await OTATaskModel.updateResult(taskId, status as 'success' | 'failed', error_message);
+
+        // 如果升级成功,更新设备的固件版本
+        if (status === 'success') {
+          const firmware = await FirmwareFileModel.getById(task.firmware_id);
+          if (firmware) {
+            await DeviceModel.update(task.device_id, { firmware_version: firmware.version });
+          }
+          
+          await this.logOTAToSystemLog(
+            task.device_id,
+            `OTA升级成功,固件版本已更新`,
+            'info',
+            {
+              taskId,
+              firmwareId: task.firmware_id,
+              status: 'success',
+              progress: 100
+            }
+          );
+          
+          LoggerService.info('OTA升级任务完成', {
+            source: 'ota',
+            module: 'update_ota_task_status',
+            details: JSON.stringify({
+              taskId,
+              deviceId: task.device_id,
+              firmwareId: task.firmware_id,
+              status: 'success',
+              progress: 100
+            })
+          }).catch(err => {
+            console.error('OTA升级成功日志写入失败:', err);
+          });
+        } else {
+          await this.logOTAToSystemLog(
+            task.device_id,
+            `OTA升级失败: ${error_message || '未知错误'}`,
+            'error',
+            {
+              taskId,
+              firmwareId: task.firmware_id,
+              status: 'failed',
+              errorMessage: error_message
+            }
+          );
+          
+          LoggerService.error('OTA升级任务失败', {
+            source: 'ota',
+            module: 'update_ota_task_status',
+            details: JSON.stringify({
+              taskId,
+              deviceId: task.device_id,
+              firmwareId: task.firmware_id,
+              status: 'failed',
+              errorMessage: error_message || '未知错误'
+            })
+          }).catch(err => {
+            console.error('OTA升级失败日志写入失败:', err);
+          });
+        }
+      }
+
+      res.status(200).json({ 
+        success: true, 
+        message: 'OTA task status updated successfully',
+        data: { taskId, status, progress } 
+      });
+    } catch (error) {
+      console.error('Error updating OTA task status:', error);
+      
+      // 记录OTA任务状态更新失败日志
+      LoggerService.error('OTA任务状态更新失败', {
+        source: 'ota',
+        module: 'update_ota_task_status',
+        details: JSON.stringify({
+          taskId: req.params.taskId,
+          status: req.body.status,
+          progress: req.body.progress,
+          error: error instanceof Error ? error.message : '未知错误'
+        })
+      }).catch(err => {
+        console.error('OTA任务状态更新失败日志写入失败:', err);
+      });
+      
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to update OTA task status',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 取消OTA任务
+  static async cancelOTATask(req: Request, res: Response): Promise<void> {
+    try {
+      const taskId = parseInt(toString(req.params.taskId));
+
+      // 验证任务是否存在
+      const task = await OTATaskModel.getById(taskId);
+      if (!task) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'OTA task not found',
+          error: 'OTA任务不存在' 
+        });
+        return;
+      }
+
+      // 只有待处理、下载中或安装中的任务可以取消
+      if (!['pending', 'downloading', 'installing'].includes(task.status)) {
+        res.status(400).json({ 
+          success: false, 
+          message: 'Only pending, downloading, or installing tasks can be cancelled',
+          error: '只有待处理、下载中或安装中的任务可以取消' 
+        });
+        return;
+      }
+
+      // 更新任务状态为已取消
+      await OTATaskModel.updateResult(taskId, 'failed', '用户取消');
+
+      await this.logOTAToSystemLog(
+        task.device_id,
+        `OTA升级任务已取消`,
+        'warn',
+        {
+          taskId,
+          status: 'failed',
+          reason: '用户取消'
+        }
+      );
+
+      res.status(200).json({ 
+        success: true, 
+        message: 'OTA task cancelled successfully',
+        data: { taskId, status: 'failed' } 
+      });
+    } catch (error) {
+      console.error('Error cancelling OTA task:', error);
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to cancel OTA task',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 重试OTA任务
+  static async retryOTATask(req: Request, res: Response): Promise<void> {
+    try {
+      const taskId = parseInt(toString(req.params.taskId));
+
+      // 验证任务是否存在
+      const task = await OTATaskModel.getById(taskId);
+      if (!task) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'OTA task not found',
+          error: 'OTA任务不存在' 
+        });
+        return;
+      }
+
+      // 只有失败或取消的任务可以重试
+      if (!['failed', 'pending'].includes(task.status)) {
+        res.status(400).json({ 
+          success: false, 
+          message: 'Only failed or pending tasks can be retried',
+          error: '只有失败或待处理的任务可以重试' 
+        });
+        return;
+      }
+
+      // 验证设备是否存在
+      const device = await DeviceModel.getByClientId(task.device_id);
+      if (!device) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'Device not found',
+          error: '设备不存在' 
+        });
+        return;
+      }
+
+      // 验证固件是否存在
+      const firmware = await FirmwareFileModel.getById(task.firmware_id);
+      if (!firmware) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'Firmware not found',
+          error: '固件不存在' 
+        });
+        return;
+      }
+
+      // 重置任务状态
+      await OTATaskModel.updateStatusAndProgress(taskId, 'pending', 0);
+
+      // 通过MQTT发送OTA升级指令
+      const mqttBroker = MqttBrokerService.getInstance();
+      let otaServerUrl = process.env.OTA_SERVER_URL || process.env.BACKEND_URL || 'http://localhost:3002';
+      otaServerUrl = otaServerUrl.replace(/\/$/, '');
+      
+      const otaCommand = {
+        act: 'upgrade',
+        ver: firmware.version,
+        url: `${otaServerUrl}/api/ota/firmware/${firmware.id}`,
+        md5: firmware.md5sum,
+        tid: taskId
+      };
+      console.log('OTA升级指令(重试):', JSON.stringify(otaCommand, null, 2));
+      console.log('MQTT Topic:', `device/${task.device_id}/ota`);
+
+      mqttBroker.publish(`device/${task.device_id}/ota`, JSON.stringify(otaCommand));
+
+      await this.logOTAToSystemLog(
+        task.device_id,
+        `OTA升级任务已重试,固件版本: ${firmware.version}`,
+        'info',
+        {
+          taskId,
+          firmwareId: firmware.id,
+          firmwareVersion: firmware.version,
+          status: 'pending'
+        }
+      );
+
+      res.status(200).json({ 
+        success: true, 
+        message: 'OTA task retried successfully',
+        data: { taskId, status: 'pending' } 
+      });
+    } catch (error) {
+      console.error('Error retrying OTA task:', error);
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to retry OTA task',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 删除OTA任务
+  static async deleteOTATask(req: Request, res: Response): Promise<void> {
+    try {
+      const taskId = parseInt(toString(req.params.taskId));
+
+      // 验证任务是否存在
+      const task = await OTATaskModel.getById(taskId);
+      if (!task) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'OTA task not found',
+          error: 'OTA任务不存在' 
+        });
+        return;
+      }
+
+      // 只有待处理、失败或取消的任务可以删除
+      if (!['pending', 'failed'].includes(task.status)) {
+        res.status(400).json({ 
+          success: false, 
+          message: 'Only pending or failed tasks can be deleted',
+          error: '只有待处理或失败的任务可以删除' 
+        });
+        return;
+      }
+
+      // 删除任务
+      await OTATaskModel.delete(taskId);
+
+      await this.logOTAToSystemLog(
+        task.device_id,
+        `OTA升级任务已删除`,
+        'info',
+        {
+          taskId,
+          status: task.status,
+          reason: '手动删除'
+        }
+      );
+
+      res.status(200).json({ 
+        success: true, 
+        message: 'OTA task deleted successfully',
+        data: { taskId } 
+      });
+    } catch (error) {
+      console.error('Error deleting OTA task:', error);
+      res.status(500).json({ 
+        success: false, 
+        message: 'Failed to delete OTA task',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+
+  // 批量创建OTA升级任务
+  static async bulkCreateOTATask(req: Request, res: Response): Promise<void> {
+    try {
+      const { firmwareId, deviceIds, retryCount = 3, retryInterval = 10000, timeout = 30000 } = req.body;
+
+      if (!Array.isArray(deviceIds) || deviceIds.length === 0) {
+        res.status(400).json({ 
+          success: false, 
+          message: 'Device IDs must be a non-empty array',
+          error: '设备ID必须是非空数组' 
+        });
+        return;
+      }
+
+      // 验证固件是否存在
+      const firmware = await FirmwareFileModel.getById(firmwareId);
+      if (!firmware) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'Firmware not found',
+          error: '固件不存在' 
+        });
+        return;
+      }
+
+      // 验证所有设备是否存在
+      const validDevices: string[] = [];
+      const invalidDevices: string[] = [];
+      
+      for (const deviceId of deviceIds) {
+        const device = await DeviceModel.getByClientId(deviceId);
+        if (device) {
+          validDevices.push(deviceId);
+        } else {
+          invalidDevices.push(deviceId);
+        }
+      }
+
+      if (validDevices.length === 0) {
+        res.status(404).json({ 
+          success: false, 
+          message: 'No valid devices found',
+          error: '未找到有效设备' 
+        });
+        return;
+      }
+
+      // 为每个有效设备创建OTA任务
+      const createdTasks: any[] = [];
+      const onlineTasks: any[] = [];
+      const offlineTasks: any[] = [];
+      const mqttBroker = MqttBrokerService.getInstance();
+      const otaServerUrl = process.env.OTA_SERVER_URL || process.env.BACKEND_URL || 'http://localhost:3002';
+      
+      for (const deviceId of validDevices) {
+        // 获取设备信息以检查在线状态
+        const device = await DeviceModel.getByClientId(deviceId);
+        
+        // 创建OTA任务
+        const taskData: Omit<OTATask, 'id' | 'created_at' | 'updated_at'> = {
+          device_id: deviceId,
+          firmware_id: firmwareId,
+          status: 'pending',
+          progress: 0,
+          start_time: new Date()
+        };
+
+        const task = await OTATaskModel.create(taskData);
+        createdTasks.push(task);
+
+        // 如果设备在线,立即发送OTA升级指令
+        // 如果设备离线,任务会保持pending状态,等设备上线时自动执行
+        if (device && device.status === 'online') {
+          onlineTasks.push({ taskId: task.id, deviceId });
+          
+          // 通过MQTT发送OTA升级指令
+          const otaCommand = {
+            act: 'upgrade',
+            ver: firmware.version,
+            url: `${otaServerUrl}/api/ota/firmware/${firmware.id}`,
+            md5: firmware.md5sum,
+            tid: task.id,
+            rc: retryCount,
+            ri: retryInterval,
+            to: timeout
+          };
+          console.log('OTA升级指令(批量):', JSON.stringify(otaCommand, null, 2));
+          console.log('MQTT Topic:', `device/${deviceId}/ota`);
+
+          mqttBroker.publish(`device/${deviceId}/ota`, JSON.stringify(otaCommand));
+        } else {
+          offlineTasks.push({ taskId: task.id, deviceId });
+          console.log(`设备 ${deviceId} 离线,OTA任务 ${task.id} 将在设备上线后自动执行`);
+        }
+      }
+
+      // 记录批量创建结果
+      LoggerService.info('批量OTA任务创建完成', {
+        source: 'ota',
+        module: 'bulk_create_ota_task',
+        details: JSON.stringify({
+          totalTasks: createdTasks.length,
+          onlineTasks: onlineTasks.length,
+          offlineTasks: offlineTasks.length,
+          invalidDevices: invalidDevices.length
+        })
+      }).catch(err => {
+        console.error('批量OTA任务创建日志写入失败:', err);
+      });
+
+      res.status(201).json({ 
+        success: true, 
+        message: `批量OTA任务创建成功(在线设备: ${onlineTasks.length}, 离线设备: ${offlineTasks.length})`, 
+        data: {
+          tasks: createdTasks,
+          validDevices: validDevices.length,
+          invalidDevices: invalidDevices,
+          onlineTasks: onlineTasks.length,
+          offlineTasks: offlineTasks.length
+        }
+      });
+    } catch (error) {
+      console.error('Error creating bulk OTA tasks:', error);
+      res.status(500).json({ 
+        success: false, 
+        message: '批量OTA任务创建失败',
+        error: error instanceof Error ? error.message : '未知错误' 
+      });
+    }
+  }
+}

+ 175 - 0
mqtt-vue-dashboard/server/src/controllers/permissionController.ts

@@ -0,0 +1,175 @@
+import { Request, Response } from 'express';
+import { AppError } from '../middleware/errorHandler';
+import { PermissionModel } from '../models/permission';
+import { toString } from '../utils/helpers';
+
+/**
+ * 权限控制器
+ * 处理权限管理相关的API请求
+ */
+export class PermissionController {
+  /**
+   * 获取所有页面列表
+   */
+  static async getAllPages(req: Request, res: Response): Promise<void> {
+    try {
+      // 从请求对象中获取当前用户
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以访问', 403);
+      }
+
+      const pages = await PermissionModel.getAllPages();
+
+      res.status(200).json({
+        success: true,
+        message: '获取页面列表成功',
+        data: pages
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 获取用户的权限列表
+   */
+  static async getUserPermissions(req: Request, res: Response): Promise<void> {
+    try {
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以访问', 403);
+      }
+
+      const userId = toString(req.params.userId);
+      if (!userId) {
+        throw new AppError('用户ID不能为空', 400);
+      }
+
+      const permissions = await PermissionModel.getUserPermissions(userId);
+
+      res.status(200).json({
+        success: true,
+        message: '获取用户权限列表成功',
+        data: permissions
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 为用户分配权限
+   */
+  static async assignPermission(req: Request, res: Response): Promise<void> {
+    try {
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以分配权限', 403);
+      }
+
+      const userId = toString(req.params.userId);
+      const { pageId } = req.body;
+
+      if (!userId || !pageId) {
+        throw new AppError('用户ID和页面ID不能为空', 400);
+      }
+
+      const permission = await PermissionModel.assignPermission(userId, pageId);
+
+      res.status(201).json({
+        success: true,
+        message: '权限分配成功',
+        data: permission
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 批量为用户分配权限
+   */
+  static async assignPermissions(req: Request, res: Response): Promise<void> {
+    try {
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以分配权限', 403);
+      }
+
+      const userId = toString(req.params.userId);
+      const { pageIds } = req.body;
+
+      if (!userId || !Array.isArray(pageIds)) {
+        throw new AppError('用户ID和页面ID列表不能为空', 400);
+      }
+
+      await PermissionModel.assignPermissions(userId, pageIds);
+
+      res.status(200).json({
+        success: true,
+        message: '权限分配成功'
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 移除用户的权限
+   */
+  static async removePermission(req: Request, res: Response): Promise<void> {
+    try {
+      const currentUser = (req as any).user;
+      if (!currentUser || currentUser.role !== 'admin') {
+        throw new AppError('权限不足,只有管理员可以移除权限', 403);
+      }
+
+      const userId = toString(req.params.userId);
+      const pageId = toString(req.params.pageId);
+
+      if (!userId || !pageId) {
+        throw new AppError('用户ID和页面ID不能为空', 400);
+      }
+
+      const success = await PermissionModel.removePermission(userId, parseInt(pageId));
+
+      if (!success) {
+        throw new AppError('移除权限失败,权限不存在', 404);
+      }
+
+      res.status(200).json({
+        success: true,
+        message: '权限移除成功'
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  /**
+   * 检查用户是否有权限访问某个页面
+   */
+  static async checkPermission(req: Request, res: Response): Promise<void> {
+    try {
+      const userId = toString(req.params.userId);
+      const pagePath = toString(req.params.pagePath);
+
+      if (!userId || !pagePath) {
+        throw new AppError('用户ID和页面路径不能为空', 400);
+      }
+
+      const hasPermission = await PermissionModel.checkUserPermission(userId, pagePath);
+
+      res.status(200).json({
+        success: true,
+        message: '权限检查成功',
+        data: {
+          hasPermission
+        }
+      });
+    } catch (error) {
+      throw error;
+    }
+  }
+}

+ 260 - 0
mqtt-vue-dashboard/server/src/controllers/roomController.ts

@@ -0,0 +1,260 @@
+import { Request, Response } from 'express';
+import { executeQuery } from '../database/connection';
+
+// 获取所有房间
+export const getAllRooms = async (req: Request, res: Response) => {
+  try {
+    const query = `
+      SELECT r.*, 
+        (SELECT COUNT(*) FROM room_devices WHERE room_id = r.id AND status = 'online') as online_devices,
+        (SELECT COUNT(*) FROM room_devices WHERE room_id = r.id) as total_devices
+      FROM rooms r
+      ORDER BY r.floor_id, r.room_number
+    `;
+    
+    const rooms = await executeQuery(query);
+    
+    return res.status(200).json({
+      success: true,
+      data: rooms
+    });
+  } catch (error) {
+    console.error('Error fetching rooms:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch rooms',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 根据ID获取单个房间
+export const getRoomById = async (req: Request, res: Response) => {
+  try {
+    const roomId = req.params.id;
+    
+    const roomQuery = 'SELECT * FROM rooms WHERE id = ?';
+    const room = await executeQuery(roomQuery, [roomId]);
+    
+    if (!room || (Array.isArray(room) && room.length === 0)) {
+      return res.status(404).json({
+        success: false,
+        message: 'Room not found'
+      });
+    }
+    
+    // 获取房间设备
+    const devicesQuery = 'SELECT * FROM room_devices WHERE room_id = ?';
+    const devices = await executeQuery(devicesQuery, [roomId]);
+    
+    return res.status(200).json({
+      success: true,
+      data: {
+        room: Array.isArray(room) ? room[0] : room,
+        devices: devices || []
+      }
+    });
+  } catch (error) {
+    console.error('Error fetching room:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch room',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 根据楼层获取房间
+export const getRoomsByFloor = async (req: Request, res: Response) => {
+  try {
+    const floorId = req.params.floorId;
+    
+    const query = `
+      SELECT r.*, 
+        (SELECT COUNT(*) FROM room_devices WHERE room_id = r.id AND status = 'online') as online_devices,
+        (SELECT COUNT(*) FROM room_devices WHERE room_id = r.id) as total_devices
+      FROM rooms r
+      WHERE r.floor_id = ?
+      ORDER BY r.room_number
+    `;
+    
+    const rooms = await executeQuery(query, [floorId]);
+    
+    return res.status(200).json({
+      success: true,
+      data: rooms
+    });
+  } catch (error) {
+    console.error('Error fetching rooms by floor:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch rooms by floor',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 创建新房间
+export const createRoom = async (req: Request, res: Response) => {
+  try {
+    const { name, floor_id, room_number, room_type, area, description, status, orientation } = req.body;
+    
+    const query = `
+      INSERT INTO rooms (name, floor_id, room_number, room_type, area, description, status, orientation)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+    
+    // 确保所有可能为undefined的值都转换为null
+    const result = await executeQuery(query, [
+      name, 
+      floor_id, 
+      room_number, 
+      room_type, 
+      area || null, 
+      description || null, 
+      status || 'active', 
+      orientation || '东'
+    ]);
+    
+    return res.status(201).json({
+      success: true,
+      message: 'Room created successfully',
+      data: {
+        id: result.insertId,
+        name,
+        floor_id,
+        room_number,
+        room_type,
+        area: area || null,
+        description: description || null,
+        status: status || 'active',
+        orientation: orientation || '东'
+      }
+    });
+  } catch (error) {
+    console.error('Error creating room:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to create room',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 更新房间信息
+export const updateRoom = async (req: Request, res: Response) => {
+  try {
+    const roomId = req.params.id;
+    const { name, floor_id, room_number, room_type, area, description, status, orientation } = req.body;
+    
+    const query = `
+      UPDATE rooms 
+      SET name = ?, floor_id = ?, room_number = ?, room_type = ?, area = ?, description = ?, status = ?, orientation = ?
+      WHERE id = ?
+    `;
+    
+    // 确保所有可能为undefined的值都转换为null
+    await executeQuery(query, [
+      name, 
+      floor_id, 
+      room_number, 
+      room_type, 
+      area || null, 
+      description || null, 
+      status || 'active', 
+      orientation || '东', 
+      roomId
+    ]);
+    
+    return res.status(200).json({
+      success: true,
+      message: 'Room updated successfully'
+    });
+  } catch (error) {
+    console.error('Error updating room:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to update room',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 删除房间
+export const deleteRoom = async (req: Request, res: Response) => {
+  try {
+    const roomId = req.params.id;
+    
+    // 检查房间是否有绑定的设备
+    const checkDevicesQuery = 'SELECT COUNT(*) as deviceCount FROM room_devices WHERE room_id = ?';
+    const deviceCountResult = await executeQuery(checkDevicesQuery, [roomId]);
+    const deviceCount = deviceCountResult[0]?.deviceCount || 0;
+    
+    if (deviceCount > 0) {
+      return res.status(400).json({
+        success: false,
+        message: `该房间有 ${deviceCount} 个设备绑定,请先解绑设备后再删除房间`
+      });
+    }
+    
+    // 删除房间
+    const query = 'DELETE FROM rooms WHERE id = ?';
+    await executeQuery(query, [roomId]);
+    
+    return res.status(200).json({
+      success: true,
+      message: 'Room deleted successfully'
+    });
+  } catch (error) {
+    console.error('Error deleting room:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to delete room',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 获取房间统计信息
+export const getRoomStats = async (req: Request, res: Response) => {
+  try {
+    const totalRoomsQuery = 'SELECT COUNT(*) as total FROM rooms';
+    const totalRoomsResult = await executeQuery(totalRoomsQuery);
+    const totalRooms = totalRoomsResult[0].total;
+    
+    const activeRoomsQuery = 'SELECT COUNT(*) as active FROM rooms WHERE status = "active"';
+    const activeRoomsResult = await executeQuery(activeRoomsQuery);
+    const activeRooms = activeRoomsResult[0].active;
+    
+    const totalDevicesQuery = 'SELECT COUNT(*) as total FROM room_devices';
+    const totalDevicesResult = await executeQuery(totalDevicesQuery);
+    const totalDevices = totalDevicesResult[0].total;
+    
+    const onlineDevicesQuery = 'SELECT COUNT(*) as online FROM room_devices WHERE status = "online"';
+    const onlineDevicesResult = await executeQuery(onlineDevicesQuery);
+    const onlineDevices = onlineDevicesResult[0].online;
+    
+    const floorsQuery = 'SELECT DISTINCT floor_id FROM rooms ORDER BY floor_id';
+    const floorsResult = await executeQuery(floorsQuery);
+    const totalFloors = floorsResult.length;
+    
+    return res.status(200).json({
+      success: true,
+      data: {
+        totalRooms,
+        activeRooms,
+        totalDevices,
+        onlineDevices,
+        totalFloors,
+        offlineDevices: totalDevices - onlineDevices
+      }
+    });
+  } catch (error) {
+    console.error('Error fetching room stats:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch room statistics',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};

+ 263 - 0
mqtt-vue-dashboard/server/src/controllers/roomDeviceController.ts

@@ -0,0 +1,263 @@
+import { Request, Response } from 'express';
+import { executeQuery } from '../database/connection';
+
+// 获取所有设备
+export const getAllDevices = async (req: Request, res: Response) => {
+  try {
+    const query = `
+      SELECT d.*, r.name as room_name, r.floor_id 
+      FROM room_devices d
+      LEFT JOIN rooms r ON d.room_id = r.id
+      ORDER BY r.floor_id, r.room_number, d.name
+    `;
+    
+    const devices = await executeQuery(query);
+    
+    return res.status(200).json({
+      success: true,
+      data: devices
+    });
+  } catch (error) {
+    console.error('Error fetching devices:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch devices',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 根据ID获取单个设备
+export const getDeviceById = async (req: Request, res: Response) => {
+  try {
+    const deviceId = req.params.id;
+    
+    const query = `
+      SELECT d.*, r.name as room_name, r.floor_id 
+      FROM room_devices d
+      LEFT JOIN rooms r ON d.room_id = r.id
+      WHERE d.id = ?
+    `;
+    
+    const device = await executeQuery(query, [deviceId]);
+    
+    if (!device || (Array.isArray(device) && device.length === 0)) {
+      return res.status(404).json({
+        success: false,
+        message: 'Device not found'
+      });
+    }
+    
+    return res.status(200).json({
+      success: true,
+      data: Array.isArray(device) ? device[0] : device
+    });
+  } catch (error) {
+    console.error('Error fetching device:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch device',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 根据房间ID获取设备
+export const getDevicesByRoom = async (req: Request, res: Response) => {
+  try {
+    const roomId = req.params.roomId;
+    
+    const query = 'SELECT * FROM room_devices WHERE room_id = ? ORDER BY name';
+    const devices = await executeQuery(query, [roomId]);
+    
+    return res.status(200).json({
+      success: true,
+      data: devices
+    });
+  } catch (error) {
+    console.error('Error fetching devices by room:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch devices by room',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 根据设备类型获取设备
+export const getDevicesByType = async (req: Request, res: Response) => {
+  try {
+    const deviceType = req.params.type;
+    
+    const query = `
+      SELECT d.*, r.name as room_name, r.floor_id 
+      FROM room_devices d
+      LEFT JOIN rooms r ON d.room_id = r.id
+      WHERE d.type = ?
+      ORDER BY r.floor_id, r.room_number, d.name
+    `;
+    
+    const devices = await executeQuery(query, [deviceType]);
+    
+    return res.status(200).json({
+      success: true,
+      data: devices
+    });
+  } catch (error) {
+    console.error('Error fetching devices by type:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch devices by type',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 创建新设备
+export const createDevice = async (req: Request, res: Response) => {
+  try {
+    const { name, type, model, room_id, status, properties } = req.body;
+    
+    const propertiesJson = properties ? JSON.stringify(properties) : null;
+    
+    const query = `
+      INSERT INTO room_devices (name, type, model, room_id, status, properties)
+      VALUES (?, ?, ?, ?, ?, ?)
+    `;
+    
+    const result = await executeQuery(query, [name, type, model, room_id, status, propertiesJson]);
+    
+    return res.status(201).json({
+      success: true,
+      message: 'Device created successfully',
+      data: {
+        id: result.insertId,
+        name,
+        type,
+        model,
+        room_id,
+        status,
+        properties: propertiesJson
+      }
+    });
+  } catch (error) {
+    console.error('Error creating device:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to create device',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 更新设备信息
+export const updateDevice = async (req: Request, res: Response) => {
+  try {
+    const deviceId = req.params.id;
+    const { name, type, model, room_id, status, properties } = req.body;
+    
+    const propertiesJson = properties ? JSON.stringify(properties) : null;
+    
+    const query = `
+      UPDATE room_devices 
+      SET name = ?, type = ?, model = ?, room_id = ?, status = ?, properties = ?
+      WHERE id = ?
+    `;
+    
+    await executeQuery(query, [name, type, model, room_id, status, propertiesJson, deviceId]);
+    
+    return res.status(200).json({
+      success: true,
+      message: 'Device updated successfully'
+    });
+  } catch (error) {
+    console.error('Error updating device:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to update device',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 更新设备状态
+export const updateDeviceStatus = async (req: Request, res: Response) => {
+  try {
+    const deviceId = req.params.id;
+    const { status } = req.body;
+    
+    const query = 'UPDATE room_devices SET status = ? WHERE id = ?';
+    await executeQuery(query, [status, deviceId]);
+    
+    return res.status(200).json({
+      success: true,
+      message: 'Device status updated successfully'
+    });
+  } catch (error) {
+    console.error('Error updating device status:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to update device status',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 删除设备
+export const deleteDevice = async (req: Request, res: Response) => {
+  try {
+    const deviceId = req.params.id;
+    
+    const query = 'DELETE FROM room_devices WHERE id = ?';
+    await executeQuery(query, [deviceId]);
+    
+    return res.status(200).json({
+      success: true,
+      message: 'Device deleted successfully'
+    });
+  } catch (error) {
+    console.error('Error deleting device:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to delete device',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};
+
+// 获取设备统计信息
+export const getDeviceStats = async (req: Request, res: Response) => {
+  try {
+    const totalDevicesQuery = 'SELECT COUNT(*) as total FROM room_devices';
+    const totalDevicesResult = await executeQuery(totalDevicesQuery);
+    const totalDevices = totalDevicesResult[0].total;
+    
+    const onlineDevicesQuery = 'SELECT COUNT(*) as online FROM room_devices WHERE status = "online"';
+    const onlineDevicesResult = await executeQuery(onlineDevicesQuery);
+    const onlineDevices = onlineDevicesResult[0].online;
+    
+    const offlineDevicesQuery = 'SELECT COUNT(*) as offline FROM room_devices WHERE status = "offline"';
+    const offlineDevicesResult = await executeQuery(offlineDevicesQuery);
+    const offlineDevices = offlineDevicesResult[0].offline;
+    
+    const devicesByTypeQuery = 'SELECT type, COUNT(*) as count FROM room_devices GROUP BY type';
+    const devicesByTypeResult = await executeQuery(devicesByTypeQuery);
+    
+    return res.status(200).json({
+      success: true,
+      data: {
+        totalDevices,
+        onlineDevices,
+        offlineDevices,
+        devicesByType: devicesByTypeResult
+      }
+    });
+  } catch (error) {
+    console.error('Error fetching device stats:', error);
+    return res.status(500).json({
+      success: false,
+      message: 'Failed to fetch device statistics',
+      error: error instanceof Error ? error.message : 'Unknown error'
+    });
+  }
+};

+ 188 - 0
mqtt-vue-dashboard/server/src/controllers/sensorDataController.ts

@@ -0,0 +1,188 @@
+import { Request, Response } from 'express';
+import { SensorDataModel } from '../models/sensorData';
+import { LoggerService } from '../services/loggerService';
+
+export class SensorDataController {
+  static async getAllSensorData(req: Request, res: Response): Promise<void> {
+    try {
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+
+      const data = await SensorDataModel.getAll(limit, offset);
+      const total = await SensorDataModel.getCount();
+
+      res.status(200).json({
+        success: true,
+        data,
+        pagination: {
+          page,
+          limit,
+          total,
+          totalPages: Math.ceil(total / limit)
+        }
+      });
+
+      LoggerService.info('获取传感器数据成功', {
+        source: 'sensor_data',
+        module: 'get_all',
+        details: JSON.stringify({ page, limit, total })
+      });
+    } catch (error) {
+      console.error('获取传感器数据失败:', error);
+      res.status(500).json({
+        success: false,
+        error: error instanceof Error ? error.message : '获取传感器数据失败'
+      });
+
+      LoggerService.error('获取传感器数据失败', {
+        source: 'sensor_data',
+        module: 'get_all',
+        details: JSON.stringify({ error: error instanceof Error ? error.message : '未知错误' })
+      });
+    }
+  }
+
+  static async getSensorDataByDevice(req: Request, res: Response): Promise<void> {
+    try {
+      const { deviceId } = req.params;
+      const limit = Number(req.query.limit) || 100;
+
+      const deviceIdStr = Array.isArray(deviceId) ? deviceId[0] : deviceId;
+      const data = await SensorDataModel.getByDeviceId(deviceIdStr, limit);
+      const total = await SensorDataModel.getCountByDeviceId(deviceIdStr);
+
+      res.status(200).json({
+        success: true,
+        data,
+        total
+      });
+
+      LoggerService.info('获取设备传感器数据成功', {
+        source: 'sensor_data',
+        module: 'get_by_device',
+        details: JSON.stringify({ deviceId: deviceIdStr, limit, total })
+      });
+    } catch (error) {
+      console.error('获取设备传感器数据失败:', error);
+      res.status(500).json({
+        success: false,
+        error: error instanceof Error ? error.message : '获取设备传感器数据失败'
+      });
+
+      LoggerService.error('获取设备传感器数据失败', {
+        source: 'sensor_data',
+        module: 'get_by_device',
+        details: JSON.stringify({ error: error instanceof Error ? error.message : '未知错误' })
+      });
+    }
+  }
+
+  static async getSensorDataByType(req: Request, res: Response): Promise<void> {
+    try {
+      const { dataType } = req.params;
+      const limit = Number(req.query.limit) || 100;
+
+      const dataTypeStr = Array.isArray(dataType) ? dataType[0] : dataType;
+      const data = await SensorDataModel.getByType(dataTypeStr, limit);
+      const total = await SensorDataModel.getCountByType(dataTypeStr);
+
+      res.status(200).json({
+        success: true,
+        data,
+        total
+      });
+
+      LoggerService.info('获取类型传感器数据成功', {
+        source: 'sensor_data',
+        module: 'get_by_type',
+        details: JSON.stringify({ dataType: dataTypeStr, limit, total })
+      });
+    } catch (error) {
+      console.error('获取类型传感器数据失败:', error);
+      res.status(500).json({
+        success: false,
+        error: error instanceof Error ? error.message : '获取类型传感器数据失败'
+      });
+
+      LoggerService.error('获取类型传感器数据失败', {
+        source: 'sensor_data',
+        module: 'get_by_type',
+        details: JSON.stringify({ error: error instanceof Error ? error.message : '未知错误' })
+      });
+    }
+  }
+
+  static async getSensorDataByDeviceAndType(req: Request, res: Response): Promise<void> {
+    try {
+      const { deviceId, dataType } = req.params;
+      const limit = Number(req.query.limit) || 100;
+      const hours = Number(req.query.hours) || 24;
+
+      const deviceIdStr = Array.isArray(deviceId) ? deviceId[0] : deviceId;
+      const dataTypeStr = Array.isArray(dataType) ? dataType[0] : dataType;
+
+      let data;
+      if (hours > 0) {
+        data = await SensorDataModel.getByTimeRange(deviceIdStr, dataTypeStr, hours);
+      } else {
+        data = await SensorDataModel.getByDeviceIdAndType(deviceIdStr, dataTypeStr, limit);
+      }
+
+      res.status(200).json({
+        success: true,
+        data
+      });
+
+      LoggerService.info('获取设备类型传感器数据成功', {
+        source: 'sensor_data',
+        module: 'get_by_device_and_type',
+        details: JSON.stringify({ deviceId: deviceIdStr, dataType: dataTypeStr, limit, hours })
+      });
+    } catch (error) {
+      console.error('获取设备类型传感器数据失败:', error);
+      res.status(500).json({
+        success: false,
+        error: error instanceof Error ? error.message : '获取设备类型传感器数据失败'
+      });
+
+      LoggerService.error('获取设备类型传感器数据失败', {
+        source: 'sensor_data',
+        module: 'get_by_device_and_type',
+        details: JSON.stringify({ error: error instanceof Error ? error.message : '未知错误' })
+      });
+    }
+  }
+
+  static async getLatestSensorData(req: Request, res: Response): Promise<void> {
+    try {
+      const { deviceId } = req.params;
+
+      const deviceIdStr = Array.isArray(deviceId) ? deviceId[0] : deviceId;
+      const data = await SensorDataModel.getLatestByDevice(deviceIdStr);
+
+      res.status(200).json({
+        success: true,
+        data
+      });
+
+      LoggerService.info('获取最新传感器数据成功', {
+        source: 'sensor_data',
+        module: 'get_latest',
+        details: JSON.stringify({ deviceId: deviceIdStr })
+      });
+    } catch (error) {
+      console.error('获取最新传感器数据失败:', error);
+      res.status(500).json({
+        success: false,
+        error: error instanceof Error ? error.message : '获取最新传感器数据失败'
+      });
+
+      LoggerService.error('获取最新传感器数据失败', {
+        source: 'sensor_data',
+        module: 'get_latest',
+        details: JSON.stringify({ error: error instanceof Error ? error.message : '未知错误' })
+      });
+    }
+  }
+}

+ 474 - 0
mqtt-vue-dashboard/server/src/controllers/systemLogController.ts

@@ -0,0 +1,474 @@
+import { Request, Response } from 'express';
+import { SystemLogModel } from '../models/systemLog';
+import { toString } from '../utils/helpers';
+
+export class SystemLogController {
+  // 获取所有系统日志
+  static async getAllSystemLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const page = Number(toString(req.query.page)) || 1;
+      const limit = Number(toString(req.query.limit)) || 20;
+      const offset = (page - 1) * limit;
+      
+      const systemLogs = await SystemLogModel.getAll(limit, offset);
+      const total = await SystemLogModel.getCount();
+      
+      res.status(200).json({
+        success: true,
+        data: systemLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('获取系统日志列表失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取系统日志列表失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据ID获取系统日志
+  static async getSystemLogById(req: Request, res: Response): Promise<void> {
+    try {
+      const id = toString(req.params.id);
+      
+      if (!id || isNaN(Number(id))) {
+        res.status(400).json({
+          success: false,
+          message: '无效的ID'
+        });
+        return;
+      }
+      
+      const systemLog = await SystemLogModel.getById(Number(id));
+      
+      if (!systemLog) {
+        res.status(404).json({
+          success: false,
+          message: '系统日志不存在'
+        });
+        return;
+      }
+      
+      res.status(200).json({
+        success: true,
+        data: systemLog
+      });
+    } catch (error) {
+      console.error('获取系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据日志级别获取系统日志
+  static async getSystemLogsByLevel(req: Request, res: Response): Promise<void> {
+    try {
+      const level = toString(req.params.level);
+      const page = Number(toString(req.query.page)) || 1;
+      const limit = Number(toString(req.query.limit)) || 20;
+      const offset = (page - 1) * limit;
+      
+      if (!level) {
+        res.status(400).json({
+          success: false,
+          message: '日志级别不能为空'
+        });
+        return;
+      }
+      
+      // 验证日志级别
+      if (!['info', 'warn', 'error', 'debug'].includes(level)) {
+        res.status(400).json({
+          success: false,
+          message: '日志级别必须是info、warn、error或debug之一'
+        });
+        return;
+      }
+      
+      const systemLogs = await SystemLogModel.getByLevel(level, limit, offset);
+      const total = await SystemLogModel.getCountByLevel(level);
+      
+      res.status(200).json({
+        success: true,
+        data: systemLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据日志级别获取系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据日志级别获取系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据来源获取系统日志
+  static async getSystemLogsBySource(req: Request, res: Response): Promise<void> {
+    try {
+      const source = toString(req.params.source);
+      const page = Number(toString(req.query.page)) || 1;
+      const limit = Number(toString(req.query.limit)) || 20;
+      const offset = (page - 1) * limit;
+      
+      if (!source) {
+        res.status(400).json({
+          success: false,
+          message: '来源不能为空'
+        });
+        return;
+      }
+      
+      const systemLogs = await SystemLogModel.getBySource(source, limit, offset);
+      const total = await SystemLogModel.getCountBySource(source);
+      
+      res.status(200).json({
+        success: true,
+        data: systemLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据来源获取系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据来源获取系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据模块获取系统日志
+  static async getSystemLogsByModule(req: Request, res: Response): Promise<void> {
+    try {
+      const module = toString(req.params.module);
+      const page = Number(toString(req.query.page)) || 1;
+      const limit = Number(toString(req.query.limit)) || 20;
+      const offset = (page - 1) * limit;
+      
+      if (!module) {
+        res.status(400).json({
+          success: false,
+          message: '模块不能为空'
+        });
+        return;
+      }
+      
+      const systemLogs = await SystemLogModel.getByModule(module, limit, offset);
+      const total = await SystemLogModel.getCountByModule ? await SystemLogModel.getCountByModule(module) : 0;
+      
+      res.status(200).json({
+        success: true,
+        data: systemLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据模块获取系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据模块获取系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据时间范围获取系统日志
+  static async getSystemLogsByTimeRange(req: Request, res: Response): Promise<void> {
+    try {
+      const { start_time, end_time } = req.query;
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      if (!start_time || !end_time) {
+        res.status(400).json({
+          success: false,
+          message: '开始时间和结束时间不能为空'
+        });
+        return;
+      }
+      
+      const startDate = new Date(start_time as string);
+      const endDate = new Date(end_time as string);
+      
+      if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
+        res.status(400).json({
+          success: false,
+          message: '无效的时间格式'
+        });
+        return;
+      }
+      
+      if (startDate >= endDate) {
+        res.status(400).json({
+          success: false,
+          message: '开始时间必须早于结束时间'
+        });
+        return;
+      }
+      
+      const systemLogs = await SystemLogModel.getByTimeRange(startDate, endDate, limit, offset);
+      const total = await SystemLogModel.getCountByTimeRange(startDate, endDate);
+      
+      res.status(200).json({
+        success: true,
+        data: systemLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据时间范围获取系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据时间范围获取系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 根据多条件查询系统日志
+  static async getSystemLogsByMultipleConditions(req: Request, res: Response): Promise<void> {
+    try {
+      const { level, source, module, message, start_time, end_time } = req.query;
+      const page = Number(req.query.page) || 1;
+      const limit = Number(req.query.limit) || 20;
+      const offset = (page - 1) * limit;
+      
+      // 构建查询条件
+      const conditions: any = {};
+      
+      if (level !== undefined && level !== '') {
+        if (!['info', 'warn', 'error', 'debug'].includes(level as string)) {
+          res.status(400).json({
+            success: false,
+            message: '日志级别必须是info、warn、error或debug之一'
+          });
+          return;
+        }
+        conditions.level = level as string;
+      }
+      if (source !== undefined && source !== '') conditions.source = source as string;
+      if (module !== undefined && module !== '') conditions.module = module as string;
+      if (message !== undefined && message !== '') conditions.message = message as string;
+      
+      let startDate, endDate;
+      if (start_time && end_time) {
+        startDate = new Date(start_time as string);
+        endDate = new Date(end_time as string);
+        
+        if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
+          res.status(400).json({
+            success: false,
+            message: '无效的时间格式'
+          });
+          return;
+        }
+        
+        if (startDate >= endDate) {
+          res.status(400).json({
+            success: false,
+            message: '开始时间必须早于结束时间'
+          });
+          return;
+        }
+      }
+      
+      // 指定需要模糊查询的字段
+      const fuzzyFields = ['message', 'source', 'module'];
+      
+      const systemLogs = await SystemLogModel.getByMultipleConditions(
+        conditions, 
+        startDate, 
+        endDate, 
+        limit, 
+        offset,
+        fuzzyFields
+      );
+      const total = await SystemLogModel.getCountByMultipleConditions(
+        conditions, 
+        startDate, 
+        endDate,
+        fuzzyFields
+      );
+      
+      res.status(200).json({
+        success: true,
+        data: systemLogs,
+        pagination: {
+          page,
+          limit,
+          total,
+          pages: Math.ceil(total / limit)
+        }
+      });
+    } catch (error) {
+      console.error('根据多条件查询系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '根据多条件查询系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取系统日志统计信息
+  static async getSystemLogStats(req: Request, res: Response): Promise<void> {
+    try {
+      const stats = await SystemLogModel.getFullStats();
+      
+      res.status(200).json({
+        success: true,
+        data: stats,
+        message: '获取系统日志统计信息成功'
+      });
+    } catch (error) {
+      console.error('获取系统日志统计信息失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取系统日志统计信息失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 获取最近系统日志
+  static async getRecentSystemLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const limit = Number(req.query.limit) || 10;
+      
+      if (limit > 100) {
+        res.status(400).json({
+          success: false,
+          message: '限制数量不能超过100'
+        });
+        return;
+      }
+      
+      const systemLogs = await SystemLogModel.getRecent(limit);
+      
+      res.status(200).json({
+        success: true,
+        data: systemLogs,
+        message: '获取最近系统日志成功'
+      });
+    } catch (error) {
+      console.error('获取最近系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '获取最近系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 创建系统日志
+  static async createSystemLog(req: Request, res: Response): Promise<void> {
+    try {
+      const { level, message, source, module, user_id, username, ip_address, details } = req.body;
+      
+      if (!level || !message || !source) {
+        res.status(400).json({
+          success: false,
+          message: '日志级别、消息和来源不能为空'
+        });
+        return;
+      }
+      
+      // 验证日志级别
+      if (!['info', 'warn', 'error', 'debug'].includes(level)) {
+        res.status(400).json({
+          success: false,
+          message: '日志级别必须是info、warn、error或debug之一'
+        });
+        return;
+      }
+      
+      const systemLog = await SystemLogModel.create({
+        level,
+        message,
+        source,
+        module,
+        user_id,
+        username,
+        ip_address,
+        details
+      });
+      
+      res.status(201).json({
+        success: true,
+        data: systemLog,
+        message: '创建系统日志成功'
+      });
+    } catch (error) {
+      console.error('创建系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '创建系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+
+  // 清理旧系统日志
+  static async cleanupOldSystemLogs(req: Request, res: Response): Promise<void> {
+    try {
+      const { days } = req.query;
+      let daysToDelete = days;
+      
+      if (!daysToDelete && req.body && req.body.days) {
+        daysToDelete = req.body.days;
+      }
+      
+      if (!daysToDelete || isNaN(Number(daysToDelete)) || Number(daysToDelete) < 1) {
+        res.status(400).json({
+          success: false,
+          message: '请提供有效的天数(大于0的数字)'
+        });
+        return;
+      }
+      
+      const deletedCount = await SystemLogModel.cleanup(Number(daysToDelete));
+      
+      res.status(200).json({
+        success: true,
+        data: { deletedCount },
+        message: `成功清理${deletedCount}条${daysToDelete}天前的系统日志`
+      });
+    } catch (error) {
+      console.error('清理旧系统日志失败:', error);
+      res.status(500).json({
+        success: false,
+        message: '清理旧系统日志失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      });
+    }
+  }
+}

+ 126 - 0
mqtt-vue-dashboard/server/src/database/connection.ts

@@ -0,0 +1,126 @@
+import mysql from 'mysql2/promise';
+import { LoggerService } from '../services/loggerService';
+
+let pool: mysql.Pool | null = null;
+
+const getDbConfig = () => ({
+  host: process.env.DB_HOST || '192.168.1.17',
+  port: parseInt(process.env.DB_PORT || '3306'),
+  user: process.env.DB_USER || 'root',
+  password: process.env.DB_PASSWORD || '123',
+  database: process.env.DB_NAME || 'mqtt_vue_dashboard',
+  charset: 'utf8mb4',
+  waitForConnections: true,
+  connectionLimit: 10,
+  queueLimit: 0
+});
+
+const getPool = (): mysql.Pool => {
+  if (!pool) {
+    const dbConfig = getDbConfig();
+    console.log('创建数据库连接池:', {
+      host: dbConfig.host,
+      port: dbConfig.port,
+      user: dbConfig.user,
+      database: dbConfig.database,
+      env_DB_HOST: process.env.DB_HOST,
+      env_DB_PORT: process.env.DB_PORT,
+      env_DB_USER: process.env.DB_USER,
+      env_DB_NAME: process.env.DB_NAME
+    });
+    pool = mysql.createPool(dbConfig);
+  }
+  return pool;
+};
+
+export const executeQuery = async (query: string, params?: any[]): Promise<any> => {
+  try {
+    const [rows] = await getPool().execute(query, params);
+    
+    LoggerService.info('数据库查询成功', {
+      source: 'database',
+      module: 'execute_query',
+      details: JSON.stringify({
+        query: query.substring(0, 200),
+        params: params ? JSON.stringify(params).substring(0, 100) : undefined,
+        resultCount: Array.isArray(rows) ? rows.length : 1
+      })
+    }).catch(err => {
+      console.error('数据库查询成功日志写入失败:', err);
+    });
+    
+    return rows;
+  } catch (error) {
+    console.error('Database query error:', error);
+    
+    LoggerService.error('数据库查询失败', {
+      source: 'database',
+      module: 'execute_query',
+      details: JSON.stringify({
+        query: query.substring(0, 200),
+        params: params ? JSON.stringify(params).substring(0, 100) : undefined,
+        error: error instanceof Error ? error.message : '未知错误'
+      })
+    }).catch(err => {
+      console.error('数据库查询失败日志写入失败:', err);
+    });
+    
+    throw error;
+  }
+};
+
+export const testConnection = async (): Promise<boolean> => {
+  try {
+    const connection = await getPool().getConnection();
+    await connection.ping();
+    connection.release();
+    console.log('Database connection successful');
+    
+    LoggerService.info('数据库连接成功', {
+      source: 'database',
+      module: 'test_connection',
+      details: JSON.stringify({
+        host: getDbConfig().host,
+        port: getDbConfig().port,
+        database: getDbConfig().database,
+        user: getDbConfig().user
+      })
+    }).catch(err => {
+      console.error('数据库连接成功日志写入失败:', err);
+    });
+    
+    return true;
+  } catch (error) {
+    console.error('Database connection failed:', error);
+    
+    LoggerService.error('数据库连接失败', {
+      source: 'database',
+      module: 'test_connection',
+      details: JSON.stringify({
+        host: getDbConfig().host,
+        port: getDbConfig().port,
+        database: getDbConfig().database,
+        user: getDbConfig().user,
+        error: error instanceof Error ? error.message : '未知错误'
+      })
+    }).catch(err => {
+      console.error('数据库连接失败日志写入失败:', err);
+    });
+    
+    return false;
+  }
+};
+
+export const closePool = async (): Promise<void> => {
+  try {
+    if (pool) {
+      await pool.end();
+      pool = null;
+      console.log('Database connection pool closed');
+    }
+  } catch (error) {
+    console.error('Error closing database connection pool:', error);
+  }
+};
+
+export default getPool;

+ 23 - 0
mqtt-vue-dashboard/server/src/database/schema.sql

@@ -0,0 +1,23 @@
+-- 用户表
+CREATE TABLE IF NOT EXISTS users (
+  id VARCHAR(36) PRIMARY KEY,
+  username VARCHAR(50) UNIQUE NOT NULL,
+  password VARCHAR(255) NOT NULL,
+  email VARCHAR(100),
+  role ENUM('admin', 'user', 'viewer') NOT NULL DEFAULT 'user',
+  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  INDEX idx_username (username),
+  INDEX idx_role (role)
+);
+
+-- 插入默认管理员用户 (密码: admin123)
+-- 注意: 生产环境中应该修改默认密码
+INSERT INTO users (id, username, password, email, role) 
+VALUES (
+  UUID(), 
+  'admin', 
+  '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 
+  'admin@example.com', 
+  'admin'
+) ON DUPLICATE KEY UPDATE username = username;

+ 149 - 0
mqtt-vue-dashboard/server/src/index.ts

@@ -0,0 +1,149 @@
+import express from 'express';
+import cors from 'cors';
+import helmet from 'helmet';
+import dotenv from 'dotenv';
+import { createServer } from 'http';
+import path from 'path';
+import fs from 'fs';
+import { testConnection } from './config/database';
+import routes from './routes';
+import { errorHandler } from './middleware/errorHandler';
+import WebSocketService, { setWebSocketService } from './services/websocketService';
+import { FirmwareFileModel } from './models/firmware';
+import { OTATaskModel } from './models/ota';
+import { MqttBrokerService } from './services/mqttBrokerService';
+import { LoggerService } from './services/loggerService';
+import { requestLogger } from './middleware/requestLogger';
+
+dotenv.config({ override: false });
+
+const envPath = path.resolve(process.cwd(), '.env.production').trim();
+console.log('加载生产环境文件:', envPath);
+dotenv.config({ path: envPath, override: true });
+
+process.env.NODE_ENV = process.env.NODE_ENV || 'production';
+
+const app = express();
+const server = createServer(app);
+
+app.use(helmet());
+app.use(cors({
+  origin: true,
+  credentials: true,
+  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
+  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-Forwarded-For', 'X-Real-IP']
+}));
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+app.use(requestLogger);
+
+app.use('/api', routes);
+
+// 托管前端静态文件
+const frontendPath = path.join(__dirname, '../../dist');
+if (fs.existsSync(frontendPath)) {
+  app.use(express.static(frontendPath));
+  
+  // 处理前端路由(SPA应用)
+  app.get('*', (req, res) => {
+    res.sendFile(path.join(frontendPath, 'index.html'));
+  });
+  console.log('前端静态文件服务已启用');
+}
+
+app.get('/health', (req, res) => {
+  res.status(200).json({
+    status: 'ok',
+    message: 'MQTT Dashboard API is running',
+    timestamp: new Date().toISOString()
+  });
+});
+
+app.use(errorHandler);
+
+const PORT = parseInt(process.env.PORT || '3002');
+
+async function startServer() {
+  try {
+    await LoggerService.info('MQTT Dashboard API服务器开始启动', {
+      source: 'system',
+      module: 'init',
+      details: JSON.stringify({ port: PORT, environment: process.env.NODE_ENV })
+    });
+
+    await testConnection();
+    console.log('数据库连接成功');
+
+    await LoggerService.info('数据库连接成功', {
+      source: 'system',
+      module: 'database'
+    });
+
+    await FirmwareFileModel.createTable();
+    console.log('固件文件表创建成功');
+
+    await OTATaskModel.createTable();
+    console.log('OTA任务表创建成功');
+
+    const webSocketService = new WebSocketService(server);
+    setWebSocketService(webSocketService);
+    console.log('WebSocket服务已初始化');
+
+    webSocketService.startRealDataUpdates();
+    console.log('实时数据更新服务已启动');
+
+    const mqttBroker = MqttBrokerService.getInstance();
+    await mqttBroker.start();
+    console.log('MQTT Broker服务已启动');
+
+    await LoggerService.info('MQTT Broker服务已启动', {
+      source: 'system',
+      module: 'mqtt_broker',
+      details: JSON.stringify({ port: process.env.MQTT_BROKER_PORT || 1883 })
+    });
+
+    server.listen(PORT, '0.0.0.0', () => {
+      console.log(`服务器运行在端口 ${PORT}`);
+      console.log(`API文档地址: http://localhost:${PORT}/api`);
+      console.log(`WebSocket服务地址: ws://localhost:${PORT}`);
+      console.log(`MQTT Broker地址: mqtt://localhost:${process.env.MQTT_BROKER_PORT || 1883}`);
+
+      LoggerService.info('服务器启动成功', {
+        source: 'system',
+        module: 'http',
+        details: JSON.stringify({
+          port: PORT,
+          apiUrl: `http://localhost:${PORT}/api`,
+          wsUrl: `ws://localhost:${PORT}`,
+          mqttBroker: `mqtt://localhost:${process.env.MQTT_BROKER_PORT || 1883}`
+        })
+      }).catch(() => {});
+    });
+  } catch (error) {
+    console.error('启动服务器失败:', error);
+    await LoggerService.error('启动服务器失败', {
+      source: 'system',
+      module: 'init',
+      details: JSON.stringify({ error: error instanceof Error ? error.message : '未知错误' })
+    });
+    process.exit(1);
+  }
+}
+
+process.on('SIGINT', async () => {
+  console.log('正在关闭服务器...');
+  const mqttBroker = MqttBrokerService.getInstance();
+  await mqttBroker.stop();
+  process.exit(0);
+});
+
+process.on('SIGTERM', async () => {
+  console.log('正在关闭服务器...');
+  const mqttBroker = MqttBrokerService.getInstance();
+  await mqttBroker.stop();
+  process.exit(0);
+});
+
+startServer();
+
+export { app, server };

+ 240 - 0
mqtt-vue-dashboard/server/src/middleware/auth.ts

@@ -0,0 +1,240 @@
+import { Request, Response, NextFunction } from 'express';
+import jwt from 'jsonwebtoken';
+import { AppError } from './errorHandler';
+import { LoggerService } from '../services/loggerService';
+
+// 扩展Request接口,添加user属性
+declare global {
+  namespace Express {
+    interface Request {
+      user?: { id: string; username: string; role?: string };
+    }
+  }
+}
+
+/**
+ * 认证中间件
+ * 验证请求头中的Authorization令牌
+ */
+export const authenticateToken = (req: Request, res: Response, next: NextFunction): void => {
+  try {
+    // 从请求头获取令牌
+    const authHeader = req.headers['authorization'];
+    const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
+
+    if (!token) {
+      // 记录认证失败日志(缺少令牌)
+      LoggerService.warn('认证失败:未提供认证令牌', {
+        source: 'auth',
+        module: 'authenticate_token',
+        details: JSON.stringify({
+          path: req.path,
+          method: req.method,
+          ip: req.ip,
+          userAgent: req.get('user-agent')
+        })
+      }).catch(err => {
+        console.error('认证失败日志写入失败:', err);
+      });
+      
+      throw new AppError('未提供认证令牌', 401);
+    }
+
+    // 检查是否为模拟token(以mock_token_开头)
+    if (token.startsWith('mock_token_')) {
+      // 解析模拟token
+      const parts = token.split('_');
+      if (parts.length < 3) {
+        // 记录认证失败日志(无效的模拟令牌)
+        LoggerService.warn('认证失败:无效的认证令牌', {
+          source: 'auth',
+          module: 'authenticate_token',
+          details: JSON.stringify({
+            tokenType: 'mock',
+            path: req.path,
+            method: req.method,
+            ip: req.ip
+          })
+        }).catch(err => {
+          console.error('认证失败日志写入失败:', err);
+        });
+        
+        throw new AppError('无效的认证令牌', 401);
+      }
+      
+      const userId = parts[2];
+      
+      // 从数据库获取用户信息
+      const UserModel = require('../models/user').UserModel;
+      UserModel.getById(userId).then((user: any) => {
+        if (!user) {
+          // 记录认证失败日志(用户不存在)
+          LoggerService.warn('认证失败:用户不存在', {
+            source: 'auth',
+            module: 'authenticate_token',
+            details: JSON.stringify({
+              userId,
+              tokenType: 'mock',
+              path: req.path,
+              method: req.method,
+              ip: req.ip
+            })
+          }).catch(err => {
+            console.error('认证失败日志写入失败:', err);
+          });
+          
+          throw new AppError('用户不存在', 401);
+        }
+        
+        // 将用户信息添加到请求对象
+        req.user = {
+          id: user.id,
+          username: user.username,
+          role: user.role
+        };
+        
+        // 记录认证成功日志
+        LoggerService.info('认证成功', {
+          source: 'auth',
+          module: 'authenticate_token',
+          details: JSON.stringify({
+            userId: user.id,
+            username: user.username,
+            role: user.role,
+            tokenType: 'mock',
+            path: req.path,
+            method: req.method,
+            ip: req.ip
+          })
+        }).catch(err => {
+          console.error('认证成功日志写入失败:', err);
+        });
+        
+        next();
+      }).catch((error: any) => {
+        next(error);
+      });
+    } else {
+      // 验证JWT令牌
+      const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as {
+        id: string;
+        username: string;
+        role?: string;
+      };
+
+      // 将解码后的用户信息添加到请求对象
+      req.user = decoded;
+      
+      // 记录认证成功日志
+      LoggerService.info('认证成功', {
+        source: 'auth',
+        module: 'authenticate_token',
+        details: JSON.stringify({
+          userId: decoded.id,
+          username: decoded.username,
+          role: decoded.role,
+          tokenType: 'jwt',
+          path: req.path,
+          method: req.method,
+          ip: req.ip
+        })
+      }).catch(err => {
+        console.error('认证成功日志写入失败:', err);
+      });
+      
+      next();
+    }
+  } catch (error) {
+    if (error instanceof jwt.JsonWebTokenError) {
+      LoggerService.warn('认证失败:无效的认证令牌', {
+        source: 'auth',
+        module: 'authenticate_token',
+        details: JSON.stringify({
+          tokenType: 'jwt',
+          error: (error as jwt.JsonWebTokenError).message,
+          path: req.path,
+          method: req.method,
+          ip: req.ip
+        })
+      }).catch((err: unknown) => {
+        console.error('认证失败日志写入失败:', err);
+      });
+      
+      next(new AppError('无效的认证令牌', 401));
+    } else if (error instanceof jwt.TokenExpiredError) {
+      LoggerService.warn('认证失败:认证令牌已过期', {
+        source: 'auth',
+        module: 'authenticate_token',
+        details: JSON.stringify({
+          tokenType: 'jwt',
+          expiredAt: (error as jwt.TokenExpiredError).expiredAt,
+          path: req.path,
+          method: req.method,
+          ip: req.ip
+        })
+      }).catch((err: unknown) => {
+        console.error('认证失败日志写入失败:', err);
+      });
+      
+      next(new AppError('认证令牌已过期', 401));
+    } else if (error instanceof Error) {
+      next(error);
+    } else {
+      next(new AppError('认证失败', 401));
+    }
+  }
+};
+
+/**
+ * 角色授权中间件
+ * 检查用户是否具有指定角色
+ */
+export const authorizeRole = (roles: string[]) => {
+  return (req: Request, res: Response, next: NextFunction): void => {
+    if (!req.user || !req.user.role || !roles.includes(req.user.role)) {
+      // 记录授权失败日志
+      LoggerService.warn('授权失败:没有权限执行此操作', {
+        source: 'auth',
+        module: 'authorize_role',
+        details: JSON.stringify({
+          userRole: req.user?.role,
+          requiredRoles: roles,
+          path: req.path,
+          method: req.method,
+          userId: req.user?.id,
+          username: req.user?.username,
+          ip: req.ip
+        })
+      }).catch(err => {
+        console.error('授权失败日志写入失败:', err);
+      });
+      
+      throw new AppError('没有权限执行此操作', 403);
+    }
+    
+    // 记录授权成功日志
+    LoggerService.info('授权成功', {
+      source: 'auth',
+      module: 'authorize_role',
+      details: JSON.stringify({
+        userRole: req.user.role,
+        requiredRoles: roles,
+        path: req.path,
+        method: req.method,
+        userId: req.user.id,
+        username: req.user.username,
+        ip: req.ip
+      })
+    }).catch(err => {
+      console.error('授权成功日志写入失败:', err);
+    });
+    
+    next();
+  };
+};
+
+/**
+ * 管理员权限中间件
+ * 检查用户是否具有admin角色
+ */
+export const requireAdmin = authorizeRole(['admin']);

+ 131 - 0
mqtt-vue-dashboard/server/src/middleware/errorHandler.ts

@@ -0,0 +1,131 @@
+import { Request, Response, NextFunction } from 'express';
+import { LoggerService } from '../services/loggerService';
+
+// 自定义错误类
+export class AppError extends Error {
+  public statusCode: number;
+  public isOperational: boolean;
+
+  constructor(message: string, statusCode: number) {
+    super(message);
+    this.statusCode = statusCode;
+    this.isOperational = true;
+
+    Error.captureStackTrace(this, this.constructor);
+  }
+}
+
+// 处理开发环境错误
+const sendErrorDev = (err: AppError, res: Response): void => {
+  res.status(err.statusCode).json({
+    status: 'error',
+    error: err,
+    message: err.message,
+    stack: err.stack
+  });
+};
+
+// 处理生产环境错误
+const sendErrorProd = (err: AppError, res: Response): void => {
+  // 操作性错误,可信任的错误:发送消息给客户端
+  if (err.isOperational) {
+    res.status(err.statusCode).json({
+      status: 'error',
+      message: err.message
+    });
+  } else {
+    // 编程或其他未知错误:不泄露错误详情
+    console.error('ERROR 💥', err);
+    res.status(500).json({
+      status: 'error',
+      message: '服务器内部错误'
+    });
+  }
+};
+
+// 处理数据库错误
+const handleDBError = (err: any): AppError => {
+  if (err.code === 'ER_NO_SUCH_TABLE') {
+    return new AppError('数据表不存在,请检查数据库设置', 500);
+  }
+  
+  if (err.code === 'ER_ACCESS_DENIED_ERROR') {
+    return new AppError('数据库访问被拒绝,请检查权限设置', 500);
+  }
+  
+  if (err.code === 'ECONNREFUSED') {
+    return new AppError('无法连接到数据库,请检查数据库服务器状态', 500);
+  }
+  
+  if (err.code === 'ER_BAD_FIELD_ERROR') {
+    return new AppError('数据库字段错误', 400);
+  }
+  
+  return new AppError('数据库操作失败', 500);
+};
+
+// 全局错误处理中间件
+export const errorHandler = (
+  err: any,
+  req: Request,
+  res: Response,
+  next: NextFunction
+): void => {
+  err.statusCode = err.statusCode || 500;
+  err.status = err.status || 'error';
+
+  // 记录错误日志
+  LoggerService.error('发生错误', {
+    source: 'error_handler',
+    module: 'global',
+    details: JSON.stringify({
+      statusCode: err.statusCode,
+      message: err.message,
+      path: req.path,
+      method: req.method,
+      ip: req.ip,
+      userAgent: req.get('user-agent')
+    })
+  }).catch(logErr => {
+    console.error('错误日志写入失败:', logErr);
+  });
+
+  // 数据库错误处理
+  if (err.code && typeof err.code === 'string' && err.code.startsWith('ER_')) {
+    err = handleDBError(err);
+  }
+
+  // 根据环境决定错误响应方式
+  if (process.env.NODE_ENV === 'development') {
+    sendErrorDev(err, res);
+  } else {
+    sendErrorProd(err, res);
+  }
+};
+
+// 处理未捕获的路由
+export const notFound = (req: Request, res: Response, next: NextFunction): void => {
+  const err = new AppError(`找不到路由 ${req.originalUrl}`, 404);
+  
+  // 记录404错误日志
+  LoggerService.warn('未找到路由', {
+    source: 'error_handler',
+    module: 'not_found',
+    details: JSON.stringify({
+      path: req.originalUrl,
+      method: req.method,
+      ip: req.ip
+    })
+  }).catch(logErr => {
+    console.error('404错误日志写入失败:', logErr);
+  });
+  
+  next(err);
+};
+
+// 异步错误捕获包装器
+export const catchAsync = (fn: Function) => {
+  return (req: Request, res: Response, next: NextFunction) => {
+    fn(req, res, next).catch(next);
+  };
+};

+ 91 - 0
mqtt-vue-dashboard/server/src/middleware/requestLogger.ts

@@ -0,0 +1,91 @@
+import { Request, Response, NextFunction } from 'express';
+import { LoggerService } from '../services/loggerService';
+
+// 请求日志中间件
+export const requestLogger = (req: Request, res: Response, next: NextFunction): void => {
+  const start = Date.now();
+  
+  // 记录请求开始
+  console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} - ${req.ip}`);
+  
+  // 使用日志服务记录请求日志
+  LoggerService.info(`${req.method} ${req.originalUrl}`, {
+    source: 'http',
+    module: 'request',
+    ip_address: req.ip,
+    details: JSON.stringify({
+      method: req.method,
+      url: req.originalUrl,
+      headers: req.headers,
+      body: req.body
+    })
+  }).catch(err => {
+    console.error('请求日志写入失败:', err);
+  });
+  
+  // 记录请求完成时间
+  res.on('finish', () => {
+    const duration = Date.now() - start;
+    const { method, originalUrl, ip } = req;
+    const { statusCode } = res;
+    
+    console.log(
+      `[${new Date().toISOString()}] ${method} ${originalUrl} - ${statusCode} - ${duration}ms`
+    );
+    
+    const logMessage = `${req.method} ${req.originalUrl} - ${statusCode} - ${duration}ms`;
+    const logOptions = {
+      source: 'http' as const,
+      module: 'request' as const,
+      ip_address: req.ip,
+      details: JSON.stringify({
+        method: req.method,
+        url: req.originalUrl,
+        statusCode,
+        duration: `${duration}ms`,
+        headers: req.headers,
+        body: req.body
+      })
+    };
+    
+    let logPromise;
+    if (statusCode >= 500) {
+      logPromise = LoggerService.error(logMessage, logOptions);
+    } else if (statusCode >= 400) {
+      logPromise = LoggerService.warn(logMessage, logOptions);
+    } else {
+      logPromise = LoggerService.info(logMessage, logOptions);
+    }
+    
+    logPromise.catch(err => {
+      console.error('请求完成日志写入失败:', err);
+    });
+  });
+  
+  next();
+};
+
+// 请求体限制中间件
+export const requestLimiter = (req: Request, res: Response, next: NextFunction): void => {
+  // 检查请求体大小
+  if (req.headers['content-length'] && parseInt(req.headers['content-length']) > 10 * 1024 * 1024) {
+    res.status(413).json({
+      success: false,
+      message: '请求体过大,最大允许10MB'
+    });
+    return;
+  }
+  
+  next();
+};
+
+// 安全头中间件
+export const securityHeaders = (req: Request, res: Response, next: NextFunction): void => {
+  // 设置安全相关的HTTP头
+  res.setHeader('X-Content-Type-Options', 'nosniff');
+  res.setHeader('X-Frame-Options', 'DENY');
+  res.setHeader('X-XSS-Protection', '1; mode=block');
+  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
+  
+  next();
+};

+ 55 - 0
mqtt-vue-dashboard/server/src/middleware/uploadMiddleware.ts

@@ -0,0 +1,55 @@
+import multer, { FileFilterCallback } from 'multer';
+import path from 'path';
+import fs from 'fs';
+
+// 定义文件接口
+interface UploadedFile {
+  fieldname: string;
+  originalname: string;
+  encoding: string;
+  mimetype: string;
+  destination: string;
+  filename: string;
+  path: string;
+  size: number;
+}
+
+// 配置multer存储引擎
+const storage = multer.diskStorage({
+  destination: (req: any, file: UploadedFile, cb: any) => {
+    const uploadDir = '/home/yangfei/OTA/temp';
+    // 确保上传目录存在
+    if (!fs.existsSync(uploadDir)) {
+      fs.mkdirSync(uploadDir, { recursive: true });
+    }
+    cb(null, uploadDir);
+  },
+  filename: (req: any, file: UploadedFile, cb: any) => {
+    // 使用时间戳作为文件名前缀,避免文件名冲突
+    const timestamp = Date.now();
+    const originalName = path.basename(file.originalname);
+    const filename = `${timestamp}-${originalName}`;
+    cb(null, filename);
+  }
+});
+
+// 文件类型过滤
+const fileFilter = (req: any, file: UploadedFile, cb: FileFilterCallback) => {
+  // 允许所有二进制文件类型
+  if (file.mimetype.startsWith('application/octet-stream') || file.mimetype === 'application/x-binary' || 
+      file.mimetype.startsWith('application/') || file.mimetype.startsWith('image/') || 
+      file.mimetype.startsWith('text/')) {
+    cb(null, true);
+  } else {
+    cb(new Error('Invalid file type. Only binary files are allowed.'));
+  }
+};
+
+// 创建multer实例
+export const upload = multer({
+  storage,
+  fileFilter,
+  limits: {
+    fileSize: 100 * 1024 * 1024 // 限制文件大小为100MB
+  }
+});

+ 567 - 0
mqtt-vue-dashboard/server/src/models/authLog.ts

@@ -0,0 +1,567 @@
+import { executeQuery } from '../config/database';
+
+export interface AuthLog {
+  id?: number;
+  clientid: string;
+  username: string;
+  ip_address: string;
+  operation_type: 'connect' | 'publish' | 'subscribe' | 'disconnect';
+  result: 'success' | 'failure';
+  reason?: string;
+  topic?: string;
+  created_at?: Date;
+}
+
+export class AuthLogModel {
+  // �瑕����㕑恕霂�𠯫敹?
+  static async getAll(limit?: number, offset?: number): Promise<AuthLog[]> {
+    let query = 'SELECT * FROM auth_log ORDER BY created_at DESC';
+    const params: (string | number)[] = [];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �寞旿ID�瑕�霈方��亙�
+  static async getById(id: number): Promise<AuthLog | null> {
+    const query = 'SELECT * FROM auth_log WHERE id = ?';
+    const logs = await executeQuery(query, [id]);
+    return logs.length > 0 ? logs[0] : null;
+  }
+
+  // �寞旿摰X�蝡涅D�瑕�霈方��亙�
+  static async getByClientid(clientid: string, limit?: number, offset?: number): Promise<AuthLog[]> {
+    let query = 'SELECT * FROM auth_log WHERE clientid = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [clientid];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �寞旿�冽��滩繮�𤥁恕霂�𠯫敹?
+  static async getByUsername(username: string, limit?: number, offset?: number): Promise<AuthLog[]> {
+    let query = 'SELECT * FROM auth_log WHERE username = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [username];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �寞旿�滢�蝐餃��瑕�霈方��亙�
+  static async getByOperationType(operationType: string, limit?: number, offset?: number): Promise<AuthLog[]> {
+    let query = 'SELECT * FROM auth_log WHERE operation_type = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [operationType];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �寞旿蝏𤘪��瑕�霈方��亙�
+  static async getByResult(result: string, limit?: number, offset?: number): Promise<AuthLog[]> {
+    let query = 'SELECT * FROM auth_log WHERE result = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [result];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �寞旿�園𡢿��凒�瑕�霈方��亙�
+  static async getByTimeRange(startTime: Date, endTime: Date, limit?: number, offset?: number): Promise<AuthLog[]> {
+    let query = 'SELECT * FROM auth_log WHERE created_at BETWEEN ? AND ? ORDER BY created_at DESC';
+    const params: any[] = [startTime, endTime];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �瑕�霈方��亙��餅㺭
+  static async getCount(): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM auth_log';
+    const result = await executeQuery(query);
+    return result[0].count;
+  }
+
+  // �瑕��滢�蝐餃�蝏蠘恣
+  static async getOperationTypeStats(startTime?: Date, endTime?: Date): Promise<any[]> {
+    let query = `
+      SELECT 
+        operation_type,
+        COUNT(*) as count
+      FROM auth_log
+    `;
+    
+    const params: any[] = [];
+    
+    if (startTime && endTime) {
+      query += ' WHERE created_at BETWEEN ? AND ?';
+      params.push(startTime, endTime);
+    }
+    
+    query += ' GROUP BY operation_type';
+    
+    return await executeQuery(query, params);
+  }
+
+  // �瑕�蝏𤘪�蝏蠘恣
+  static async getResultStats(startTime?: Date, endTime?: Date): Promise<any[]> {
+    let query = `
+      SELECT 
+        result,
+        COUNT(*) as count
+      FROM auth_log
+    `;
+    
+    const params: any[] = [];
+    
+    if (startTime && endTime) {
+      query += ' WHERE created_at BETWEEN ? AND ?';
+      params.push(startTime, endTime);
+    }
+    
+    query += ' GROUP BY result';
+    
+    return await executeQuery(query, params);
+  }
+
+  // �瑕�瘥𤩺𠯫霈方�蝏蠘恣
+  static async getDailyStats(days: number = 7): Promise<any[]> {
+    const query = `
+      SELECT 
+        DATE(created_at) as date,
+        COUNT(*) as total,
+        SUM(CASE WHEN result = 'success' THEN 1 ELSE 0 END) as success,
+        SUM(CASE WHEN result = 'failure' THEN 1 ELSE 0 END) as failure
+      FROM auth_log
+      WHERE created_at >= DATE_SUB(CURRENT_DATE(), INTERVAL ? DAY)
+      GROUP BY DATE(created_at)
+      ORDER BY date DESC
+    `;
+    
+    return await executeQuery(query, [days]);
+  }
+
+  // �瑕�瘥誩��嗉恕霂��霈?
+  static async getHourlyStats(hours: number = 24): Promise<any[]> {
+    const query = `
+      SELECT 
+        HOUR(created_at) as hour,
+        COUNT(*) as total,
+        SUM(CASE WHEN result = 'success' THEN 1 ELSE 0 END) as success,
+        SUM(CASE WHEN result = 'failure' THEN 1 ELSE 0 END) as failure
+      FROM auth_log
+      WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? HOUR)
+      GROUP BY HOUR(created_at)
+      ORDER BY hour DESC
+    `;
+    
+    return await executeQuery(query, [hours]);
+  }
+
+  // �瑕�瘣餉�摰X�蝡舐�霈?
+  static async getTopClients(limit: number = 10): Promise<any[]> {
+    const query = `
+      SELECT 
+        clientid,
+        COUNT(*) as count
+      FROM auth_log
+      WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+      GROUP BY clientid
+      ORDER BY count DESC
+      LIMIT ?
+    `;
+    
+    return await executeQuery(query, [limit]);
+  }
+
+  // �瑕�瘣餉�IP蝏蠘恣
+  static async getTopIps(limit: number = 10): Promise<any[]> {
+    const query = `
+      SELECT 
+        ip_address,
+        COUNT(*) as count
+      FROM auth_log
+      WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+      GROUP BY ip_address
+      ORDER BY count DESC
+      LIMIT ?
+    `;
+    
+    return await executeQuery(query, [limit]);
+  }
+
+  // �𥕦遣霈方��亙�
+  static async create(authLog: Omit<AuthLog, 'id' | 'created_at'>): Promise<AuthLog> {
+    const query = `
+      INSERT INTO auth_log (clientid, username, ip_address, operation_type, result, reason, topic)
+      VALUES (?, ?, ?, ?, ?, ?, ?)
+    `;
+    const params = [
+      authLog.clientid,
+      authLog.username,
+      authLog.ip_address,
+      authLog.operation_type,
+      authLog.result,
+      authLog.reason || null,
+      authLog.topic || null
+    ];
+    
+    const result = await executeQuery(query, params) as any;
+    return { ...authLog, id: result.insertId, created_at: new Date() };
+  }
+
+  // �𦦵揣霈方��亙�
+  static async search(searchTerm: string, limit?: number, offset?: number): Promise<AuthLog[]> {
+    let query = `
+      SELECT * FROM auth_log 
+      WHERE clientid LIKE ? OR username LIKE ? OR ip_address LIKE ? OR operation_type LIKE ? OR result LIKE ? OR reason LIKE ? OR topic LIKE ?
+      ORDER BY created_at DESC
+    `;
+    const params: (string | number)[] = [
+      `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`, 
+      `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`, 
+      `%${searchTerm}%`
+    ];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �瑕��𦦵揣蝏𤘪��餅㺭
+  static async getSearchCount(searchTerm: string): Promise<number> {
+    const query = `
+      SELECT COUNT(*) as count 
+      FROM auth_log 
+      WHERE clientid LIKE ? OR username LIKE ? OR ip_address LIKE ? OR operation_type LIKE ? OR result LIKE ? OR reason LIKE ? OR topic LIKE ?
+    `;
+    const params = [
+      `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`, 
+      `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`, 
+      `%${searchTerm}%`
+    ];
+    const result = await executeQuery(query, params);
+    return result[0].count;
+  }
+
+  // 皜���抒�霈方��亙�
+  static async cleanup(daysToKeep: number = 30): Promise<number> {
+    const query = 'DELETE FROM auth_log WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)';
+    const result = await executeQuery(query, [daysToKeep]) as any;
+    return result.affectedRows;
+  }
+
+  // �瑕���餈𤑳�霈方��亙�
+  static async getRecent(limit: number = 10): Promise<AuthLog[]> {
+    const query = 'SELECT * FROM auth_log ORDER BY created_at DESC LIMIT ?';
+    return await executeQuery(query, [limit]);
+  }
+
+  // 皜���抒�霈方��亙�嚗���齿䲮瘜𤏪�
+  static async cleanupOldLogs(daysToKeep: number = 30): Promise<number> {
+    return await this.cleanup(daysToKeep);
+  }
+
+  // �瑕�摰峕㟲��恕霂�𠯫敹㛖�霈∩縑�?
+  static async getFullStats(): Promise<any> {
+    try {
+      // �瑕��餅㺭
+      const totalQuery = 'SELECT COUNT(*) as count FROM auth_log';
+      const totalResult = await executeQuery(totalQuery);
+      const total = totalResult[0].count;
+
+      // �瑕��𣂼���仃韐交㺭
+      const resultQuery = `
+        SELECT 
+          result,
+          COUNT(*) as count
+        FROM auth_log
+        GROUP BY result
+      `;
+      const resultStats = await executeQuery(resultQuery);
+      let success = 0;
+      let failure = 0;
+      
+      resultStats.forEach((stat: any) => {
+        if (stat.result === 'success') {
+          success = stat.count;
+        } else if (stat.result === 'failure') {
+          failure = stat.count;
+        }
+      });
+
+      // �瑕��滢�蝐餃�蝏蠘恣
+      const operationQuery = `
+        SELECT 
+          operation_type,
+          COUNT(*) as count
+        FROM auth_log
+        GROUP BY operation_type
+      `;
+      const operationStats = await executeQuery(operationQuery);
+      const byOperationType: Record<string, number> = {};
+      
+      operationStats.forEach((stat: any) => {
+        byOperationType[stat.operation_type] = stat.count;
+      });
+
+      // �瑕��園𡢿��凒蝏蠘恣
+      const todayQuery = `
+        SELECT COUNT(*) as count 
+        FROM auth_log 
+        WHERE DATE(created_at) = CURDATE()
+      `;
+      const todayResult = await executeQuery(todayQuery);
+      const today = todayResult[0].count;
+
+      const weekQuery = `
+        SELECT COUNT(*) as count 
+        FROM auth_log 
+        WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+      `;
+      const weekResult = await executeQuery(weekQuery);
+      const week = weekResult[0].count;
+
+      const monthQuery = `
+        SELECT COUNT(*) as count 
+        FROM auth_log 
+        WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
+      `;
+      const monthResult = await executeQuery(monthQuery);
+      const month = monthResult[0].count;
+
+      return {
+        total,
+        success,
+        failure,
+        byOperationType,
+        byTimeRange: {
+          today,
+          week,
+          month
+        }
+      };
+    } catch (error) {
+      console.error('�瑕�霈方��亙�蝏蠘恣靽⊥�憭梯揖:', error);
+      // 餈𥪜�暺䁅恕�?
+      return {
+        total: 0,
+        success: 0,
+        failure: 0,
+        byOperationType: {},
+        byTimeRange: {
+          today: 0,
+          week: 0,
+          month: 0
+        }
+      };
+    }
+  }
+
+  // �瑕��滢�蝏蠘恣嚗���齿䲮瘜𤏪�
+  static async getOperationStats(startTime?: Date, endTime?: Date): Promise<any[]> {
+    return await this.getOperationTypeStats(startTime, endTime);
+  }
+
+  // �寞旿憭帋葵�∩辣�瑕�霈方��亙��圈�
+  static async getCountByMultipleConditions(
+    conditions: { [key: string]: any },
+    startTime?: Date,
+    endTime?: Date,
+    fuzzyFields?: string[] // �����閬�芋蝟𦠜䰻霂Y�摮埈挾
+  ): Promise<number> {
+    let query = 'SELECT COUNT(*) as count FROM auth_log WHERE 1=1';
+    const params: any[] = [];
+    
+    // 瘛餃��∩辣
+    for (const [key, value] of Object.entries(conditions)) {
+      if (value !== undefined && value !== null) {
+        // 璉��交糓�阡�閬�芋蝟𦠜䰻霂?
+        if (fuzzyFields && fuzzyFields.includes(key)) {
+          query += ` AND ${key} LIKE ?`;
+          params.push(`%${value}%`);
+        } else if (Array.isArray(value)) {
+          const placeholders = value.map(() => '?').join(', ');
+          query += ` AND ${key} IN (${placeholders})`;
+          params.push(...value);
+        } else {
+          query += ` AND ${key} = ?`;
+          params.push(value);
+        }
+      }
+    }
+    
+    // 瘛餃��園𡢿��凒
+    if (startTime && endTime) {
+      query += ' AND created_at BETWEEN ? AND ?';
+      params.push(startTime, endTime);
+    }
+    
+    const result = await executeQuery(query, params) as any;
+    return result[0].count;
+  }
+
+  // �寞旿憭帋葵�∩辣�瑕�霈方��亙�
+  static async getByMultipleConditions(
+    conditions: { [key: string]: any },
+    startTime?: Date,
+    endTime?: Date,
+    limit?: number,
+    offset?: number,
+    fuzzyFields?: string[] // �����閬�芋蝟𦠜䰻霂Y�摮埈挾
+  ): Promise<AuthLog[]> {
+    let query = 'SELECT * FROM auth_log WHERE 1=1';
+    const params: any[] = [];
+    
+    // 瘛餃��∩辣
+    for (const [key, value] of Object.entries(conditions)) {
+      if (value !== undefined && value !== null) {
+        // 璉��交糓�阡�閬�芋蝟𦠜䰻霂?
+        if (fuzzyFields && fuzzyFields.includes(key)) {
+          query += ` AND ${key} LIKE ?`;
+          params.push(`%${value}%`);
+        } else if (Array.isArray(value)) {
+          const placeholders = value.map(() => '?').join(', ');
+          query += ` AND ${key} IN (${placeholders})`;
+          params.push(...value);
+        } else {
+          query += ` AND ${key} = ?`;
+          params.push(value);
+        }
+      }
+    }
+    
+    // 瘛餃��園𡢿��凒
+    if (startTime && endTime) {
+      query += ' AND created_at BETWEEN ? AND ?';
+      params.push(startTime, endTime);
+    }
+    
+    // 瘛餃��鍦�
+    query += ' ORDER BY created_at DESC';
+    
+    // 瘛餃���△
+    if (limit !== undefined && offset !== undefined) {
+      query += ' LIMIT ? OFFSET ?';
+      params.push(limit, offset);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �寞旿�園𡢿��凒�瑕�霈方��亙��圈�
+  static async getCountByTimeRange(startTime: Date, endTime: Date): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM auth_log WHERE created_at BETWEEN ? AND ?';
+    const result = await executeQuery(query, [startTime, endTime]) as any;
+    return result[0].count;
+  }
+
+  // �寞旿蝏𤘪��瑕�霈方��亙��圈�
+  static async getCountByResult(result: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM auth_log WHERE result = ?';
+    const queryResult = await executeQuery(query, [result]) as any;
+    return queryResult[0].count;
+  }
+
+  // �寞旿IP�啣��瑕�霈方��亙��圈�
+  static async getCountByIpAddress(ipAddress: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM auth_log WHERE ip_address = ?';
+    const result = await executeQuery(query, [ipAddress]) as any;
+    return result[0].count;
+  }
+
+  // �寞旿�滢�蝐餃��瑕�霈方��亙��圈�
+  static async getCountByOperationType(operationType: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM auth_log WHERE operation_type = ?';
+    const result = await executeQuery(query, [operationType]) as any;
+    return result[0].count;
+  }
+
+  // �寞旿�冽��滩繮�𤥁恕霂�𠯫敹埈㺭�?
+  static async getCountByUsername(username: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM auth_log WHERE username = ?';
+    const result = await executeQuery(query, [username]) as any;
+    return result[0].count;
+  }
+
+  // �寞旿IP�啣��瑕�霈方��亙�
+  static async getByIpAddress(ipAddress: string, limit?: number, offset?: number): Promise<AuthLog[]> {
+    let query = 'SELECT * FROM auth_log WHERE ip_address = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [ipAddress];
+    
+    if (limit !== undefined && offset !== undefined) {
+      query += ' LIMIT ? OFFSET ?';
+      params.push(limit, offset);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �寞旿摰X�蝡涅D�瑕�霈方��亙��圈�
+  static async getCountByClientid(clientid: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM auth_log WHERE clientid = ?';
+    const result = await executeQuery(query, [clientid]) as any;
+    return result[0].count;
+  }
+}

+ 188 - 0
mqtt-vue-dashboard/server/src/models/clientAcl.ts

@@ -0,0 +1,188 @@
+import { executeQuery } from '../config/database';
+
+export interface ClientAcl {
+  id?: number;
+  clientid?: string;
+  username: string;
+  topic: string;
+  action: 'publish' | 'subscribe' | 'pubsub';
+  permission: 'allow' | 'deny';
+  priority?: number;
+  description?: string;
+  created_at?: Date;
+  updated_at?: Date;
+}
+
+export class ClientAclModel {
+  static async getAll(limit?: number, offset?: number): Promise<ClientAcl[]> {
+    let query = 'SELECT * FROM client_acl ORDER BY username, priority DESC, created_at DESC';
+    const params: any[] = [];
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    return await executeQuery(query, params);
+  }
+
+  static async getById(id: number): Promise<ClientAcl | null> {
+    const query = 'SELECT * FROM client_acl WHERE id = ?';
+    const acls = await executeQuery(query, [id]);
+    return acls.length > 0 ? acls[0] : null;
+  }
+
+  static async getByUsername(username: string): Promise<ClientAcl[]> {
+    const query = 'SELECT * FROM client_acl WHERE username = ? ORDER BY priority DESC, created_at DESC';
+    return await executeQuery(query, [username]);
+  }
+
+  static async getByAction(action: string): Promise<ClientAcl[]> {
+    const query = 'SELECT * FROM client_acl WHERE action = ? ORDER BY priority DESC, created_at DESC';
+    return await executeQuery(query, [action]);
+  }
+
+  static async getByTopic(topic: string): Promise<ClientAcl[]> {
+    const query = 'SELECT * FROM client_acl WHERE topic LIKE ? ORDER BY priority DESC, created_at DESC';
+    return await executeQuery(query, [`%`+topic+`%`]);
+  }
+
+  static async getByPermission(permission: string): Promise<ClientAcl[]> {
+    const query = 'SELECT * FROM client_acl WHERE permission = ? ORDER BY priority DESC, created_at DESC';
+    return await executeQuery(query, [permission]);
+  }
+
+  static async getCount(): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM client_acl';
+    const result = await executeQuery(query);
+    return result[0].count;
+  }
+
+  static async getActionStats(): Promise<any[]> {
+    const query = 'SELECT action, COUNT(*) as count FROM client_acl GROUP BY action';
+    return await executeQuery(query);
+  }
+
+  static async getPermissionStats(): Promise<any[]> {
+    const query = 'SELECT permission, COUNT(*) as count FROM client_acl GROUP BY permission';
+    return await executeQuery(query);
+  }
+
+  static async create(aclData: Omit<ClientAcl, 'id' | 'created_at' | 'updated_at'>): Promise<ClientAcl> {
+    const query = 'INSERT INTO client_acl (clientid, username, topic, action, permission, priority, description) VALUES (?, ?, ?, ?, ?, ?, ?)';
+    const values = [
+      aclData.clientid || null,
+      aclData.username,
+      aclData.topic,
+      aclData.action,
+      aclData.permission,
+      aclData.priority || 0,
+      aclData.description || null
+    ];
+    const result = await executeQuery(query, values) as any;
+    return this.getById(result.insertId) as Promise<ClientAcl>;
+  }
+
+  static async update(id: number, updateData: Partial<Omit<ClientAcl, 'id' | 'created_at'>>): Promise<boolean> {
+    const fields = [];
+    const values = [];
+    if (updateData.clientid !== undefined) { fields.push('clientid = ?'); values.push(updateData.clientid); }
+    if (updateData.username !== undefined) { fields.push('username = ?'); values.push(updateData.username); }
+    if (updateData.topic !== undefined) { fields.push('topic = ?'); values.push(updateData.topic); }
+    if (updateData.action !== undefined) { fields.push('action = ?'); values.push(updateData.action); }
+    if (updateData.permission !== undefined) { fields.push('permission = ?'); values.push(updateData.permission); }
+    if (updateData.priority !== undefined) { fields.push('priority = ?'); values.push(updateData.priority); }
+    if (updateData.description !== undefined) { fields.push('description = ?'); values.push(updateData.description); }
+    fields.push('updated_at = NOW()');
+    if (fields.length === 0) return false;
+    values.push(id);
+    const query = 'UPDATE client_acl SET ' + fields.join(', ') + ' WHERE id = ?';
+    const result = await executeQuery(query, values) as any;
+    return result.affectedRows > 0;
+  }
+
+  static async delete(id: number): Promise<boolean> {
+    const query = 'DELETE FROM client_acl WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  static async deleteByUsername(username: string): Promise<boolean> {
+    const query = 'DELETE FROM client_acl WHERE username = ?';
+    const result = await executeQuery(query, [username]) as any;
+    return result.affectedRows > 0;
+  }
+
+  static async search(searchTerm: string, limit?: number, offset?: number): Promise<ClientAcl[]> {
+    let query = 'SELECT * FROM client_acl WHERE username LIKE ? OR topic LIKE ? OR action LIKE ? OR permission LIKE ? OR description LIKE ? ORDER BY username, priority DESC, created_at DESC';
+    const params: (string | number)[] = [`%`+searchTerm+`%`, `%`+searchTerm+`%`, `%`+searchTerm+`%`, `%`+searchTerm+`%`, `%`+searchTerm+`%`];
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      if (offset !== undefined) { query += ' OFFSET ?'; params.push(offset); }
+    }
+    return await executeQuery(query, params);
+  }
+
+  static async getSearchCount(searchTerm: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM client_acl WHERE username LIKE ? OR topic LIKE ? OR action LIKE ? OR permission LIKE ? OR description LIKE ?';
+    const params = [`%`+searchTerm+`%`, `%`+searchTerm+`%`, `%`+searchTerm+`%`, `%`+searchTerm+`%`, `%`+searchTerm+`%`];
+    const result = await executeQuery(query, params);
+    return result[0].count;
+  }
+
+  static async createBatch(aclDataList: Omit<ClientAcl, 'id' | 'created_at' | 'updated_at'>[]): Promise<boolean> {
+    if (aclDataList.length === 0) return false;
+    const values: (string | number | null)[] = [];
+    const placeholders: string[] = [];
+    aclDataList.forEach(acl => {
+      placeholders.push('(?, ?, ?, ?, ?, ?, ?)');
+      values.push(acl.clientid || null, acl.username, acl.topic, acl.action, acl.permission, acl.priority || 0, acl.description || null);
+    });
+    const query = 'INSERT INTO client_acl (clientid, username, topic, action, permission, priority, description) VALUES ' + placeholders.join(', ');
+    const result = await executeQuery(query, values) as any;
+    return result.affectedRows > 0;
+  }
+
+  static async copyToUser(sourceUsername: string, targetUsername: string): Promise<boolean> {
+    const sourceAcls = await this.getByUsername(sourceUsername);
+    if (sourceAcls.length === 0) return false;
+    const targetAcls = sourceAcls.map(acl => ({
+      clientid: acl.clientid,
+      username: targetUsername,
+      topic: acl.topic,
+      action: acl.action,
+      permission: acl.permission,
+      priority: acl.priority,
+      description: acl.description
+    }));
+    return await this.createBatch(targetAcls);
+  }
+
+  static async deleteMultiple(ids: number[]): Promise<boolean> {
+    if (ids.length === 0) return false;
+    const placeholders = ids.map(() => '?').join(',');
+    const query = 'DELETE FROM client_acl WHERE id IN (' + placeholders + ')';
+    const result = await executeQuery(query, ids) as any;
+    return result.affectedRows > 0;
+  }
+
+  static async getByUsernameAndAction(username: string, action: string): Promise<ClientAcl[]> {
+    const query = 'SELECT * FROM client_acl WHERE username = ? AND action = ? ORDER BY priority DESC, created_at DESC';
+    return await executeQuery(query, [username, action]);
+  }
+
+  static async checkPermission(username: string, topic: string, action: string): Promise<boolean> {
+    const query = "SELECT * FROM client_acl WHERE username = ? AND action IN (?, 'pubsub') ORDER BY priority DESC";
+    const acls = await executeQuery(query, [username, action]);
+    if (acls.length === 0) return false;
+    for (const acl of acls) {
+      const topicPattern = acl.topic.replace('%', '*');
+      const regex = new RegExp(topicPattern.replace(/\*/g, '.*'));
+      if (regex.test(topic)) return acl.permission === 'allow';
+    }
+    return false;
+  }
+}

+ 1035 - 0
mqtt-vue-dashboard/server/src/models/clientAuth.ts

@@ -0,0 +1,1035 @@
+import { executeQuery } from '../config/database';
+import * as crypto from 'crypto';
+
+export interface ClientAuth {
+  id?: number;
+  username: string;
+  clientid: string;
+  password?: string; // �擧�撖��嚗䔶��其��𥕦遣�峕凒�?
+  password_hash: string;
+  salt: string;
+  use_salt?: boolean; // �啣�摮埈挾嚗峕�霂�糓�虫蝙�函��?
+  status: 'enabled' | 'disabled';
+  device_type?: string;
+  description?: string;
+  is_superuser?: boolean;
+  created_at?: Date;
+  updated_at?: Date;
+  last_login_at?: Date;
+  // �冽��恕霂�㮾�喳�畾?
+  auth_method?: 'password' | 'token' | 'certificate' | 'external'; // 霈方��寞�嚗䔶��唳旿摨𤘪�銝曄掩�衤��?
+  auth_expiry?: Date | null; // 霈方�餈���園𡢿
+  allowed_ip_ranges?: string | null; // ��捂��P��凒嚗𥇍SON�澆�
+  allowed_time_ranges?: string | null; // ��捂��𧒄�渲��湛�JSON�澆�
+  auth_policy_id?: number | null; // �唾���恕霂���付D
+}
+
+export interface AuthMethod {
+  id: number;
+  method_name: string;
+  method_type: 'password' | 'token' | 'certificate' | 'external';
+  config: any; // JSON�澆����蝵?
+  is_active: boolean;
+  created_at: Date;
+  updated_at: Date;
+}
+
+export interface AuthPolicy {
+  id: number;
+  policy_name: string;
+  priority: number;
+  conditions: any; // JSON�澆���辺隞?
+  actions: any; // JSON�澆����雿?
+  is_active: boolean;
+  description?: string;
+  created_at: Date;
+  updated_at: Date;
+}
+
+export interface ClientToken {
+  id?: number;
+  clientid: string;
+  token_type: 'jwt' | 'temporary' | 'refresh';
+  token_value: string;
+  expires_at: Date;
+  status: 'active' | 'revoked' | 'expired';
+  created_at?: Date;
+  updated_at?: Date;
+}
+
+export interface ClientCertificate {
+  id?: number;
+  clientid: string;
+  certificate_pem: string;
+  fingerprint: string;
+  expires_at: Date;
+  status: 'active' | 'revoked' | 'expired';
+  created_at?: Date;
+  updated_at?: Date;
+}
+
+export class ClientAuthModel {
+  // ����𤩺㦤�𣂼�?
+  static generateSalt(): string {
+    return crypto.randomBytes(16).toString('hex');
+  }
+
+  // ���撖�����嚗��EMQX SHA256+suffix�澆捆嚗?
+  static generatePasswordHash(password: string, salt: string, useSalt: boolean = true): string {
+    if (useSalt) {
+      // 雿輻鍂SHA256蝞埈�嚗���鞉䲮撘譍蛹suffix嚗������W��琜�
+      return crypto.createHash('sha256').update(password + salt).digest('hex');
+    } else {
+      // 銝滚��琜��湔𦻖撖孵����銵��撣?
+      return crypto.createHash('sha256').update(password).digest('hex');
+    }
+  }
+
+  // ���撖�����嚗��憪閪BKDF2�寞�嚗䔶��嗘�銝箏��剁�
+  static generatePasswordHashPBKDF2(password: string, salt: string): string {
+    return crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
+  }
+
+  // 撉諹�撖��
+  static verifyPassword(password: string, salt: string, hash: string, useSalt: boolean = true): boolean {
+    const hashVerify = this.generatePasswordHash(password, salt, useSalt);
+    return hash === hashVerify;
+  }
+
+  // 撉諹��冽�������其�EMQX HTTP霈方��亙藁嚗?
+  static async verifyDynamicPassword(username: string, clientid: string, password: string): Promise<{ valid: boolean }> {
+    try {
+      // �桀�瘝⊥��瑚���𢆡������霂��餉�嚗諹��𤜆alse霈拍頂蝏笔����啣虜閫�����霂?
+      // �臭誑�寞旿摰鮋���瘙��撅訫𢆡������霂��餉�
+      return { valid: false };
+    } catch (error) {
+      console.error('�冽������霂�仃韐?', error);
+      return { valid: false };
+    }
+  }
+
+  // �瑕����匧恥�瑞垢霈方�靽⊥�
+  static async getAll(limit?: number, offset?: number): Promise<ClientAuth[]> {
+    let query = 'SELECT id, username, clientid, status, device_type, description, is_superuser, use_salt, salt, auth_method, auth_expiry, allowed_ip_ranges, allowed_time_ranges, auth_policy_id, created_at, updated_at, last_login_at FROM client_auth ORDER BY created_at DESC';
+    const params: any[] = [];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �寞旿ID�瑕�摰X�蝡航恕霂�縑�?
+  static async getById(id: number): Promise<ClientAuth | null> {
+    const query = 'SELECT * FROM client_auth WHERE id = ?';
+    const clients = await executeQuery(query, [id]);
+    return clients.length > 0 ? clients[0] : null;
+  }
+
+  // �寞旿�冽��滚�摰X�蝡涅D�瑕�霈方�靽⊥�
+  static async getByUsernameAndClientid(username: string, clientid: string): Promise<ClientAuth | null> {
+    const query = 'SELECT * FROM client_auth WHERE username = ? AND clientid = ?';
+    const clients = await executeQuery(query, [username, clientid]);
+    return clients.length > 0 ? clients[0] : null;
+  }
+
+  // �寞旿�嗆��繮�硋恥�瑞垢霈方�靽⊥�
+  static async getByStatus(status: string): Promise<ClientAuth[]> {
+    const query = 'SELECT id, username, clientid, status, device_type, description, is_superuser, use_salt, salt, auth_method, auth_expiry, allowed_ip_ranges, allowed_time_ranges, auth_policy_id, created_at, updated_at, last_login_at FROM client_auth WHERE status = ? ORDER BY created_at DESC';
+    return await executeQuery(query, [status]);
+  }
+
+  // �瑕�摰X�蝡航恕霂��餅㺭
+  static async getCount(): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM client_auth';
+    const result = await executeQuery(query);
+    return result[0].count;
+  }
+
+  // �瑕��嗆���霈?
+  static async getStatusStats(): Promise<any> {
+    try {
+      // �瑕��餅㺭�?
+      const totalCountQuery = 'SELECT COUNT(*) as count FROM client_auth';
+      const totalResult = await executeQuery(totalCountQuery);
+      const total = totalResult[0].count;
+      
+      // �瑕���𠶖��㺭�?
+      const statusQuery = `
+        SELECT 
+          status,
+          COUNT(*) as count
+        FROM client_auth
+        GROUP BY status
+      `;
+      const statusResults = await executeQuery(statusQuery);
+      
+      // �瑕�頞�漣�冽��圈�
+      const superuserQuery = 'SELECT COUNT(*) as count FROM client_auth WHERE is_superuser = 1';
+      const superuserResult = await executeQuery(superuserQuery);
+      const superuserCount = superuserResult[0].count;
+      
+      // 蝏蠘恣��𠶖��㺭�?
+      let activeCount = 0;
+      let inactiveCount = 0;
+      
+      statusResults.forEach((row: any) => {
+        if (row.status === 'enabled') {
+          activeCount = row.count;
+        } else if (row.status === 'disabled') {
+          inactiveCount = row.count;
+        }
+      });
+      
+      return {
+        total,
+        active: activeCount,
+        inactive: inactiveCount,
+        superuser: superuserCount
+      };
+    } catch (error) {
+      console.error('�瑕�摰X�蝡航恕霂��霈∩縑�臬仃韐?', error);
+      throw error;
+    }
+  }
+
+  // �瑕�霈曉�蝐餃�蝏蠘恣
+  static async getDeviceTypeStats(): Promise<any[]> {
+    const query = `
+      SELECT 
+        device_type,
+        COUNT(*) as count
+      FROM client_auth
+      GROUP BY device_type
+      ORDER BY count DESC
+    `;
+    return await executeQuery(query);
+  }
+
+  // �𥕦遣�啣恥�瑞垢霈方�
+  static async create(clientAuthData: Omit<ClientAuth, 'id' | 'created_at' | 'updated_at'>): Promise<ClientAuth> {
+    // 暺䁅恕雿輻鍂�𣂼�?
+    const useSalt = clientAuthData.use_salt !== undefined ? clientAuthData.use_salt : true;
+    
+    // �芣��其蝙�函��潭𧒄�滨��鞟��?
+    if (useSalt && !clientAuthData.salt) {
+      clientAuthData.salt = this.generateSalt();
+    }
+    
+    // 憒��銝滢蝙�函��潘�蝖桐�salt銝箇征摮㛖泵銝?
+    if (!useSalt) {
+      clientAuthData.salt = '';
+    }
+    
+    // 憒���𣂷�鈭���������嗵��𣂼�撣?
+    if (clientAuthData.password && !clientAuthData.password_hash) {
+      clientAuthData.password_hash = this.generatePasswordHash(clientAuthData.password, clientAuthData.salt, useSalt);
+    }
+    
+    const query = `
+      INSERT INTO client_auth (username, clientid, password_hash, salt, use_salt, status, device_type, description, is_superuser, auth_method, auth_expiry, allowed_ip_ranges, allowed_time_ranges, auth_policy_id)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      clientAuthData.username,
+      clientAuthData.clientid,
+      clientAuthData.password_hash,
+      clientAuthData.salt,
+      useSalt ? 1 : 0,
+      clientAuthData.status || 'enabled',
+      clientAuthData.device_type || 'unknown',
+      clientAuthData.description || null,
+      clientAuthData.is_superuser ? 1 : 0,
+      clientAuthData.auth_method || 'password',
+      clientAuthData.auth_expiry || null,
+      clientAuthData.allowed_ip_ranges || null,
+      clientAuthData.allowed_time_ranges || null,
+      clientAuthData.auth_policy_id || null
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    // 餈𥪜��𥕦遣��恥�瑞垢霈方�
+    return this.getById(result.insertId) as Promise<ClientAuth>;
+  }
+
+  // �湔鰵摰X�蝡航恕霂�縑�?
+  static async update(id: number, updateData: Partial<Omit<ClientAuth, 'id' | 'created_at'>>): Promise<ClientAuth | null> {
+    const fields = [];
+    const values = [];
+    
+    // �冽���撱箸凒�啣�畾?
+    if (updateData.username !== undefined) {
+      fields.push('username = ?');
+      values.push(updateData.username);
+    }
+    
+    if (updateData.clientid !== undefined) {
+      fields.push('clientid = ?');
+      values.push(updateData.clientid);
+    }
+    
+    if (updateData.password_hash !== undefined) {
+      fields.push('password_hash = ?');
+      values.push(updateData.password_hash);
+    }
+    
+    if (updateData.salt !== undefined) {
+      fields.push('salt = ?');
+      values.push(updateData.salt);
+    }
+    
+    if (updateData.use_salt !== undefined) {
+      fields.push('use_salt = ?');
+      values.push(updateData.use_salt ? 1 : 0);
+    }
+    
+    if (updateData.status !== undefined) {
+      fields.push('status = ?');
+      values.push(updateData.status);
+    }
+    
+    if (updateData.device_type !== undefined) {
+      fields.push('device_type = ?');
+      values.push(updateData.device_type);
+    }
+    
+    if (updateData.description !== undefined) {
+      fields.push('description = ?');
+      values.push(updateData.description);
+    }
+    
+    if (updateData.is_superuser !== undefined) {
+      fields.push('is_superuser = ?');
+      values.push(updateData.is_superuser ? 1 : 0);
+    }
+    
+    if (updateData.last_login_at !== undefined) {
+      fields.push('last_login_at = ?');
+      values.push(updateData.last_login_at);
+    }
+    
+    // �冽��恕霂�㮾�喳�畾?
+    if (updateData.auth_method !== undefined) {
+      fields.push('auth_method = ?');
+      values.push(updateData.auth_method);
+    }
+    
+    if (updateData.auth_expiry !== undefined) {
+      fields.push('auth_expiry = ?');
+      values.push(updateData.auth_expiry);
+    }
+    
+    if (updateData.allowed_ip_ranges !== undefined) {
+      fields.push('allowed_ip_ranges = ?');
+      values.push(updateData.allowed_ip_ranges);
+    }
+    
+    if (updateData.allowed_time_ranges !== undefined) {
+      fields.push('allowed_time_ranges = ?');
+      values.push(updateData.allowed_time_ranges);
+    }
+    
+    if (updateData.auth_policy_id !== undefined) {
+      fields.push('auth_policy_id = ?');
+      values.push(updateData.auth_policy_id);
+    }
+    
+    // 瘛餃��湔鰵�園𡢿
+    fields.push('updated_at = CURRENT_TIMESTAMP');
+    
+    // ��遣�亥砭
+    const query = `UPDATE client_auth SET ${fields.join(', ')} WHERE id = ?`;
+    values.push(id);
+    
+    await executeQuery(query, values);
+    
+    // 餈𥪜��湔鰵�𡒊�摰X�蝡航恕霂?
+    return this.getById(id);
+  }
+
+  // �湔鰵撖��
+  static async updatePassword(id: number, newPassword: string, useSalt: boolean = true): Promise<boolean> {
+    const salt = useSalt ? this.generateSalt() : '';
+    const passwordHash = this.generatePasswordHash(newPassword, salt, useSalt);
+    
+    const query = 'UPDATE client_auth SET password_hash = ?, salt = ?, use_salt = ?, updated_at = NOW() WHERE id = ?';
+    const result = await executeQuery(query, [passwordHash, salt, useSalt ? 1 : 0, id]) as any;
+    
+    return result.affectedRows > 0;
+  }
+
+  // �𣳇膄摰X�蝡航恕霂?
+  static async delete(id: number): Promise<boolean> {
+    const query = 'DELETE FROM client_auth WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    
+    return result.affectedRows > 0;
+  }
+
+  // �𦦵揣摰X�蝡航恕霂?
+  static async search(searchTerm: string, limit?: number, offset?: number): Promise<ClientAuth[]> {
+    let query = `
+      SELECT id, username, clientid, status, device_type, description, is_superuser, use_salt, salt, created_at, updated_at, last_login_at 
+      FROM client_auth 
+      WHERE username LIKE ? OR clientid LIKE ? OR device_type LIKE ? OR description LIKE ?
+      ORDER BY created_at DESC
+    `;
+    const params: (string | number)[] = [`%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // �瑕��𦦵揣蝏𤘪��餅㺭
+  static async getSearchCount(searchTerm: string): Promise<number> {
+    const query = `
+      SELECT COUNT(*) as count 
+      FROM client_auth 
+      WHERE username LIKE ? OR clientid LIKE ? OR device_type LIKE ? OR description LIKE ?
+    `;
+    const params = [`%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`];
+    const result = await executeQuery(query, params);
+    return result[0].count;
+  }
+
+  // �寞旿�冽��滩繮�硋恥�瑞垢霈方�靽⊥�
+  static async getByUsername(username: string): Promise<ClientAuth | null> {
+    const query = 'SELECT * FROM client_auth WHERE username = ?';
+    const clients = await executeQuery(query, [username]);
+    return clients.length > 0 ? clients[0] : null;
+  }
+
+  // �寞旿摰X�蝡涅D�瑕�摰X�蝡航恕霂�縑�?
+  static async getByClientId(clientid: string): Promise<ClientAuth | null> {
+    const query = 'SELECT * FROM client_auth WHERE clientid = ?';
+    const clients = await executeQuery(query, [clientid]);
+    return clients.length > 0 ? clients[0] : null;
+  }
+
+  // 撉諹�摰X�蝡航恕霂�縑�?
+  static async verifyClient(username: string, clientid: string, password: string): Promise<boolean> {
+    const clientAuth = await this.getByUsernameAndClientid(username, clientid);
+    if (!clientAuth || clientAuth.status !== 'enabled') {
+      return false;
+    }
+    // �寞旿�唳旿摨㮖葉��se_salt摮埈挾�喳�撉諹��孵�
+    const useSalt = clientAuth.use_salt !== undefined ? clientAuth.use_salt : true;
+    return this.verifyPassword(password, clientAuth.salt, clientAuth.password_hash, useSalt);
+  }
+
+  // �冽��恕霂�㮾�單䲮瘜?
+
+  // �瑕����㕑恕霂�䲮瘜?
+  static async getAuthMethods(): Promise<AuthMethod[]> {
+    const query = 'SELECT * FROM auth_methods ORDER BY method_name';
+    return await executeQuery(query);
+  }
+
+  // �寞旿ID�瑕�霈方��寞�
+  static async getAuthMethodById(id: number): Promise<AuthMethod | null> {
+    const query = 'SELECT * FROM auth_methods WHERE id = ?';
+    const methods = await executeQuery(query, [id]);
+    return methods.length > 0 ? methods[0] : null;
+  }
+
+  // �寞旿�寞��滨妍�瑕�霈方��寞�
+  static async getAuthMethodByName(name: string): Promise<AuthMethod | null> {
+    const query = 'SELECT * FROM auth_methods WHERE method_name = ?';
+    const methods = await executeQuery(query, [name]);
+    return methods.length > 0 ? methods[0] : null;
+  }
+
+  // �𥕦遣霈方��寞�
+  static async createAuthMethod(authMethod: Omit<AuthMethod, 'id' | 'created_at' | 'updated_at'>): Promise<AuthMethod> {
+    const query = `
+      INSERT INTO auth_methods (method_name, method_type, config, is_active)
+      VALUES (?, ?, ?, ?)
+    `;
+    
+    const values = [
+      authMethod.method_name,
+      authMethod.method_type,
+      authMethod.config,
+      authMethod.is_active ? 1 : 0
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    return this.getAuthMethodById(result.insertId) as Promise<AuthMethod>;
+  }
+
+  // �湔鰵霈方��寞�
+  static async updateAuthMethod(id: number, updateData: Partial<Omit<AuthMethod, 'id' | 'created_at'>>): Promise<AuthMethod | null> {
+    const fields = [];
+    const values = [];
+    
+    if (updateData.method_name !== undefined) {
+      fields.push('method_name = ?');
+      values.push(updateData.method_name);
+    }
+    
+    if (updateData.method_type !== undefined) {
+      fields.push('method_type = ?');
+      values.push(updateData.method_type);
+    }
+    
+    if (updateData.config !== undefined) {
+      fields.push('config = ?');
+      values.push(updateData.config);
+    }
+    
+    if (updateData.is_active !== undefined) {
+      fields.push('is_active = ?');
+      values.push(updateData.is_active ? 1 : 0);
+    }
+    
+    fields.push('updated_at = CURRENT_TIMESTAMP');
+    
+    const query = `UPDATE auth_methods SET ${fields.join(', ')} WHERE id = ?`;
+    values.push(id);
+    
+    await executeQuery(query, values);
+    return this.getAuthMethodById(id);
+  }
+
+  // �𣳇膄霈方��寞�
+  static async deleteAuthMethod(id: number): Promise<boolean> {
+    const query = 'DELETE FROM auth_methods WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // �瑕����㕑恕霂���?
+  static async getAuthPolicies(): Promise<AuthPolicy[]> {
+    const query = 'SELECT * FROM auth_policies ORDER BY priority DESC, created_at ASC';
+    return await executeQuery(query);
+  }
+
+  // �寞旿ID�瑕�霈方�蝑𣇉裦
+  static async getAuthPolicyById(id: number): Promise<AuthPolicy | null> {
+    const query = 'SELECT * FROM auth_policies WHERE id = ?';
+    const policies = await executeQuery(query, [id]);
+    return policies.length > 0 ? policies[0] : null;
+  }
+
+  // �𥕦遣霈方�蝑𣇉裦
+  static async createAuthPolicy(authPolicy: Omit<AuthPolicy, 'id' | 'created_at' | 'updated_at'>): Promise<AuthPolicy> {
+    const query = `
+      INSERT INTO auth_policies (policy_name, priority, conditions, actions, is_active, description)
+      VALUES (?, ?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      authPolicy.policy_name,
+      authPolicy.priority,
+      authPolicy.conditions,
+      authPolicy.actions,
+      authPolicy.is_active ? 1 : 0,
+      authPolicy.description || null
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    return this.getAuthPolicyById(result.insertId) as Promise<AuthPolicy>;
+  }
+
+  // �湔鰵霈方�蝑𣇉裦
+  static async updateAuthPolicy(id: number, updateData: Partial<Omit<AuthPolicy, 'id' | 'created_at'>>): Promise<AuthPolicy | null> {
+    const fields = [];
+    const values = [];
+    
+    if (updateData.policy_name !== undefined) {
+      fields.push('policy_name = ?');
+      values.push(updateData.policy_name);
+    }
+    
+    if (updateData.priority !== undefined) {
+      fields.push('priority = ?');
+      values.push(updateData.priority);
+    }
+    
+    if (updateData.conditions !== undefined) {
+      fields.push('conditions = ?');
+      values.push(updateData.conditions);
+    }
+    
+    if (updateData.actions !== undefined) {
+      fields.push('actions = ?');
+      values.push(updateData.actions);
+    }
+    
+    if (updateData.is_active !== undefined) {
+      fields.push('is_active = ?');
+      values.push(updateData.is_active ? 1 : 0);
+    }
+    
+    if (updateData.description !== undefined) {
+      fields.push('description = ?');
+      values.push(updateData.description);
+    }
+    
+    fields.push('updated_at = CURRENT_TIMESTAMP');
+    
+    const query = `UPDATE auth_policies SET ${fields.join(', ')} WHERE id = ?`;
+    values.push(id);
+    
+    await executeQuery(query, values);
+    return this.getAuthPolicyById(id);
+  }
+
+  // �𣳇膄霈方�蝑𣇉裦
+  static async deleteAuthPolicy(id: number): Promise<boolean> {
+    const query = 'DELETE FROM auth_policies WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // �瑕�摰X�蝡臭誘�?
+  static async getClientTokens(clientid: string): Promise<ClientToken[]> {
+    const query = 'SELECT * FROM client_tokens WHERE clientid = ? ORDER BY created_at DESC';
+    return await executeQuery(query, [clientid]);
+  }
+
+  // �寞旿隞斤��潸繮�硋恥�瑞垢隞斤�
+  static async getClientTokenByValue(tokenValue: string): Promise<ClientToken | null> {
+    const query = 'SELECT * FROM client_tokens WHERE token_value = ?';
+    const tokens = await executeQuery(query, [tokenValue]);
+    return tokens.length > 0 ? tokens[0] : null;
+  }
+
+  // �𥕦遣摰X�蝡臭誘�?
+  static async createClientToken(clientToken: Omit<ClientToken, 'id' | 'created_at' | 'updated_at'>): Promise<ClientToken> {
+    const query = `
+      INSERT INTO client_tokens (clientid, token_type, token_value, expires_at, status)
+      VALUES (?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      clientToken.clientid,
+      clientToken.token_type,
+      clientToken.token_value,
+      clientToken.expires_at,
+      clientToken.status || 'active'
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    // 餈𥪜��𥕦遣��恥�瑞垢隞斤�
+    const query2 = 'SELECT * FROM client_tokens WHERE id = ?';
+    const tokens = await executeQuery(query2, [result.insertId]);
+    return tokens[0] as ClientToken;
+  }
+
+  // �湔鰵摰X�蝡臭誘�?
+  static async updateClientToken(id: number, updateData: Partial<Omit<ClientToken, 'id' | 'created_at'>>): Promise<ClientToken | null> {
+    const fields = [];
+    const values = [];
+    
+    if (updateData.clientid !== undefined) {
+      fields.push('clientid = ?');
+      values.push(updateData.clientid);
+    }
+    
+    if (updateData.token_type !== undefined) {
+      fields.push('token_type = ?');
+      values.push(updateData.token_type);
+    }
+    
+    if (updateData.token_value !== undefined) {
+      fields.push('token_value = ?');
+      values.push(updateData.token_value);
+    }
+    
+    if (updateData.expires_at !== undefined) {
+      fields.push('expires_at = ?');
+      values.push(updateData.expires_at);
+    }
+    
+    if (updateData.status !== undefined) {
+      fields.push('status = ?');
+      values.push(updateData.status);
+    }
+    
+    fields.push('updated_at = CURRENT_TIMESTAMP');
+    
+    const query = `UPDATE client_tokens SET ${fields.join(', ')} WHERE id = ?`;
+    values.push(id);
+    
+    await executeQuery(query, values);
+    
+    // 餈𥪜��湔鰵�𡒊�摰X�蝡臭誘�?
+    const query2 = 'SELECT * FROM client_tokens WHERE id = ?';
+    const tokens = await executeQuery(query2, [id]);
+    return tokens.length > 0 ? tokens[0] : null;
+  }
+
+  // �𣳇膄摰X�蝡臭誘�?
+  static async deleteClientToken(id: number): Promise<boolean> {
+    const query = 'DELETE FROM client_tokens WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // �瑕�摰X�蝡航�銋?
+  static async getClientCertificates(clientid: string): Promise<ClientCertificate[]> {
+    const query = 'SELECT * FROM client_certificates WHERE clientid = ? ORDER BY created_at DESC';
+    return await executeQuery(query, [clientid]);
+  }
+
+  // �寞旿��犒�瑕�摰X�蝡航�銋?
+  static async getClientCertificateByFingerprint(fingerprint: string): Promise<ClientCertificate | null> {
+    const query = 'SELECT * FROM client_certificates WHERE fingerprint = ?';
+    const certificates = await executeQuery(query, [fingerprint]);
+    return certificates.length > 0 ? certificates[0] : null;
+  }
+
+  // �𥕦遣摰X�蝡航�銋?
+  static async createClientCertificate(clientCertificate: Omit<ClientCertificate, 'id' | 'created_at' | 'updated_at'>): Promise<ClientCertificate> {
+    const query = `
+      INSERT INTO client_certificates (clientid, certificate_pem, fingerprint, expires_at, status)
+      VALUES (?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      clientCertificate.clientid,
+      clientCertificate.certificate_pem,
+      clientCertificate.fingerprint,
+      clientCertificate.expires_at,
+      clientCertificate.status || 'active'
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    // 餈𥪜��𥕦遣��恥�瑞垢霂�髡
+    const query2 = 'SELECT * FROM client_certificates WHERE id = ?';
+    const certificates = await executeQuery(query2, [result.insertId]);
+    return certificates[0] as ClientCertificate;
+  }
+
+  // �湔鰵摰X�蝡航�銋?
+  static async updateClientCertificate(id: number, updateData: Partial<Omit<ClientCertificate, 'id' | 'created_at'>>): Promise<ClientCertificate | null> {
+    const fields = [];
+    const values = [];
+    
+    if (updateData.clientid !== undefined) {
+      fields.push('clientid = ?');
+      values.push(updateData.clientid);
+    }
+    
+    if (updateData.certificate_pem !== undefined) {
+      fields.push('certificate_pem = ?');
+      values.push(updateData.certificate_pem);
+    }
+    
+    if (updateData.fingerprint !== undefined) {
+      fields.push('fingerprint = ?');
+      values.push(updateData.fingerprint);
+    }
+    
+    if (updateData.expires_at !== undefined) {
+      fields.push('expires_at = ?');
+      values.push(updateData.expires_at);
+    }
+    
+    if (updateData.status !== undefined) {
+      fields.push('status = ?');
+      values.push(updateData.status);
+    }
+    
+    fields.push('updated_at = CURRENT_TIMESTAMP');
+    
+    const query = `UPDATE client_certificates SET ${fields.join(', ')} WHERE id = ?`;
+    values.push(id);
+    
+    await executeQuery(query, values);
+    
+    // 餈𥪜��湔鰵�𡒊�摰X�蝡航�銋?
+    const query2 = 'SELECT * FROM client_certificates WHERE id = ?';
+    const certificates = await executeQuery(query2, [id]);
+    return certificates.length > 0 ? certificates[0] : null;
+  }
+
+  // �𣳇膄摰X�蝡航�銋?
+  static async deleteClientCertificate(id: number): Promise<boolean> {
+    const query = 'DELETE FROM client_certificates WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // �冽��恕霂��霂�䲮瘜?
+  static async dynamicAuthVerify(username: string, clientid: string, authData: any, ipAddress?: string): Promise<{ success: boolean, reason?: string, policy?: any }> {
+    try {
+      // 1. �瑕�摰X�蝡航恕霂�縑�?
+      const clientAuth = await this.getByUsernameAndClientid(username, clientid);
+      if (!clientAuth) {
+        return { success: false, reason: 'Client not found' };
+      }
+      
+      if (clientAuth.status !== 'enabled') {
+        return { success: false, reason: 'Client is disabled' };
+      }
+      
+      // 2. 璉��亥恕霂�糓�西��?
+      if (clientAuth.auth_expiry && new Date(clientAuth.auth_expiry) < new Date()) {
+        return { success: false, reason: 'Authentication expired' };
+      }
+      
+      // 3. 璉��付P��凒�𣂼�
+      if (clientAuth.allowed_ip_ranges && ipAddress) {
+        const allowedRanges = JSON.parse(clientAuth.allowed_ip_ranges);
+        if (!this.isIpAllowed(ipAddress, allowedRanges)) {
+          return { success: false, reason: 'IP address not allowed' };
+        }
+      }
+      
+      // 4. 璉��交𧒄�渲��湧��?
+      if (clientAuth.allowed_time_ranges) {
+        const allowedTimeRanges = JSON.parse(clientAuth.allowed_time_ranges);
+        if (!this.isTimeAllowed(allowedTimeRanges)) {
+          return { success: false, reason: 'Access not allowed at this time' };
+        }
+      }
+      
+      // 5. �寞旿霈方��寞�餈𥡝�撉諹�
+      let authResult: { success: boolean, reason?: string } = { success: false, reason: 'Authentication method not supported' };
+      
+      if (clientAuth.auth_method) {
+        const authMethod = await this.getAuthMethodByName(clientAuth.auth_method);
+        if (authMethod && authMethod.is_active) {
+          authResult = await this.verifyByMethod(authMethod, clientAuth, authData);
+        }
+      } else {
+        // 暺䁅恕撖��撉諹�
+        const useSalt = clientAuth.use_salt !== undefined ? clientAuth.use_salt : true;
+        const isValid = this.verifyPassword(authData.password || '', clientAuth.salt, clientAuth.password_hash, useSalt);
+        authResult = {
+          success: isValid,
+          reason: isValid ? 'Authentication successful' : ('Invalid password' as string)
+        };
+      }
+      
+      if (!authResult.success) {
+        return authResult;
+      }
+      
+      // 6. 摨𠉛鍂霈方�蝑𣇉裦
+      if (clientAuth.auth_policy_id) {
+        const policy = await this.getAuthPolicyById(clientAuth.auth_policy_id);
+        if (policy && policy.is_active) {
+          const policyResult = await this.applyAuthPolicy(policy, clientAuth, authData, ipAddress);
+          if (!policyResult.success) {
+            return policyResult;
+          }
+          return { success: true, policy };
+        }
+      }
+      
+      return { success: true };
+    } catch (error) {
+      console.error('Dynamic authentication error:', error);
+      return { success: false, reason: 'Internal authentication error' };
+    }
+  }
+
+  // �寞旿霈方��寞�撉諹�
+  private static async verifyByMethod(authMethod: AuthMethod, clientAuth: ClientAuth, authData: any): Promise<{ success: boolean, reason?: string }> {
+    const config = JSON.parse(authMethod.config);
+    
+    switch (authMethod.method_type) {
+      case 'password':
+        const useSalt = clientAuth.use_salt !== undefined ? clientAuth.use_salt : true;
+        const isValid = this.verifyPassword(authData.password || '', clientAuth.salt, clientAuth.password_hash, useSalt);
+        return {
+          success: isValid,
+          reason: isValid ? 'Authentication successful' : ('Invalid password' as string)
+        };
+      
+      case 'token':
+        if (authData.token) {
+          const token = await this.getClientTokenByValue(authData.token);
+          if (token && token.status === 'active' && token.expires_at > new Date()) {
+            return { success: true };
+          }
+          return { success: false, reason: 'Invalid or expired token' };
+        }
+        return { success: false, reason: 'Token required' };
+      
+      case 'certificate':
+        if (authData.fingerprint) {
+          const certificate = await this.getClientCertificateByFingerprint(authData.fingerprint);
+          if (certificate && certificate.status === 'active' && certificate.expires_at > new Date()) {
+            return { success: true };
+          }
+          return { success: false, reason: 'Invalid or expired certificate' };
+        }
+        return { success: false, reason: 'Certificate fingerprint required' };
+      
+      case 'external':
+        // 憭㚚�霈方�嚗�虾隞交�撅蓥蛹靚�鍂憭㚚�API
+        return { success: false, reason: 'External authentication not implemented' };
+      
+      default:
+        return { success: false, reason: 'Unknown authentication method' };
+    }
+  }
+
+  // 摨𠉛鍂霈方�蝑𣇉裦
+  private static async applyAuthPolicy(policy: AuthPolicy, clientAuth: ClientAuth, authData: any, ipAddress?: string): Promise<{ success: boolean, reason?: string }> {
+    const conditions = JSON.parse(policy.conditions);
+    const actions = JSON.parse(policy.actions);
+    
+    // 璉��交辺隞?
+    for (const condition of conditions) {
+      let conditionMet = false;
+      
+      switch (condition.type) {
+        case 'time_range':
+          if (condition.value && Array.isArray(condition.value)) {
+            conditionMet = this.isTimeAllowed(condition.value);
+          }
+          break;
+        
+        case 'ip_range':
+          if (condition.value && Array.isArray(condition.value) && ipAddress) {
+            conditionMet = this.isIpAllowed(ipAddress, condition.value);
+          }
+          break;
+        
+        case 'device_type':
+          if (condition.value && clientAuth.device_type) {
+            conditionMet = clientAuth.device_type === condition.value;
+          }
+          break;
+        
+        case 'custom':
+          // �臭誑�拙��芸�銋㗇辺隞?
+          conditionMet = true; // 暺䁅恕�朞�
+          break;
+      }
+      
+      // 憒���∩辣銝齿說頞喉��寞旿蝑𣇉裦�滢��喳�蝏𤘪�
+      if (!conditionMet) {
+        if (condition.action === 'deny') {
+          return { success: false, reason: `Policy condition not met: ${condition.type}` };
+        }
+        // 憒���臬�霈豢�雿頣�蝏抒賒璉��乩�銝�銝芣辺隞?
+      }
+    }
+    
+    // 摨𠉛鍂蝑𣇉裦�滢�
+    for (const action of actions) {
+      switch (action.type) {
+        case 'log':
+          // 霈啣��亙�
+          console.log(`Auth policy applied: ${policy.policy_name} for client ${clientAuth.clientid}`);
+          break;
+        
+        case 'notify':
+          // �煾���𡁶䰻
+          console.log(`Notification sent for auth policy: ${policy.policy_name}`);
+          break;
+        
+        case 'custom':
+          // �芸�銋㗇�雿?
+          break;
+      }
+    }
+    
+    return { success: true };
+  }
+
+  // 璉��付P�臬炏�典�霈貉��游�
+  private static isIpAllowed(ip: string, allowedRanges: string[]): boolean {
+    // 蝞��訫��堆�摰鮋�摨𠉛鍂銝剖虾�賡�閬�凒憭齿���P��凒璉��?
+    if (allowedRanges.includes('0.0.0.0/0')) {
+      return true; // ��捂���釟P
+    }
+    
+    if (allowedRanges.includes(ip)) {
+      return true;
+    }
+    
+    // 餈䠷��臭誑瘛餃��游����IP��凒璉��仿�餉�
+    return false;
+  }
+
+  // 璉��亙��齿𧒄�湔糓�血銁��捂��凒�?
+  private static isTimeAllowed(timeRanges: any[]): boolean {
+    if (!timeRanges || timeRanges.length === 0) {
+      return true; // 瘝⊥��園𡢿�𣂼�
+    }
+    
+    const now = new Date();
+    const currentHour = now.getHours();
+    const currentDay = now.getDay(); // 0 = Sunday, 1 = Monday, etc.
+    
+    for (const range of timeRanges) {
+      if (range.days && range.days.includes(currentDay)) {
+        if (range.start_hour !== undefined && range.end_hour !== undefined) {
+          if (currentHour >= range.start_hour && currentHour <= range.end_hour) {
+            return true;
+          }
+        } else {
+          return true; // �芣��交��𣂼�嚗峕瓷�㗇𧒄�湧��?
+        }
+      }
+    }
+    
+    return false;
+  }
+
+  static async findByUsernameAndClientId(username: string, clientid: string): Promise<ClientAuth | null> {
+    const query = 'SELECT * FROM client_auth WHERE username = ? AND clientid = ? AND status = ?';
+    const clients = await executeQuery(query, [username, clientid, 'enabled']);
+    return clients.length > 0 ? clients[0] : null;
+  }
+
+  static async findByUsername(username: string): Promise<ClientAuth | null> {
+    const query = 'SELECT * FROM client_auth WHERE username = ? LIMIT 1';
+    const clients = await executeQuery(query, [username]);
+    return clients.length > 0 ? clients[0] : null;
+  }
+
+  static async updateLastLogin(username: string, clientid: string): Promise<void> {
+    const query = 'UPDATE client_auth SET last_login_at = NOW() WHERE username = ? AND clientid = ?';
+    await executeQuery(query, [username, clientid]);
+  }
+
+  static async logAuthEvent(clientid: string, username: string, operationType: string, result: 'success' | 'failure', reason?: string, ipAddress?: string, topic?: string, authMethod?: string, policyId?: number, executionTime?: number): Promise<void> {
+    const query = `
+      INSERT INTO auth_log (clientid, username, ip_address, operation_type, result, reason, topic, auth_method, auth_policy_id, execution_time_ms)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      clientid,
+      username,
+      ipAddress || null,
+      operationType,
+      result,
+      reason || null,
+      topic || null,
+      authMethod || null,
+      policyId || null,
+      executionTime || null
+    ];
+    
+    await executeQuery(query, values);
+  }
+}

+ 239 - 0
mqtt-vue-dashboard/server/src/models/clientConnection.ts

@@ -0,0 +1,239 @@
+import { executeQuery } from '../config/database';
+
+export interface ClientConnection {
+  id?: number;
+  username?: string;
+  clientid: string;
+  event: string;
+  timestamp?: Date;
+  connected_at?: Date;
+  node: string;
+  peername: string;
+  sockname: string;
+  proto_name: string;
+  proto_ver: number;
+  mountpoint?: string;
+  keepalive: number;
+  is_bridge?: number;
+  clean_start?: number;
+  expiry_interval?: number;
+  rule_id?: string;
+  namespace?: string;
+  client_attrs?: string;
+  conn_props?: string;
+  created_at?: Date;
+  reason?: string;
+  event_time?: Date;
+  connection_duration?: number;
+}
+
+export class ClientConnectionModel {
+  // 获取所有连接记录
+  static async getAll(limit?: number, offset?: number): Promise<ClientConnection[]> {
+    let query = 'SELECT * FROM vw_client_connections ORDER BY timestamp DESC';
+    const params: any[] = [];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 带过滤条件获取连接记录
+  static async getAllWithFilters(
+    limit?: number, 
+    offset?: number, 
+    filters?: { clientid?: string; event?: string; startDate?: string; endDate?: string }
+  ): Promise<ClientConnection[]> {
+    let query = 'SELECT * FROM vw_client_connections WHERE 1=1';
+    const params: any[] = [];
+    
+    // 添加过滤条件
+    if (filters?.clientid) {
+      query += ' AND clientid LIKE ?';
+      params.push(`%${filters.clientid}%`);
+    }
+    
+    if (filters?.event) {
+      query += ' AND event = ?';
+      params.push(filters.event);
+    }
+    
+    if (filters?.startDate) {
+      query += ' AND timestamp >= ?';
+      params.push(new Date(filters.startDate));
+    }
+    
+    if (filters?.endDate) {
+      query += ' AND timestamp <= ?';
+      params.push(new Date(filters.endDate));
+    }
+    
+    query += ' ORDER BY timestamp DESC';
+    
+    // 添加分页
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 获取带过滤条件的连接记录总数
+  static async getCountWithFilters(
+    filters?: { clientid?: string; event?: string; startDate?: string; endDate?: string }
+  ): Promise<number> {
+    let query = 'SELECT COUNT(*) as count FROM vw_client_connections WHERE 1=1';
+    const params: any[] = [];
+    
+    // 添加过滤条件
+    if (filters?.clientid) {
+      query += ' AND clientid LIKE ?';
+      params.push(`%${filters.clientid}%`);
+    }
+    
+    if (filters?.event) {
+      query += ' AND event = ?';
+      params.push(filters.event);
+    }
+    
+    if (filters?.startDate) {
+      query += ' AND timestamp >= ?';
+      params.push(new Date(filters.startDate));
+    }
+    
+    if (filters?.endDate) {
+      query += ' AND timestamp <= ?';
+      params.push(new Date(filters.endDate));
+    }
+    
+    const result = await executeQuery(query, params);
+    return result[0].count;
+  }
+
+  // 获取连接记录总数
+  static async getCount(): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM vw_client_connections';
+    const result = await executeQuery(query);
+    return result[0].count;
+  }
+
+  // 根据客户端ID获取连接记录
+  static async getByClientId(clientid: string, limit?: number): Promise<ClientConnection[]> {
+    let query = 'SELECT * FROM vw_client_connections WHERE clientid = ? ORDER BY timestamp DESC';
+    const params: any[] = [clientid];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据事件类型获取连接记录
+  static async getByEvent(event: string, limit?: number): Promise<ClientConnection[]> {
+    let query = 'SELECT * FROM vw_client_connections WHERE event = ? ORDER BY timestamp DESC';
+    const params: any[] = [event];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 获取指定时间范围内的连接记录
+  static async getByTimeRange(startTime: Date, endTime: Date): Promise<ClientConnection[]> {
+    const query = 'SELECT * FROM vw_client_connections WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp DESC';
+    return await executeQuery(query, [startTime, endTime]);
+  }
+
+  // 获取各事件类型统计
+  static async getEventStats(): Promise<any[]> {
+    const query = 'SELECT event, COUNT(*) as count FROM vw_client_connections GROUP BY event';
+    return await executeQuery(query);
+  }
+
+  // 获取连接和断开连接统计
+  static async getConnectionStats(): Promise<any[]> {
+    const query = `
+      SELECT 
+        CASE 
+          WHEN event = 'client.connected' THEN 'connected'
+          WHEN event = 'client.disconnected' THEN 'disconnected'
+          ELSE 'other'
+        END as connection_type,
+        COUNT(*) as count
+      FROM vw_client_connections 
+      WHERE event IN ('client.connected', 'client.disconnected')
+      GROUP BY connection_type
+    `;
+    return await executeQuery(query);
+  }
+
+  static async create(connectionData: Omit<ClientConnection, 'id' | 'created_at'>): Promise<ClientConnection> {
+    const query = `
+      INSERT INTO client_connections (username, clientid, event, timestamp, connected_at, node, peername, sockname, proto_name, proto_ver, keepalive, clean_start, reason, connection_duration)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+    const values = [
+      connectionData.username || null,
+      connectionData.clientid,
+      connectionData.event,
+      connectionData.timestamp || new Date(),
+      connectionData.connected_at || null,
+      connectionData.node,
+      connectionData.peername || '',
+      connectionData.sockname || '',
+      connectionData.proto_name || 'MQTT',
+      connectionData.proto_ver || 4,
+      connectionData.keepalive || 60,
+      connectionData.clean_start ?? 1,
+      connectionData.reason || null,
+      connectionData.connection_duration || null
+    ];
+    const result = await executeQuery(query, values) as any;
+    return { ...connectionData, id: result.insertId, created_at: new Date() };
+  }
+
+  // 根据ID获取连接记录
+  static async getById(id: number): Promise<ClientConnection | null> {
+    const query = 'SELECT * FROM vw_client_connections WHERE id = ?';
+    const results = await executeQuery(query, [id]) as ClientConnection[];
+    return results.length > 0 ? results[0] : null;
+  }
+
+  /**
+   * 获取每日连接统计
+   */
+  static async getDailyStats(days: number = 7): Promise<any[]> {
+    const query = `
+      SELECT 
+        DATE(timestamp) as date,
+        COUNT(*) as total_connections,
+        SUM(CASE WHEN event = 'connect' THEN 1 ELSE 0 END) as connections,
+        SUM(CASE WHEN event = 'disconnect' THEN 1 ELSE 0 END) as disconnections
+      FROM vw_client_connections 
+      WHERE timestamp >= DATE_SUB(NOW(), INTERVAL ${days} DAY)
+      GROUP BY DATE(timestamp)
+      ORDER BY date DESC
+    `;
+    
+    return await executeQuery(query);
+  }
+}

+ 269 - 0
mqtt-vue-dashboard/server/src/models/device.ts

@@ -0,0 +1,269 @@
+import { executeQuery, config } from '../config/database';
+
+export interface Device {
+  id?: number;
+  clientid: string;
+  device_name?: string;
+  username?: string;
+  firmware_version?: string;
+  device_ip_port?: string;
+  last_ip_port?: string;
+  status?: 'online' | 'offline' | 'unknown';
+  last_event_time?: Date;
+  last_online_time?: Date;
+  last_offline_time?: Date;
+  online_duration?: number;
+  connect_count?: number;
+  rssi?: number;
+  created_at?: Date;
+  updated_at?: Date;
+}
+
+export class DeviceModel {
+  // 获取所有设备
+  static async getAll(limit?: number, offset?: number): Promise<Device[]> {
+    let query = 'SELECT * FROM devices ORDER BY updated_at DESC';
+    const params: any[] = [];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据状态获取设备
+  static async getByStatus(status: string): Promise<Device[]> {
+    const query = 'SELECT * FROM devices WHERE status = ? ORDER BY updated_at DESC';
+    return await executeQuery(query, [status]);
+  }
+
+  // 根据客户端ID获取设备
+  static async getByClientId(clientid: string): Promise<Device | null> {
+    const query = 'SELECT * FROM devices WHERE clientid = ?';
+    const devices = await executeQuery(query, [clientid]);
+    return devices.length > 0 ? devices[0] : null;
+  }
+
+  // 根据搜索条件获取设备
+  static async getBySearch(searchTerm: string): Promise<Device[]> {
+    const query = 'SELECT * FROM devices WHERE clientid LIKE ? OR device_name LIKE ? ORDER BY updated_at DESC';
+    const searchPattern = `%${searchTerm}%`;
+    return await executeQuery(query, [searchPattern, searchPattern]);
+  }
+
+  // 根据状态获取设备总数
+  static async getCountByStatus(status: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM devices WHERE status = ?';
+    const result = await executeQuery(query, [status]);
+    return result[0].count;
+  }
+
+  // 根据搜索条件获取设备总数
+  static async getCountBySearch(searchTerm: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM devices WHERE clientid LIKE ? OR device_name LIKE ?';
+    const searchPattern = `%${searchTerm}%`;
+    const result = await executeQuery(query, [searchPattern, searchPattern]);
+    return result[0].count;
+  }
+
+  // 获取设备总数
+  static async getCount(): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM devices';
+    const result = await executeQuery(query);
+    return result[0].count;
+  }
+
+  /**
+   * 获取设备状态统计
+   */
+  static async getStatusStats(): Promise<any[]> {
+    const connection = await config.getConnection().getConnection();
+    try {
+      const [rows] = await connection.execute(
+        `SELECT 
+          status,
+          COUNT(*) as count
+        FROM devices
+        GROUP BY status`
+      );
+      
+      return rows as any[];
+    } finally {
+      connection.release();
+    }
+  }
+
+  /**
+   * 获取设备统计信息
+   */
+  static async getDeviceStats(): Promise<any> {
+    const connection = await config.getConnection().getConnection();
+    try {
+      // 获取设备总数
+      const [totalResult] = await connection.execute('SELECT COUNT(*) as total FROM devices');
+      const total = (totalResult as any[])[0].total;
+      
+      // 获取在线设备数
+      const [onlineResult] = await connection.execute(
+        "SELECT COUNT(*) as online FROM devices WHERE status = 'online'"
+      );
+      const online = (onlineResult as any[])[0].online;
+      
+      // 获取离线设备数
+      const [offlineResult] = await connection.execute(
+        "SELECT COUNT(*) as offline FROM devices WHERE status = 'offline'"
+      );
+      const offline = (offlineResult as any[])[0].offline;
+      
+      return {
+        total,
+        online,
+        offline,
+        onlineRate: total > 0 ? Math.round((online / total) * 100) : 0
+      };
+    } finally {
+      connection.release();
+    }
+  }
+
+  // 创建新设备
+  static async create(deviceData: Omit<Device, 'id' | 'created_at' | 'updated_at'>): Promise<Device> {
+    const query = `
+      INSERT INTO devices (clientid, device_name, username, firmware_version, device_ip_port, last_ip_port, status, last_event_time, last_online_time, last_offline_time, online_duration, connect_count)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      deviceData.clientid,
+      deviceData.device_name || null,
+      deviceData.username || null,
+      deviceData.firmware_version || null,
+      deviceData.device_ip_port || null,
+      deviceData.last_ip_port || null,
+      deviceData.status || 'unknown',
+      deviceData.last_event_time || null,
+      deviceData.last_online_time || null,
+      deviceData.last_offline_time || null,
+      deviceData.online_duration || 0,
+      deviceData.connect_count || 0
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    // 获取插入的设备ID
+    const insertId = result.insertId;
+    
+    // 返回创建的设备
+    return this.getByClientId(deviceData.clientid) as Promise<Device>;
+  }
+
+  // 更新设备信息
+  static async update(clientId: string, updateData: Partial<Omit<Device, 'id' | 'created_at' | 'clientid'>>): Promise<boolean> {
+    console.log('DeviceModel.update 被调用,clientId:', clientId, 'updateData:', updateData);
+    
+    const fields = [];
+    const values = [];
+    
+    // 动态构建更新字段
+    if (updateData.device_name !== undefined) {
+      fields.push('device_name = ?');
+      values.push(updateData.device_name);
+      console.log('添加 device_name 字段到更新列表:', updateData.device_name);
+    }
+    
+    if (updateData.username !== undefined) {
+      fields.push('username = ?');
+      values.push(updateData.username);
+    }
+    
+    if (updateData.firmware_version !== undefined) {
+      fields.push('firmware_version = ?');
+      values.push(updateData.firmware_version);
+    }
+    
+    if (updateData.device_ip_port !== undefined) {
+      fields.push('device_ip_port = ?');
+      values.push(updateData.device_ip_port);
+    }
+    
+    if (updateData.last_ip_port !== undefined) {
+      fields.push('last_ip_port = ?');
+      values.push(updateData.last_ip_port);
+    }
+    
+    if (updateData.status !== undefined) {
+      fields.push('status = ?');
+      values.push(updateData.status);
+    }
+    
+    if (updateData.last_event_time !== undefined) {
+      fields.push('last_event_time = ?');
+      values.push(updateData.last_event_time);
+    }
+    
+    if (updateData.last_online_time !== undefined) {
+      fields.push('last_online_time = ?');
+      values.push(updateData.last_online_time);
+    }
+    
+    if (updateData.last_offline_time !== undefined) {
+      fields.push('last_offline_time = ?');
+      values.push(updateData.last_offline_time);
+    }
+    
+    if (updateData.online_duration !== undefined) {
+      fields.push('online_duration = ?');
+      values.push(updateData.online_duration);
+    }
+    
+    if (updateData.connect_count !== undefined) {
+      fields.push('connect_count = ?');
+      values.push(updateData.connect_count);
+    }
+    
+    if (updateData.rssi !== undefined) {
+      fields.push('rssi = ?');
+      values.push(updateData.rssi);
+    }
+    
+    // 添加updated_at字段
+    fields.push('updated_at = NOW()');
+    
+    // 添加WHERE条件参数
+    values.push(clientId);
+    
+    const query = `
+      UPDATE devices 
+      SET ${fields.join(', ')}
+      WHERE clientid = ?
+    `;
+    
+    console.log('执行更新SQL:', query);
+    console.log('参数值:', values);
+    
+    const result = await executeQuery(query, values) as any;
+    
+    console.log('更新结果:', result);
+    
+    return result.affectedRows > 0;
+  }
+
+  static async updateOnlineDuration(clientId: string, duration: number): Promise<boolean> {
+    const query = 'UPDATE devices SET online_duration = online_duration + ? WHERE clientid = ?';
+    const result = await executeQuery(query, [duration, clientId]) as any;
+    return result.affectedRows > 0;
+  }
+
+  static async deleteByClientId(clientId: string): Promise<boolean> {
+    const query = 'DELETE FROM devices WHERE clientid = ?';
+    const result = await executeQuery(query, [clientId]) as any;
+    return result.affectedRows > 0;
+  }
+}

+ 437 - 0
mqtt-vue-dashboard/server/src/models/deviceBinding.ts

@@ -0,0 +1,437 @@
+import { executeQuery } from '../config/database';
+
+export interface DeviceBinding {
+  id?: number;
+  device_clientid: string;  // 关联devices表的clientid
+  room_id: number;          // 关联rooms表的id
+  device_name?: string;     // 在房间中的显示名称(可选,覆盖原设备名)
+  device_type?: string;     // 在房间中的设备类型(可选)
+  properties?: string;      // JSON格式的额外属性
+  created_at?: Date;
+  updated_at?: Date;
+}
+
+export class DeviceBindingModel {
+  // 获取所有设备绑定关系
+  static async getAll(): Promise<DeviceBinding[]> {
+    const query = `
+      SELECT db.*, d.device_name as original_device_name, d.status as device_status,
+             r.name as room_name, r.room_number, r.floor_id
+      FROM device_bindings db
+      LEFT JOIN devices d ON db.device_clientid = d.clientid
+      LEFT JOIN rooms r ON db.room_id = r.id
+      ORDER BY r.floor_id, r.room_number, db.device_name
+    `;
+    
+    return await executeQuery(query);
+  }
+
+  // 根据房间ID获取绑定的设备
+  static async getByRoomId(roomId: number): Promise<DeviceBinding[]> {
+    const bindings = await executeQuery(`
+      SELECT 
+        db.id,
+        db.device_clientid as clientid,
+        db.room_id,
+        COALESCE(db.device_name, d.device_name, d.clientid) as device_name,
+        db.device_type,
+        db.properties,
+        db.created_at,
+        db.updated_at,
+        d.status as device_status,
+        d.firmware_version, 
+        d.last_ip_port, 
+        d.last_online_time, 
+        d.last_offline_time,
+        d.rssi
+      FROM device_bindings db
+      LEFT JOIN devices d ON db.device_clientid COLLATE utf8mb4_unicode_ci = d.clientid COLLATE utf8mb4_unicode_ci
+      WHERE db.room_id = ? COLLATE utf8mb4_unicode_ci
+      ORDER BY db.device_name
+    `, [roomId]);
+    
+    // 为每个设备添加最新的传感器数据
+    for (const binding of bindings) {
+      const deviceId = binding.clientid;
+      
+      // 获取设备的最新温度数据
+      const tempData = await executeQuery(`
+        SELECT value, timestamp
+        FROM sensor_data
+        WHERE device_id = ? AND data_type = 'temperature'
+        ORDER BY timestamp DESC
+        LIMIT 1
+      `, [deviceId]);
+      
+      // 获取设备的最新继电器状态数据
+      const relayData = await executeQuery(`
+        SELECT value, timestamp
+        FROM sensor_data
+        WHERE device_id = ? AND data_type = 'relay'
+        ORDER BY timestamp DESC
+        LIMIT 1
+      `, [deviceId]);
+      
+      // 将传感器数据合并到设备对象中
+      if (tempData.length > 0) {
+        binding.temperature = tempData[0].value;
+        binding.temperature_timestamp = tempData[0].timestamp;
+      }
+      
+      if (relayData.length > 0) {
+        binding.relay_status = relayData[0].value === 'ON' ? true : false;
+        binding.relay_timestamp = relayData[0].timestamp;
+      }
+      
+      // 设置value字段,兼容前端现有的逻辑
+      binding.value = {
+        temperature: tempData.length > 0 ? tempData[0].value : null,
+        relay: relayData.length > 0 ? (relayData[0].value === 'ON' ? true : false) : false
+      };
+    }
+    
+    return bindings;
+  }
+
+  // 根据设备客户端ID获取绑定关系
+  static async getByDeviceClientId(clientId: string): Promise<DeviceBinding | null> {
+    const query = `
+      SELECT db.*, d.device_name as original_device_name, d.status as device_status,
+             r.name as room_name, r.room_number, r.floor_id
+      FROM device_bindings db
+      LEFT JOIN devices d ON db.device_clientid = d.clientid
+      LEFT JOIN rooms r ON db.room_id = r.id
+      WHERE db.device_clientid = ?
+    `;
+    
+    const bindings = await executeQuery(query, [clientId]);
+    return bindings.length > 0 ? bindings[0] : null;
+  }
+
+  // 绑定设备到房间(简化版本,只需要device_clientid和room_id)
+  static async bindDevice(deviceClientId: string, roomId: number): Promise<DeviceBinding> {
+    // 先检查设备是否已绑定
+    const existingBinding = await this.getByDeviceClientId(deviceClientId);
+    if (existingBinding) {
+      // 如果已绑定,更新绑定关系
+      return this.updateBinding(existingBinding.id as number, { room_id: roomId });
+    }
+    
+    const query = `
+      INSERT INTO device_bindings (device_clientid, room_id)
+      VALUES (?, ?)
+    `;
+    
+    const values = [deviceClientId, roomId];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    // 返回创建的绑定关系
+    return this.getByDeviceClientId(deviceClientId) as Promise<DeviceBinding>;
+  }
+
+  // 绑定设备到房间(完整版本,支持更多参数)
+  static async bindDeviceWithDetails(bindingData: Omit<DeviceBinding, 'id' | 'created_at' | 'updated_at'>): Promise<DeviceBinding> {
+    // 先检查设备是否已绑定
+    const existingBinding = await this.getByDeviceClientId(bindingData.device_clientid);
+    if (existingBinding) {
+      // 如果已绑定,更新绑定关系
+      return this.updateBinding(existingBinding.id as number, bindingData);
+    }
+    
+    const query = `
+      INSERT INTO device_bindings (device_clientid, room_id, device_name, device_type, properties)
+      VALUES (?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      bindingData.device_clientid,
+      bindingData.room_id,
+      bindingData.device_name || null,
+      bindingData.device_type || null,
+      bindingData.properties ? JSON.stringify(bindingData.properties) : null
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    // 返回创建的绑定关系
+    return this.getByDeviceClientId(bindingData.device_clientid) as Promise<DeviceBinding>;
+  }
+
+  // 解绑设备
+  static async unbindDevice(deviceClientId: string): Promise<boolean> {
+    const query = 'DELETE FROM device_bindings WHERE device_clientid = ?';
+    const result = await executeQuery(query, [deviceClientId]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // 更新绑定关系
+  static async updateBinding(id: number, updateData: Partial<Omit<DeviceBinding, 'id' | 'created_at' | 'updated_at'>>): Promise<DeviceBinding> {
+    const fields = [];
+    const values = [];
+    
+    if (updateData.room_id !== undefined) {
+      fields.push('room_id = ?');
+      values.push(updateData.room_id);
+    }
+    
+    if (updateData.device_name !== undefined) {
+      fields.push('device_name = ?');
+      values.push(updateData.device_name);
+    }
+    
+    if (updateData.device_type !== undefined) {
+      fields.push('device_type = ?');
+      values.push(updateData.device_type);
+    }
+    
+    if (updateData.properties !== undefined) {
+      fields.push('properties = ?');
+      values.push(updateData.properties ? JSON.stringify(updateData.properties) : null);
+    }
+    
+    fields.push('updated_at = NOW()');
+    values.push(id);
+    
+    const query = `
+      UPDATE device_bindings 
+      SET ${fields.join(', ')}
+      WHERE id = ?
+    `;
+    
+    await executeQuery(query, values);
+    
+    // 获取设备客户端ID
+    const bindingQuery = 'SELECT device_clientid FROM device_bindings WHERE id = ?';
+    const binding = await executeQuery(bindingQuery, [id]) as any[];
+    
+    if (binding.length > 0) {
+      return this.getByDeviceClientId(binding[0].device_clientid) as Promise<DeviceBinding>;
+    }
+    
+    throw new Error('Failed to retrieve updated binding');
+  }
+
+  // 获取可绑定的设备(未绑定到任何房间的设备)
+  static async getAvailableDevices(): Promise<any[]> {
+    const query = `
+      SELECT d.* FROM devices d
+      LEFT JOIN device_bindings db ON d.clientid COLLATE utf8mb4_unicode_ci = db.device_clientid COLLATE utf8mb4_unicode_ci
+      WHERE db.device_clientid IS NULL
+      ORDER BY d.device_name, d.clientid
+    `;
+    
+    return await executeQuery(query);
+  }
+
+  // 获取房间设备详情(合并devices表和绑定关系)
+  static async getRoomDevicesWithDetails(roomId: number): Promise<any[]> {
+    const query = `
+      SELECT 
+        d.clientid,
+        d.device_name as original_device_name,
+        d.status as device_status,
+        d.firmware_version,
+        d.last_ip_port,
+        d.last_online_time,
+        d.last_offline_time,
+        d.online_duration,
+        d.connect_count,
+        d.rssi,
+        db.device_name,
+        db.device_type,
+        db.properties,
+        r.name as room_name,
+        r.floor_id
+      FROM device_bindings db
+      LEFT JOIN devices d ON db.device_clientid COLLATE utf8mb4_unicode_ci = d.clientid COLLATE utf8mb4_unicode_ci
+      LEFT JOIN rooms r ON db.room_id = r.id
+      WHERE db.room_id = ?
+      ORDER BY db.device_name
+    `;
+    
+    return await executeQuery(query, [roomId]);
+  }
+
+  // 获取房间中的设备列表(用于设备控制器)
+  static async getDevicesByRoomId(roomId: number): Promise<any[]> {
+    const query = `
+      SELECT 
+        d.clientid,
+        d.device_name,
+        d.username,
+        d.status,
+        d.firmware_version,
+        d.device_ip_port,
+        d.last_ip_port,
+        d.last_event_time,
+        d.last_online_time,
+        d.last_offline_time,
+        d.online_duration,
+        d.connect_count,
+        d.rssi,
+        d.created_at,
+        d.updated_at,
+        db.device_name as room_device_name,
+        db.device_type as room_device_type,
+        db.properties
+      FROM device_bindings db
+      LEFT JOIN devices d ON db.device_clientid COLLATE utf8mb4_unicode_ci = d.clientid COLLATE utf8mb4_unicode_ci
+      WHERE db.room_id = ?
+      ORDER BY d.device_name
+    `;
+    
+    return await executeQuery(query, [roomId]);
+  }
+
+  // 获取未绑定的设备列表(用于设备控制器)
+  static async getUnboundDevices(): Promise<any[]> {
+    const query = `
+      SELECT 
+        d.clientid,
+        d.device_name,
+        d.username,
+        d.status,
+        d.firmware_version,
+        d.device_ip_port,
+        d.last_ip_port,
+        d.last_event_time,
+        d.last_online_time,
+        d.last_offline_time,
+        d.online_duration,
+        d.connect_count,
+        d.created_at,
+        d.updated_at
+      FROM devices d
+      LEFT JOIN device_bindings db ON d.clientid COLLATE utf8mb4_unicode_ci = db.device_clientid COLLATE utf8mb4_unicode_ci
+      WHERE db.device_clientid IS NULL
+      ORDER BY d.device_name, d.clientid
+    `;
+    
+    return await executeQuery(query);
+  }
+
+  // 获取所有设备及其绑定状态,支持分页、筛选和搜索
+  static async getAllDevicesWithBindingStatus(
+    page: number = 1,
+    pageSize: number = 10,
+    filters: { status?: string, room_id?: number, search?: string } = {}
+  ): Promise<{ devices: any[], total: number }> {
+    const offset = (page - 1) * pageSize;
+    const conditions: string[] = [];
+    const params: any[] = [];
+
+    // 添加筛选条件
+    if (filters.status) {
+      conditions.push('d.status = ?');
+      params.push(filters.status);
+    }
+
+    if (filters.room_id) {
+      conditions.push('db.room_id = ?');
+      params.push(filters.room_id);
+    }
+
+    if (filters.search) {
+      const searchTerm = `%${filters.search}%`;
+      conditions.push('(d.clientid LIKE ? OR d.device_name LIKE ? OR d.username LIKE ? OR r.name LIKE ?)');
+      params.push(searchTerm, searchTerm, searchTerm, searchTerm);
+    }
+
+    const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
+
+    // 主查询
+    const query = `
+      SELECT 
+        d.*,
+        db.id as binding_id,
+        db.room_id,
+        db.device_name as room_device_name,
+        db.device_type as room_device_type,
+        r.name as room_name,
+        r.room_number,
+        r.floor_id
+      FROM devices d
+      LEFT JOIN device_bindings db ON d.clientid COLLATE utf8mb4_unicode_ci = db.device_clientid COLLATE utf8mb4_unicode_ci
+      LEFT JOIN rooms r ON db.room_id = r.id
+      ${whereClause}
+      ORDER BY d.device_name, d.clientid
+      LIMIT ? OFFSET ?
+    `;
+    params.push(pageSize, offset);
+
+    // 计算总数的查询
+    const countQuery = `
+      SELECT COUNT(DISTINCT d.id) as total
+      FROM devices d
+      LEFT JOIN device_bindings db ON d.clientid COLLATE utf8mb4_unicode_ci = db.device_clientid COLLATE utf8mb4_unicode_ci
+      LEFT JOIN rooms r ON db.room_id = r.id
+      ${whereClause}
+    `;
+    const countParams = conditions.length > 0 ? params.slice(0, -2) : [];
+
+    const [devices, countResult] = await Promise.all([
+      executeQuery(query, params),
+      executeQuery(countQuery, countParams)
+    ]);
+
+    // 为每个设备添加最新的传感器数据
+    for (const device of devices) {
+      const deviceId = device.clientid;
+      
+      // 获取设备的最新温度数据
+      const tempData = await executeQuery(`
+        SELECT value, timestamp
+        FROM sensor_data
+        WHERE device_id = ? AND data_type = 'temperature'
+        ORDER BY timestamp DESC
+        LIMIT 1
+      `, [deviceId]);
+      
+      // 获取设备的最新继电器状态数据
+      const relayData = await executeQuery(`
+        SELECT value, timestamp
+        FROM sensor_data
+        WHERE device_id = ? AND data_type = 'relay'
+        ORDER BY timestamp DESC
+        LIMIT 1
+      `, [deviceId]);
+      
+      // 将传感器数据合并到设备对象中
+      if (tempData.length > 0) {
+        device.temperature = tempData[0].value;
+        device.temperature_timestamp = tempData[0].timestamp;
+      }
+      
+      if (relayData.length > 0) {
+        device.relay_status = relayData[0].value === 'ON' ? true : false;
+        device.relay_timestamp = relayData[0].timestamp;
+      }
+      
+      // 设置value字段,兼容前端现有的逻辑
+      device.value = {
+        temperature: tempData.length > 0 ? tempData[0].value : null,
+        relay: relayData.length > 0 ? (relayData[0].value === 'ON' ? true : false) : false
+      };
+    }
+
+    return {
+      devices,
+      total: countResult[0].total
+    };
+  }
+
+  // 更新设备值
+  static async updateDeviceValue(deviceClientId: string, value: string): Promise<boolean> {
+    // 注意:device_bindings表本身不存储设备值,值存储在devices表中
+    // 这个方法只是为了保持API一致性
+    const query = `
+      UPDATE devices 
+      SET value = ? 
+      WHERE clientid = ?
+    `;
+    
+    const result = await executeQuery(query, [value, deviceClientId]) as any;
+    return result.affectedRows > 0;
+  }
+}

+ 191 - 0
mqtt-vue-dashboard/server/src/models/deviceLog.ts

@@ -0,0 +1,191 @@
+import { executeQuery } from '../config/database';
+
+export interface DeviceLog {
+  id?: number;
+  clientid: string;
+  event_type: 'connect' | 'disconnect' | 'publish' | 'subscribe' | 'unsubscribe';
+  event_time: Date;
+  topic?: string;
+  payload?: string;
+  qos?: number;
+  username?: string;
+  peername?: string;
+  proto_ver?: number;
+  node?: string;
+  details?: string;
+  created_at?: Date;
+}
+
+export class DeviceLogModel {
+  static async getByClientId(
+    clientid: string,
+    filters?: {
+      event_type?: string;
+      start_time?: Date;
+      end_time?: Date;
+    },
+    limit?: number,
+    offset?: number
+  ): Promise<{ logs: DeviceLog[]; total: number }> {
+    const params: any[] = [clientid];
+    let whereClause = 'WHERE clientid = ?';
+
+    if (filters?.event_type) {
+      whereClause += ' AND event_type = ?';
+      params.push(filters.event_type);
+    }
+
+    if (filters?.start_time) {
+      whereClause += ' AND event_time >= ?';
+      params.push(filters.start_time);
+    }
+
+    if (filters?.end_time) {
+      whereClause += ' AND event_time <= ?';
+      params.push(filters.end_time);
+    }
+
+    const countQuery = `SELECT COUNT(*) as total FROM vw_device_logs ${whereClause}`;
+    const countResult = await executeQuery(countQuery, params);
+    const total = countResult[0].total;
+
+    let query = `SELECT * FROM vw_device_logs ${whereClause} ORDER BY event_time DESC`;
+
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+
+    const logs = await executeQuery(query, params);
+
+    return { logs, total };
+  }
+
+  static async getEventTypesStats(clientid: string): Promise<any[]> {
+    const query = `
+      SELECT 
+        event_type,
+        COUNT(*) as count
+      FROM vw_device_logs
+      WHERE clientid = ?
+      GROUP BY event_type
+    `;
+    return await executeQuery(query, [clientid]);
+  }
+
+  static async getRecentLogs(limit: number = 50): Promise<DeviceLog[]> {
+    const query = `SELECT * FROM vw_device_logs ORDER BY event_time DESC LIMIT ?`;
+    return await executeQuery(query, [limit]);
+  }
+
+  static async getConnectDisconnectLogs(
+    clientid: string,
+    limit?: number
+  ): Promise<DeviceLog[]> {
+    let query = `
+      SELECT 
+        id,
+        clientid,
+        event_type,
+        event_time,
+        username,
+        peername,
+        proto_ver,
+        node,
+        details
+      FROM vw_device_logs
+      WHERE clientid = ? 
+      AND event_type IN ('connect', 'disconnect')
+      ORDER BY event_time DESC
+    `;
+    const params: any[] = [clientid];
+
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+
+    return await executeQuery(query, params);
+  }
+
+  static async getPublishLogs(
+    clientid: string,
+    limit?: number
+  ): Promise<DeviceLog[]> {
+    let query = `
+      SELECT 
+        id,
+        clientid,
+        event_type,
+        event_time,
+        topic,
+        payload,
+        qos,
+        node
+      FROM vw_device_logs
+      WHERE clientid = ? 
+      AND event_type = 'publish'
+      ORDER BY event_time DESC
+    `;
+    const params: any[] = [clientid];
+
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+
+    return await executeQuery(query, params);
+  }
+
+  static async getSubscribeLogs(
+    clientid: string,
+    limit?: number
+  ): Promise<DeviceLog[]> {
+    let query = `
+      SELECT 
+        id,
+        clientid,
+        event_type,
+        event_time,
+        topic,
+        qos,
+        node
+      FROM vw_device_logs
+      WHERE clientid = ? 
+      AND event_type IN ('subscribe', 'unsubscribe')
+      ORDER BY event_time DESC
+    `;
+    const params: any[] = [clientid];
+
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+
+    return await executeQuery(query, params);
+  }
+
+  static async getDailyStats(clientid: string, days: number = 7): Promise<any[]> {
+    const query = `
+      SELECT 
+        DATE(event_time) as date,
+        COUNT(*) as total_events,
+        SUM(CASE WHEN event_type = 'connect' THEN 1 ELSE 0 END) as connects,
+        SUM(CASE WHEN event_type = 'disconnect' THEN 1 ELSE 0 END) as disconnects,
+        SUM(CASE WHEN event_type = 'publish' THEN 1 ELSE 0 END) as publishes,
+        SUM(CASE WHEN event_type = 'subscribe' THEN 1 ELSE 0 END) as subscribes,
+        SUM(CASE WHEN event_type = 'unsubscribe' THEN 1 ELSE 0 END) as unsubscribes
+      FROM vw_device_logs
+      WHERE clientid = ? 
+      AND event_time >= DATE_SUB(NOW(), INTERVAL ? DAY)
+      GROUP BY DATE(event_time)
+      ORDER BY date DESC
+    `;
+    return await executeQuery(query, [clientid, days]);
+  }
+}

+ 97 - 0
mqtt-vue-dashboard/server/src/models/firmware.ts

@@ -0,0 +1,97 @@
+import { executeQuery, config } from '../config/database';
+
+export interface FirmwareFile {
+  id?: number;
+  version: string;
+  filename: string;
+  filepath: string;
+  filesize: number;
+  md5sum: string;
+  description?: string;
+  status?: 'active' | 'inactive';
+  created_by?: string;
+  created_at?: Date;
+  updated_at?: Date;
+}
+
+export class FirmwareFileModel {
+  // 创建固件文件记录
+  static async create(firmwareData: Omit<FirmwareFile, 'id' | 'created_at' | 'updated_at'>): Promise<FirmwareFile> {
+    const query = `
+      INSERT INTO firmware_files (version, filename, filepath, filesize, md5sum, description, status, created_by)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      firmwareData.version,
+      firmwareData.filename,
+      firmwareData.filepath,
+      firmwareData.filesize,
+      firmwareData.md5sum,
+      firmwareData.description || null,
+      firmwareData.status || 'active',
+      firmwareData.created_by || null
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    const insertId = result.insertId;
+    
+    // 返回创建的固件文件记录
+    return this.getById(insertId) as Promise<FirmwareFile>;
+  }
+
+  // 根据ID获取固件文件
+  static async getById(id: number): Promise<FirmwareFile | null> {
+    const query = 'SELECT * FROM firmware_files WHERE id = ?';
+    const firmwareFiles = await executeQuery(query, [id]);
+    return firmwareFiles.length > 0 ? firmwareFiles[0] : null;
+  }
+
+  // 获取所有固件文件
+  static async getAll(): Promise<FirmwareFile[]> {
+    const query = 'SELECT * FROM firmware_files ORDER BY version DESC';
+    return await executeQuery(query);
+  }
+
+  // 根据状态获取固件文件
+  static async getByStatus(status: string): Promise<FirmwareFile[]> {
+    const query = 'SELECT * FROM firmware_files WHERE status = ? ORDER BY version DESC';
+    return await executeQuery(query, [status]);
+  }
+
+  // 更新固件文件状态
+  static async updateStatus(id: number, status: 'active' | 'inactive'): Promise<boolean> {
+    const query = 'UPDATE firmware_files SET status = ? WHERE id = ?';
+    const result = await executeQuery(query, [status, id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // 删除固件文件
+  static async delete(id: number): Promise<boolean> {
+    const query = 'DELETE FROM firmware_files WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // 创建固件文件表(如果不存在)
+  static async createTable(): Promise<void> {
+    const query = `
+      CREATE TABLE IF NOT EXISTS firmware_files (
+        id INT AUTO_INCREMENT PRIMARY KEY,
+        version VARCHAR(50) NOT NULL UNIQUE,
+        filename VARCHAR(255) NOT NULL,
+        filepath VARCHAR(255) NOT NULL,
+        filesize BIGINT NOT NULL,
+        md5sum VARCHAR(32) NOT NULL,
+        description TEXT,
+        status ENUM('active', 'inactive') DEFAULT 'active',
+        created_by VARCHAR(50),
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+        INDEX idx_version (version),
+        INDEX idx_status (status)
+      )
+    `;
+    await executeQuery(query);
+  }
+}

+ 207 - 0
mqtt-vue-dashboard/server/src/models/mqttMessage.ts

@@ -0,0 +1,207 @@
+import { executeQuery } from '../config/database';
+
+export interface MqttMessage {
+  id?: number;
+  clientid: string;
+  topic: string;
+  payload?: string;
+  qos: number;
+  retain: number;
+  message_id?: string;
+  message_type: 'publish' | 'subscribe' | 'unsubscribe';
+  timestamp: number;
+  node: string;
+  username?: string;
+  proto_ver: number;
+  payload_format?: string;
+  created_at?: Date;
+  message_time: Date;
+}
+
+export class MqttMessageModel {
+  // 获取所有消息
+  static async getAll(limit?: number, offset?: number): Promise<MqttMessage[]> {
+    let query = 'SELECT * FROM mqtt_messages ORDER BY timestamp DESC';
+    const params: any[] = [];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据客户端ID获取消息
+  static async getByClientId(clientid: string, limit?: number): Promise<MqttMessage[]> {
+    let query = 'SELECT * FROM mqtt_messages WHERE clientid = ? ORDER BY timestamp DESC';
+    const params: any[] = [clientid];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据主题获取消息
+  static async getByTopic(topic: string, limit?: number): Promise<MqttMessage[]> {
+    let query = 'SELECT * FROM mqtt_messages WHERE topic LIKE ? ORDER BY timestamp DESC';
+    const params: any[] = [`%${topic}%`];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据消息类型获取消息
+  static async getByType(messageType: string, limit?: number): Promise<MqttMessage[]> {
+    let query = 'SELECT * FROM mqtt_messages WHERE message_type = ? ORDER BY timestamp DESC';
+    const params: any[] = [messageType];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 获取指定时间范围内的消息
+  static async getByTimeRange(startTime: Date, endTime: Date): Promise<MqttMessage[]> {
+    const startTimestamp = startTime.getTime();
+    const endTimestamp = endTime.getTime();
+    
+    const query = 'SELECT * FROM mqtt_messages WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp DESC';
+    return await executeQuery(query, [startTimestamp, endTimestamp]);
+  }
+
+  // 获取消息总数
+  static async getCount(): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM mqtt_messages';
+    const result = await executeQuery(query);
+    return result[0].count;
+  }
+
+  // 获取各消息类型统计
+  static async getTypeStats(): Promise<any[]> {
+    const query = 'SELECT message_type, COUNT(*) as count FROM mqtt_messages GROUP BY message_type';
+    return await executeQuery(query);
+  }
+
+  // 获取各QoS等级统计
+  static async getQosStats(): Promise<any[]> {
+    const query = 'SELECT qos, COUNT(*) as count FROM mqtt_messages GROUP BY qos';
+    return await executeQuery(query);
+  }
+
+  // 获取消息大小统计
+  static async getSizeStats(): Promise<any[]> {
+    const query = `
+      SELECT 
+        AVG(LENGTH(payload)) as avg_size,
+        MIN(LENGTH(payload)) as min_size,
+        MAX(LENGTH(payload)) as max_size,
+        COUNT(*) as total_messages
+      FROM mqtt_messages
+    `;
+    return await executeQuery(query);
+  }
+
+  // 获取每小时消息统计
+  static async getHourlyStats(hours: number = 24): Promise<any[]> {
+    const query = `
+      SELECT 
+        DATE_FORMAT(FROM_UNIXTIME(timestamp / 1000), '%Y-%m-%d %H:00:00') as hour,
+        COUNT(*) as message_count,
+        AVG(LENGTH(payload)) as avg_payload_size
+      FROM mqtt_messages 
+      WHERE timestamp >= (UNIX_TIMESTAMP() - ?) * 1000
+      GROUP BY hour
+      ORDER BY hour DESC
+    `;
+    return await executeQuery(query, [hours]);
+  }
+
+  // 获取热门主题
+  static async getPopularTopics(limit: number = 10): Promise<any[]> {
+    const query = `
+      SELECT topic, COUNT(*) as message_count
+      FROM mqtt_messages 
+      WHERE message_type = 'publish'
+      GROUP BY topic
+      ORDER BY message_count DESC
+      LIMIT ?
+    `;
+    return await executeQuery(query, [limit]);
+  }
+
+  // 获取活跃客户端
+  static async getActiveClients(limit: number = 10): Promise<any[]> {
+    const query = `
+      SELECT clientid, COUNT(*) as message_count
+      FROM mqtt_messages 
+      WHERE message_type = 'publish'
+      GROUP BY clientid
+      ORDER BY message_count DESC
+      LIMIT ?
+    `;
+    return await executeQuery(query, [limit]);
+  }
+
+  // 创建新消息记录
+  static async create(messageData: Omit<MqttMessage, 'id' | 'created_at'>): Promise<MqttMessage> {
+    const query = `
+      INSERT INTO mqtt_messages (clientid, topic, payload, message_type, qos, timestamp)
+      VALUES (?, ?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      messageData.clientid,
+      messageData.topic,
+      messageData.payload,
+      messageData.message_type,
+      messageData.qos,
+      messageData.timestamp
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    // 获取插入的消息ID
+    const insertId = result.insertId;
+    
+    // 返回创建的消息
+    return this.getById(insertId) as Promise<MqttMessage>;
+  }
+
+  // 根据ID获取消息
+  static async getById(id: number): Promise<MqttMessage | null> {
+    const query = 'SELECT * FROM mqtt_messages WHERE id = ?';
+    const results = await executeQuery(query, [id]) as MqttMessage[];
+    return results.length > 0 ? results[0] : null;
+  }
+
+  // 获取消息热力图数据
+  static async getHeatmapData(days: number = 7): Promise<any[]> {
+    const query = `
+      SELECT 
+        DAYOFWEEK(FROM_UNIXTIME(timestamp / 1000)) - 1 as day,
+        HOUR(FROM_UNIXTIME(timestamp / 1000)) as hour,
+        COUNT(*) as value
+      FROM mqtt_messages 
+      WHERE timestamp >= (UNIX_TIMESTAMP() - ? * 24 * 3600) * 1000
+      GROUP BY day, hour
+      ORDER BY day, hour
+    `;
+    return await executeQuery(query, [days]);
+  }
+}

+ 127 - 0
mqtt-vue-dashboard/server/src/models/ota.ts

@@ -0,0 +1,127 @@
+import { executeQuery, config } from '../config/database';
+
+export interface OTATask {
+  id?: number;
+  device_id: string;
+  firmware_id: number;
+  status: 'pending' | 'downloading' | 'installing' | 'success' | 'failed';
+  progress: number;
+  error_message?: string;
+  start_time?: Date;
+  end_time?: Date;
+  created_at?: Date;
+  updated_at?: Date;
+}
+
+export class OTATaskModel {
+  // 创建OTA任务
+  static async create(taskData: Omit<OTATask, 'id' | 'created_at' | 'updated_at'>): Promise<OTATask> {
+    const query = `
+      INSERT INTO ota_tasks (device_id, firmware_id, status, progress, error_message, start_time, end_time)
+      VALUES (?, ?, ?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      taskData.device_id,
+      taskData.firmware_id,
+      taskData.status || 'pending',
+      taskData.progress || 0,
+      taskData.error_message || null,
+      taskData.start_time || null,
+      taskData.end_time || null
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    const insertId = result.insertId;
+    
+    // 返回创建的OTA任务
+    return this.getById(insertId) as Promise<OTATask>;
+  }
+
+  // 根据ID获取OTA任务
+  static async getById(id: number): Promise<OTATask | null> {
+    const query = 'SELECT * FROM ota_tasks WHERE id = ?';
+    const tasks = await executeQuery(query, [id]);
+    return tasks.length > 0 ? tasks[0] : null;
+  }
+
+  // 根据设备ID获取OTA任务
+  static async getByDeviceId(deviceId: string): Promise<OTATask[]> {
+    const query = 'SELECT * FROM ota_tasks WHERE device_id = ? ORDER BY created_at DESC';
+    return await executeQuery(query, [deviceId]);
+  }
+
+  // 获取所有OTA任务
+  static async getAll(): Promise<OTATask[]> {
+    const query = 'SELECT * FROM ota_tasks ORDER BY created_at DESC';
+    return await executeQuery(query);
+  }
+
+  // 根据状态获取OTA任务
+  static async getByStatus(status: string): Promise<OTATask[]> {
+    const query = 'SELECT * FROM ota_tasks WHERE status = ? ORDER BY created_at DESC';
+    return await executeQuery(query, [status]);
+  }
+
+  // 根据设备ID和状态获取OTA任务
+  static async getByDeviceIdAndStatus(deviceId: string, status: string): Promise<OTATask[]> {
+    const query = 'SELECT * FROM ota_tasks WHERE device_id = ? AND status = ? ORDER BY created_at DESC';
+    return await executeQuery(query, [deviceId, status]);
+  }
+
+  // 获取设备的待处理OTA任务
+  static async getPendingTasksByDeviceId(deviceId: string): Promise<OTATask[]> {
+    const query = 'SELECT * FROM ota_tasks WHERE device_id = ? AND status = ? ORDER BY created_at ASC';
+    return await executeQuery(query, [deviceId, 'pending']);
+  }
+
+  // 获取设备的未完成OTA任务(非success和failed状态)
+  static async getIncompleteTasksByDeviceId(deviceId: string): Promise<OTATask[]> {
+    const query = 'SELECT * FROM ota_tasks WHERE device_id = ? AND status NOT IN (?, ?) ORDER BY created_at ASC';
+    return await executeQuery(query, [deviceId, 'success', 'failed']);
+  }
+
+  // 更新OTA任务状态和进度
+  static async updateStatusAndProgress(id: number, status: OTATask['status'], progress: number): Promise<boolean> {
+    const query = 'UPDATE ota_tasks SET status = ?, progress = ?, updated_at = NOW() WHERE id = ?';
+    const result = await executeQuery(query, [status, progress, id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // 更新OTA任务结果
+  static async updateResult(id: number, status: 'success' | 'failed', errorMessage?: string): Promise<boolean> {
+    const query = 'UPDATE ota_tasks SET status = ?, progress = ?, error_message = ?, end_time = NOW() WHERE id = ?';
+    const result = await executeQuery(query, [status, status === 'success' ? 100 : 0, errorMessage || null, id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // 删除OTA任务
+  static async delete(id: number): Promise<boolean> {
+    const query = 'DELETE FROM ota_tasks WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  // 创建OTA任务表(如果不存在)
+  static async createTable(): Promise<void> {
+    const query = `
+      CREATE TABLE IF NOT EXISTS ota_tasks (
+        id INT AUTO_INCREMENT PRIMARY KEY,
+        device_id VARCHAR(255) NOT NULL,
+        firmware_id INT NOT NULL,
+        status ENUM('pending', 'downloading', 'installing', 'success', 'failed') DEFAULT 'pending',
+        progress INT DEFAULT 0,
+        error_message TEXT,
+        start_time TIMESTAMP NULL,
+        end_time TIMESTAMP NULL,
+        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+        INDEX idx_device_id (device_id),
+        INDEX idx_firmware_id (firmware_id),
+        INDEX idx_status (status),
+        FOREIGN KEY (firmware_id) REFERENCES firmware_files(id) ON DELETE CASCADE
+      )
+    `;
+    await executeQuery(query);
+  }
+}

+ 235 - 0
mqtt-vue-dashboard/server/src/models/permission.ts

@@ -0,0 +1,235 @@
+import { executeQuery } from '../config/database';
+
+// 页面接口
+export interface Page {
+  id: number;
+  name: string;
+  path: string;
+  description?: string;
+  created_at: Date;
+  updated_at: Date;
+}
+
+// 用户权限接口
+export interface UserPermission {
+  id: number;
+  user_id: string;
+  page_id: number;
+  created_at: Date;
+}
+
+/**
+ * 权限模型
+ * 处理页面和用户权限的CRUD操作
+ */
+export class PermissionModel {
+  /**
+   * 获取所有页面
+   */
+  static async getAllPages(): Promise<Page[]> {
+    try {
+      const query = `
+        SELECT id, name, path, description, created_at, updated_at
+        FROM pages
+        ORDER BY id
+      `;
+      
+      const result = await executeQuery(query);
+      return result as Page[];
+    } catch (error) {
+      console.error('获取页面列表失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 根据ID获取页面
+   */
+  static async getPageById(id: number): Promise<Page | null> {
+    try {
+      const query = `
+        SELECT id, name, path, description, created_at, updated_at
+        FROM pages
+        WHERE id = ?
+      `;
+      
+      const result = await executeQuery(query, [id]);
+      if (result.length === 0) {
+        return null;
+      }
+      return result[0] as Page;
+    } catch (error) {
+      console.error('根据ID获取页面失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 根据路径获取页面
+   */
+  static async getPageByPath(path: string): Promise<Page | null> {
+    try {
+      const query = `
+        SELECT id, name, path, description, created_at, updated_at
+        FROM pages
+        WHERE path = ?
+      `;
+      
+      const result = await executeQuery(query, [path]);
+      if (result.length === 0) {
+        return null;
+      }
+      return result[0] as Page;
+    } catch (error) {
+      console.error('根据路径获取页面失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取用户的所有权限
+   */
+  static async getUserPermissions(userId: string): Promise<Page[]> {
+    try {
+      const query = `
+        SELECT p.*
+        FROM pages p
+        INNER JOIN user_permissions up ON p.id = up.page_id
+        WHERE up.user_id = ?
+        ORDER BY p.id
+      `;
+      
+      const result = await executeQuery(query, [userId]);
+      return result as Page[];
+    } catch (error) {
+      console.error('获取用户权限失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 检查用户是否有权限访问某个页面
+   */
+  static async checkUserPermission(userId: string, pagePath: string): Promise<boolean> {
+    try {
+      const query = `
+        SELECT COUNT(*) as count
+        FROM pages p
+        INNER JOIN user_permissions up ON p.id = up.page_id
+        WHERE up.user_id = ? AND p.path = ?
+      `;
+      
+      const result = await executeQuery(query, [userId, pagePath]);
+      return (result[0].count as number) > 0;
+    } catch (error) {
+      console.error('检查用户权限失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 为用户分配权限
+   */
+  static async assignPermission(userId: string, pageId: number): Promise<UserPermission> {
+    try {
+      const query = `
+        INSERT INTO user_permissions (user_id, page_id)
+        VALUES (?, ?)
+        ON DUPLICATE KEY UPDATE id = id
+      `;
+      
+      const result = await executeQuery(query, [userId, pageId]);
+      
+      // 返回创建的权限
+      const permission = await this.getUserPermissionById(result.insertId);
+      if (!permission) {
+        throw new Error('分配权限失败,无法获取创建的权限信息');
+      }
+      return permission;
+    } catch (error) {
+      console.error('分配权限失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 批量为用户分配权限
+   */
+  static async assignPermissions(userId: string, pageIds: number[]): Promise<void> {
+    try {
+      // 先删除用户现有的所有权限
+      await this.removeAllUserPermissions(userId);
+      
+      // 批量插入新权限
+      if (pageIds.length > 0) {
+        const values = pageIds.map(pageId => `('${userId}', ${pageId})`).join(', ');
+        const query = `
+          INSERT INTO user_permissions (user_id, page_id)
+          VALUES ${values}
+        `;
+        await executeQuery(query);
+      }
+    } catch (error) {
+      console.error('批量分配权限失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 移除用户的某个权限
+   */
+  static async removePermission(userId: string, pageId: number): Promise<boolean> {
+    try {
+      const query = `
+        DELETE FROM user_permissions
+        WHERE user_id = ? AND page_id = ?
+      `;
+      
+      const result = await executeQuery(query, [userId, pageId]);
+      return result.affectedRows > 0;
+    } catch (error) {
+      console.error('移除用户权限失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 移除用户的所有权限
+   */
+  static async removeAllUserPermissions(userId: string): Promise<boolean> {
+    try {
+      const query = `
+        DELETE FROM user_permissions
+        WHERE user_id = ?
+      `;
+      
+      const result = await executeQuery(query, [userId]);
+      return result.affectedRows > 0;
+    } catch (error) {
+      console.error('移除用户所有权限失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 根据ID获取用户权限
+   */
+  private static async getUserPermissionById(id: number): Promise<UserPermission | null> {
+    try {
+      const query = `
+        SELECT id, user_id, page_id, created_at
+        FROM user_permissions
+        WHERE id = ?
+      `;
+      
+      const result = await executeQuery(query, [id]);
+      if (result.length === 0) {
+        return null;
+      }
+      return result[0] as UserPermission;
+    } catch (error) {
+      console.error('根据ID获取用户权限失败:', error);
+      throw error;
+    }
+  }
+}

+ 146 - 0
mqtt-vue-dashboard/server/src/models/sensorData.ts

@@ -0,0 +1,146 @@
+import { executeQuery } from '../config/database';
+
+export interface SensorData {
+  id?: number;
+  device_id: string;
+  topic: string;
+  data_type: string;
+  value: string;
+  timestamp: Date;
+  created_at?: Date;
+}
+
+export class SensorDataModel {
+  static async getAll(limit: number = 100, offset: number = 0): Promise<SensorData[]> {
+    const query = `
+      SELECT * FROM sensor_data
+      ORDER BY timestamp DESC
+      LIMIT ? OFFSET ?
+    `;
+    return await executeQuery(query, [limit, offset]);
+  }
+
+  static async getByDeviceId(deviceId: string, limit: number = 100): Promise<SensorData[]> {
+    const query = `
+      SELECT * FROM sensor_data
+      WHERE device_id = ?
+      ORDER BY timestamp DESC
+      LIMIT ?
+    `;
+    return await executeQuery(query, [deviceId, limit]);
+  }
+
+  static async getByDeviceIdAndType(deviceId: string, dataType: string, limit: number = 100): Promise<SensorData[]> {
+    const query = `
+      SELECT * FROM sensor_data
+      WHERE device_id = ? AND data_type = ?
+      ORDER BY timestamp DESC
+      LIMIT ?
+    `;
+    return await executeQuery(query, [deviceId, dataType, limit]);
+  }
+
+  static async getByType(dataType: string, limit: number = 100): Promise<SensorData[]> {
+    const query = `
+      SELECT * FROM sensor_data
+      WHERE data_type = ?
+      ORDER BY timestamp DESC
+      LIMIT ?
+    `;
+    return await executeQuery(query, [dataType, limit]);
+  }
+
+  static async getByTimeRange(deviceId: string, dataType: string, hours: number = 24): Promise<SensorData[]> {
+    const query = `
+      SELECT * FROM sensor_data
+      WHERE device_id = ? AND data_type = ? AND timestamp >= DATE_SUB(NOW(), INTERVAL ? HOUR)
+      ORDER BY timestamp ASC
+    `;
+    return await executeQuery(query, [deviceId, dataType, hours]);
+  }
+
+  static async getLatestByDevice(deviceId: string): Promise<SensorData[]> {
+    const query = `
+      SELECT * FROM sensor_data
+      WHERE device_id = ?
+      ORDER BY timestamp DESC
+      LIMIT 1
+    `;
+    return await executeQuery(query, [deviceId]);
+  }
+
+  static async getCount(): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM sensor_data';
+    const result = await executeQuery(query);
+    return result[0].count;
+  }
+
+  static async getCountByDeviceId(deviceId: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM sensor_data WHERE device_id = ?';
+    const result = await executeQuery(query, [deviceId]);
+    return result[0].count;
+  }
+
+  static async getCountByType(dataType: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM sensor_data WHERE data_type = ?';
+    const result = await executeQuery(query, [dataType]);
+    return result[0].count;
+  }
+
+  static async insert(data: Omit<SensorData, 'id' | 'created_at'>): Promise<SensorData> {
+    const query = `
+      INSERT INTO sensor_data (device_id, topic, data_type, value, timestamp)
+      VALUES (?, ?, ?, ?, ?)
+    `;
+    
+    const values = [
+      data.device_id,
+      data.topic || '',
+      data.data_type,
+      data.value,
+      data.timestamp || new Date()
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    return {
+      id: result.insertId,
+      ...data,
+      created_at: new Date()
+    };
+  }
+
+  static async updateLatestByDeviceAndType(deviceId: string, dataType: string, value: string): Promise<boolean> {
+    const query = `
+      UPDATE sensor_data
+      SET value = ?, timestamp = NOW()
+      WHERE id = (
+        SELECT id FROM (
+          SELECT id FROM sensor_data
+          WHERE device_id = ? AND data_type = ?
+          ORDER BY timestamp DESC
+          LIMIT 1
+        ) as temp
+      )
+    `;
+    
+    const result = await executeQuery(query, [value, deviceId, dataType]) as any;
+    return result.affectedRows > 0;
+  }
+
+  static async upsertByDeviceAndType(deviceId: string, dataType: string, value: string, topic?: string): Promise<void> {
+    const existingData = await this.getByDeviceIdAndType(deviceId, dataType, 1);
+    
+    if (existingData.length > 0) {
+      await this.updateLatestByDeviceAndType(deviceId, dataType, value);
+    } else {
+      await this.insert({
+        device_id: deviceId,
+        topic: topic || `device/${deviceId}/${dataType}`,
+        data_type: dataType,
+        value: value,
+        timestamp: new Date()
+      });
+    }
+  }
+}

+ 498 - 0
mqtt-vue-dashboard/server/src/models/systemLog.ts

@@ -0,0 +1,498 @@
+import { executeQuery } from '../config/database';
+
+export interface SystemLog {
+  id?: number;
+  level: 'info' | 'warn' | 'error' | 'debug';
+  message: string;
+  source: string;
+  module?: string;
+  user_id?: number;
+  username?: string;
+  ip_address?: string;
+  details?: string;
+  created_at?: Date;
+}
+
+export class SystemLogModel {
+  // 获取所有系统日志
+  static async getAll(limit?: number, offset?: number): Promise<SystemLog[]> {
+    let query = 'SELECT * FROM system_logs ORDER BY created_at DESC';
+    const params: (string | number)[] = [];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据ID获取系统日志
+  static async getById(id: number): Promise<SystemLog | null> {
+    const query = 'SELECT * FROM system_logs WHERE id = ?';
+    const logs = await executeQuery(query, [id]);
+    return logs.length > 0 ? logs[0] : null;
+  }
+
+  // 根据日志级别获取系统日志
+  static async getByLevel(level: string, limit?: number, offset?: number): Promise<SystemLog[]> {
+    let query = 'SELECT * FROM system_logs WHERE level = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [level];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据来源获取系统日志
+  static async getBySource(source: string, limit?: number, offset?: number): Promise<SystemLog[]> {
+    let query = 'SELECT * FROM system_logs WHERE source = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [source];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据模块获取系统日志
+  static async getByModule(module: string, limit?: number, offset?: number): Promise<SystemLog[]> {
+    let query = 'SELECT * FROM system_logs WHERE module = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [module];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据时间范围获取系统日志
+  static async getByTimeRange(startTime: Date, endTime: Date, limit?: number, offset?: number): Promise<SystemLog[]> {
+    let query = 'SELECT * FROM system_logs WHERE created_at BETWEEN ? AND ? ORDER BY created_at DESC';
+    const params: any[] = [startTime, endTime];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 获取系统日志总数
+  static async getCount(): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM system_logs';
+    const result = await executeQuery(query);
+    return result[0].count;
+  }
+
+  // 获取日志级别统计
+  static async getLevelStats(startTime?: Date, endTime?: Date): Promise<any[]> {
+    let query = `
+      SELECT 
+        level,
+        COUNT(*) as count
+      FROM system_logs
+    `;
+    
+    const params: any[] = [];
+    
+    if (startTime && endTime) {
+      query += ' WHERE created_at BETWEEN ? AND ?';
+      params.push(startTime, endTime);
+    }
+    
+    query += ' GROUP BY level';
+    
+    return await executeQuery(query, params);
+  }
+
+  // 获取来源统计
+  static async getSourceStats(startTime?: Date, endTime?: Date): Promise<any[]> {
+    let query = `
+      SELECT 
+        source,
+        COUNT(*) as count
+      FROM system_logs
+    `;
+    
+    const params: any[] = [];
+    
+    if (startTime && endTime) {
+      query += ' WHERE created_at BETWEEN ? AND ?';
+      params.push(startTime, endTime);
+    }
+    
+    query += ' GROUP BY source';
+    
+    return await executeQuery(query, params);
+  }
+
+  // 获取每日系统日志统计
+  static async getDailyStats(days: number = 7): Promise<any[]> {
+    const query = `
+      SELECT 
+        DATE(created_at) as date,
+        COUNT(*) as total,
+        SUM(CASE WHEN level = 'info' THEN 1 ELSE 0 END) as info,
+        SUM(CASE WHEN level = 'warn' THEN 1 ELSE 0 END) as warn,
+        SUM(CASE WHEN level = 'error' THEN 1 ELSE 0 END) as error,
+        SUM(CASE WHEN level = 'debug' THEN 1 ELSE 0 END) as debug
+      FROM system_logs
+      WHERE created_at >= DATE_SUB(CURRENT_DATE(), INTERVAL ? DAY)
+      GROUP BY DATE(created_at)
+      ORDER BY date DESC
+    `;
+    
+    return await executeQuery(query, [days]);
+  }
+
+  // 创建系统日志
+  static async create(systemLog: Omit<SystemLog, 'id' | 'created_at'>): Promise<SystemLog> {
+    const query = `
+      INSERT INTO system_logs (level, message, source, module, user_id, username, ip_address, details)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+    const params = [
+      systemLog.level,
+      systemLog.message,
+      systemLog.source,
+      systemLog.module || null,
+      systemLog.user_id || null,
+      systemLog.username || null,
+      systemLog.ip_address || null,
+      systemLog.details || null
+    ];
+    
+    const result = await executeQuery(query, params) as any;
+    return { ...systemLog, id: result.insertId, created_at: new Date() };
+  }
+
+  // 搜索系统日志
+  static async search(searchTerm: string, limit?: number, offset?: number): Promise<SystemLog[]> {
+    let query = `
+      SELECT * FROM system_logs 
+      WHERE message LIKE ? OR source LIKE ? OR module LIKE ? OR details LIKE ?
+      ORDER BY created_at DESC
+    `;
+    const params: (string | number)[] = [
+      `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`
+    ];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 获取搜索结果总数
+  static async getSearchCount(searchTerm: string): Promise<number> {
+    const query = `
+      SELECT COUNT(*) as count 
+      FROM system_logs 
+      WHERE message LIKE ? OR source LIKE ? OR module LIKE ? OR details LIKE ?
+    `;
+    const params = [
+      `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`, `%${searchTerm}%`
+    ];
+    const result = await executeQuery(query, params);
+    return result[0].count;
+  }
+
+  // 清理旧的系统日志
+  static async cleanup(daysToKeep: number = 30): Promise<number> {
+    const query = 'DELETE FROM system_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)';
+    const result = await executeQuery(query, [daysToKeep]) as any;
+    return result.affectedRows;
+  }
+
+  // 获取最近的系统日志
+  static async getRecent(limit: number = 10): Promise<SystemLog[]> {
+    const query = 'SELECT * FROM system_logs ORDER BY created_at DESC LIMIT ?';
+    return await executeQuery(query, [limit]);
+  }
+
+  // 根据用户ID获取系统日志
+  static async getByUserId(userId: number, limit?: number, offset?: number): Promise<SystemLog[]> {
+    let query = 'SELECT * FROM system_logs WHERE user_id = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [userId];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据用户名获取系统日志
+  static async getByUsername(username: string, limit?: number, offset?: number): Promise<SystemLog[]> {
+    let query = 'SELECT * FROM system_logs WHERE username = ? ORDER BY created_at DESC';
+    const params: (string | number)[] = [username];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 获取完整的系统日志统计信息
+  static async getFullStats(): Promise<any> {
+    try {
+      // 获取总数
+      const totalQuery = 'SELECT COUNT(*) as count FROM system_logs';
+      const totalResult = await executeQuery(totalQuery);
+      const total = totalResult[0].count;
+
+      // 获取各级别数量
+      const levelQuery = `
+        SELECT 
+          level,
+          COUNT(*) as count
+        FROM system_logs
+        GROUP BY level
+      `;
+      const levelStats = await executeQuery(levelQuery);
+      const byLevel: Record<string, number> = {};
+      
+      levelStats.forEach((stat: any) => {
+        byLevel[stat.level] = stat.count;
+      });
+
+      // 获取来源统计
+      const sourceQuery = `
+        SELECT 
+          source,
+          COUNT(*) as count
+        FROM system_logs
+        GROUP BY source
+      `;
+      const sourceStats = await executeQuery(sourceQuery);
+      const bySource: Record<string, number> = {};
+      
+      sourceStats.forEach((stat: any) => {
+        bySource[stat.source] = stat.count;
+      });
+
+      // 获取时间范围统计
+      const todayQuery = `
+        SELECT COUNT(*) as count 
+        FROM system_logs 
+        WHERE DATE(created_at) = CURDATE()
+      `;
+      const todayResult = await executeQuery(todayQuery);
+      const today = todayResult[0].count;
+
+      const weekQuery = `
+        SELECT COUNT(*) as count 
+        FROM system_logs 
+        WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+      `;
+      const weekResult = await executeQuery(weekQuery);
+      const week = weekResult[0].count;
+
+      const monthQuery = `
+        SELECT COUNT(*) as count 
+        FROM system_logs 
+        WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
+      `;
+      const monthResult = await executeQuery(monthQuery);
+      const month = monthResult[0].count;
+
+      return {
+        total,
+        byLevel,
+        bySource,
+        byTimeRange: {
+          today,
+          week,
+          month
+        }
+      };
+    } catch (error) {
+      console.error('获取系统日志统计信息失败:', error);
+      // 返回默认值
+      return {
+        total: 0,
+        byLevel: {},
+        bySource: {},
+        byTimeRange: {
+          today: 0,
+          week: 0,
+          month: 0
+        }
+      };
+    }
+  }
+
+  // 根据多个条件获取系统日志数量
+  static async getCountByMultipleConditions(
+    conditions: { [key: string]: any },
+    startTime?: Date,
+    endTime?: Date,
+    fuzzyFields?: string[]
+  ): Promise<number> {
+    let query = 'SELECT COUNT(*) as count FROM system_logs WHERE 1=1';
+    const params: any[] = [];
+    
+    for (const [key, value] of Object.entries(conditions)) {
+      if (value !== undefined && value !== null) {
+        if (fuzzyFields && fuzzyFields.includes(key)) {
+          query += ` AND ${key} LIKE ?`;
+          params.push(`%${value}%`);
+        } else if (Array.isArray(value)) {
+          const placeholders = value.map(() => '?').join(', ');
+          query += ` AND ${key} IN (${placeholders})`;
+          params.push(...value);
+        } else {
+          query += ` AND ${key} = ?`;
+          params.push(value);
+        }
+      }
+    }
+    
+    if (startTime && endTime) {
+      query += ' AND created_at BETWEEN ? AND ?';
+      params.push(startTime, endTime);
+    }
+    
+    const result = await executeQuery(query, params) as any;
+    return result[0].count;
+  }
+
+  // 根据多个条件获取系统日志
+  static async getByMultipleConditions(
+    conditions: { [key: string]: any },
+    startTime?: Date,
+    endTime?: Date,
+    limit?: number,
+    offset?: number,
+    fuzzyFields?: string[]
+  ): Promise<SystemLog[]> {
+    let query = 'SELECT * FROM system_logs WHERE 1=1';
+    const params: any[] = [];
+    
+    for (const [key, value] of Object.entries(conditions)) {
+      if (value !== undefined && value !== null) {
+        if (fuzzyFields && fuzzyFields.includes(key)) {
+          query += ` AND ${key} LIKE ?`;
+          params.push(`%${value}%`);
+        } else if (Array.isArray(value)) {
+          const placeholders = value.map(() => '?').join(', ');
+          query += ` AND ${key} IN (${placeholders})`;
+          params.push(...value);
+        } else {
+          query += ` AND ${key} = ?`;
+          params.push(value);
+        }
+      }
+    }
+    
+    if (startTime && endTime) {
+      query += ' AND created_at BETWEEN ? AND ?';
+      params.push(startTime, endTime);
+    }
+    
+    query += ' ORDER BY created_at DESC';
+    
+    if (limit !== undefined && offset !== undefined) {
+      query += ' LIMIT ? OFFSET ?';
+      params.push(limit, offset);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  // 根据时间范围获取系统日志数量
+  static async getCountByTimeRange(startTime: Date, endTime: Date): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM system_logs WHERE created_at BETWEEN ? AND ?';
+    const result = await executeQuery(query, [startTime, endTime]) as any;
+    return result[0].count;
+  }
+
+  // 根据日志级别获取系统日志数量
+  static async getCountByLevel(level: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM system_logs WHERE level = ?';
+    const result = await executeQuery(query, [level]) as any;
+    return result[0].count;
+  }
+
+  // 根据来源获取系统日志数量
+  static async getCountBySource(source: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM system_logs WHERE source = ?';
+    const result = await executeQuery(query, [source]) as any;
+    return result[0].count;
+  }
+
+  // 根据模块获取系统日志数量
+  static async getCountByModule(module: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM system_logs WHERE module = ?';
+    const result = await executeQuery(query, [module]) as any;
+    return result[0].count;
+  }
+
+  // 根据用户ID获取系统日志数量
+  static async getCountByUserId(userId: number): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM system_logs WHERE user_id = ?';
+    const result = await executeQuery(query, [userId]) as any;
+    return result[0].count;
+  }
+
+  // 根据用户名获取系统日志数量
+  static async getCountByUsername(username: string): Promise<number> {
+    const query = 'SELECT COUNT(*) as count FROM system_logs WHERE username = ?';
+    const result = await executeQuery(query, [username]) as any;
+    return result[0].count;
+  }
+}

+ 240 - 0
mqtt-vue-dashboard/server/src/models/user.ts

@@ -0,0 +1,240 @@
+import bcrypt from 'bcryptjs';
+import { executeQuery } from '../config/database';
+
+// 用户角色类型
+export type UserRole = 'admin' | 'user' | 'viewer';
+
+// 用户模型接口
+export interface User {
+  id: string;
+  username: string;
+  password: string;
+  email?: string;
+  role: UserRole;
+  created_at: Date;
+  updated_at: Date;
+}
+
+// 用户创建参数接口
+export interface UserCreateParams {
+  username: string;
+  password: string;
+  role?: UserRole;
+  email?: string;
+}
+
+/**
+ * 用户模型
+ * 处理用户数据的CRUD操作
+ */
+export class UserModel {
+  /**
+   * 创建用户
+   */
+  static async create(params: UserCreateParams): Promise<User> {
+    try {
+      const { username, password, role = 'user', email } = params;
+      
+      // 生成密码哈希
+      const salt = await bcrypt.genSalt(10);
+      const hashedPassword = await bcrypt.hash(password, salt);
+      
+      // 确保email为null而不是undefined
+      const emailValue = email || null;
+      
+      // 生成UUID作为id
+      const uuidResult = await executeQuery('SELECT UUID() as uuid');
+      const id = uuidResult[0].uuid;
+      
+      // 插入用户数据
+      const query = `
+        INSERT INTO users (id, username, password, email, role, created_at, updated_at)
+        VALUES (?, ?, ?, ?, ?, NOW(), NOW())
+      `;
+      
+      await executeQuery(query, [id, username, hashedPassword, emailValue, role]);
+      
+      // 返回创建的用户
+      const user = await this.getById(id);
+      if (!user) {
+        throw new Error('创建用户失败,无法获取创建的用户信息');
+      }
+      return user;
+    } catch (error) {
+      console.error('创建用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 根据ID获取用户
+   */
+  static async getById(id: any): Promise<User | null> {
+    try {
+      const query = `
+        SELECT id, username, password, email, role, created_at, updated_at
+        FROM users
+        WHERE id = ?
+      `;
+      
+      const result = await executeQuery(query, [id]);
+      
+      if (result.length === 0) {
+        return null;
+      }
+      
+      return result[0] as User;
+    } catch (error) {
+      console.error('根据ID获取用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 根据用户名获取用户
+   */
+  static async getByUsername(username: string): Promise<User | null> {
+    try {
+      const query = `
+        SELECT id, username, password, email, role, created_at, updated_at
+        FROM users
+        WHERE username = ?
+      `;
+      
+      const result = await executeQuery(query, [username]);
+      
+      if (result.length === 0) {
+        return null;
+      }
+      
+      return result[0] as User;
+    } catch (error) {
+      console.error('根据用户名获取用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 更新用户
+   */
+  static async update(id: any, updates: Partial<Omit<User, 'id' | 'created_at' | 'password'>>): Promise<User | null> {
+    try {
+      // 构建更新字段和参数
+      const updateFields: string[] = [];
+      const params: any[] = [];
+      
+      if (updates.username) {
+        updateFields.push('username = ?');
+        params.push(updates.username);
+      }
+      
+      if (updates.role) {
+        updateFields.push('role = ?');
+        params.push(updates.role);
+      }
+      
+      if (updates.email) {
+        updateFields.push('email = ?');
+        params.push(updates.email);
+      }
+      
+      // 总是更新updated_at字段
+      updateFields.push('updated_at = NOW()');
+      
+      if (updateFields.length === 1) {
+        // 只有updated_at字段更新,直接返回当前用户
+        return await this.getById(id);
+      }
+      
+      params.push(id);
+      
+      // 执行更新
+      const query = `
+        UPDATE users
+        SET ${updateFields.join(', ')}
+        WHERE id = ?
+      `;
+      
+      await executeQuery(query, params);
+      
+      // 返回更新后的用户
+      return await this.getById(id);
+    } catch (error) {
+      console.error('更新用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 更新用户密码
+   */
+  static async updatePassword(id: any, newPassword: string): Promise<void> {
+    try {
+      // 生成新的密码哈希
+      const salt = await bcrypt.genSalt(10);
+      const hashedPassword = await bcrypt.hash(newPassword, salt);
+      
+      // 更新密码
+      const query = `
+        UPDATE users
+        SET password = ?, updated_at = NOW()
+        WHERE id = ?
+      `;
+      
+      await executeQuery(query, [hashedPassword, id]);
+    } catch (error) {
+      console.error('更新用户密码失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 删除用户
+   */
+  static async delete(id: number): Promise<boolean> {
+    try {
+      const query = 'DELETE FROM users WHERE id = ?';
+      
+      const result = await executeQuery(query, [id]);
+      
+      return result.affectedRows > 0;
+    } catch (error) {
+      console.error('删除用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取所有用户
+   */
+  static async getAll(limit?: number, offset?: number): Promise<User[]> {
+    try {
+      let query = `
+        SELECT id, username, password, email, role, created_at, updated_at
+        FROM users
+        ORDER BY created_at DESC
+      `;
+      
+      const params: any[] = [];
+      
+      if (limit !== undefined) {
+        query += ' LIMIT ?';
+        params.push(limit);
+        
+        if (offset !== undefined) {
+          query += ' OFFSET ?';
+          params.push(offset);
+        }
+      }
+      
+      const result = await executeQuery(query, params);
+      
+      return result as User[];
+    } catch (error) {
+      console.error('获取所有用户失败:', error);
+      throw error;
+    }
+  }
+
+
+}

+ 130 - 0
mqtt-vue-dashboard/server/src/models/wifiConfig.ts

@@ -0,0 +1,130 @@
+import { executeQuery } from '../config/database';
+
+export interface WiFiConfiguration {
+  id?: number;
+  device_clientid: string;
+  ssid: string;
+  password: string;
+  status: 'pending' | 'sent' | 'applied' | 'failed';
+  sent_at?: Date;
+  applied_at?: Date;
+  created_at?: Date;
+  updated_at?: Date;
+}
+
+export class WiFiConfigModel {
+  /**
+   * 创建WiFi配置记录
+   */
+  static async create(configData: Omit<WiFiConfiguration, 'id' | 'created_at' | 'updated_at'>): Promise<WiFiConfiguration> {
+    const query = `
+      INSERT INTO wifi_configurations (device_clientid, ssid, password, status, sent_at)
+      VALUES (?, ?, ?, ?, NOW())
+    `;
+    
+    const values = [
+      configData.device_clientid,
+      configData.ssid,
+      configData.password,
+      configData.status || 'sent'
+    ];
+    
+    const result = await executeQuery(query, values) as any;
+    
+    return {
+      id: result.insertId,
+      ...configData,
+      sent_at: new Date(),
+      created_at: new Date(),
+      updated_at: new Date()
+    };
+  }
+
+  /**
+   * 根据设备ID获取最新的WiFi配置
+   */
+  static async getLatestByDeviceId(deviceClientId: string): Promise<WiFiConfiguration | null> {
+    const query = `
+      SELECT * FROM wifi_configurations 
+      WHERE device_clientid = ? 
+      ORDER BY created_at DESC 
+      LIMIT 1
+    `;
+    
+    const configs = await executeQuery(query, [deviceClientId]);
+    return configs.length > 0 ? configs[0] : null;
+  }
+
+  /**
+   * 根据设备ID获取所有WiFi配置历史
+   */
+  static async getByDeviceId(deviceClientId: string, limit?: number): Promise<WiFiConfiguration[]> {
+    let query = `
+      SELECT * FROM wifi_configurations 
+      WHERE device_clientid = ? 
+      ORDER BY created_at DESC
+    `;
+    
+    const params: any[] = [deviceClientId];
+    
+    if (limit) {
+      query += ' LIMIT ?';
+      params.push(limit);
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  /**
+   * 更新WiFi配置状态
+   */
+  static async updateStatus(id: number, status: 'pending' | 'sent' | 'applied' | 'failed'): Promise<boolean> {
+    const query = `
+      UPDATE wifi_configurations 
+      SET status = ?, updated_at = NOW()
+      ${status === 'applied' ? ', applied_at = NOW()' : ''}
+      WHERE id = ?
+    `;
+    
+    const result = await executeQuery(query, [status, id]) as any;
+    return result.affectedRows > 0;
+  }
+
+  /**
+   * 获取所有WiFi配置
+   */
+  static async getAll(limit?: number, offset?: number): Promise<WiFiConfiguration[]> {
+    let query = 'SELECT * FROM wifi_configurations ORDER BY created_at DESC';
+    const params: any[] = [];
+    
+    if (limit !== undefined) {
+      query += ' LIMIT ?';
+      params.push(limit);
+      
+      if (offset !== undefined) {
+        query += ' OFFSET ?';
+        params.push(offset);
+      }
+    }
+    
+    return await executeQuery(query, params);
+  }
+
+  /**
+   * 根据ID获取WiFi配置
+   */
+  static async getById(id: number): Promise<WiFiConfiguration | null> {
+    const query = 'SELECT * FROM wifi_configurations WHERE id = ?';
+    const configs = await executeQuery(query, [id]);
+    return configs.length > 0 ? configs[0] : null;
+  }
+
+  /**
+   * 删除WiFi配置
+   */
+  static async delete(id: number): Promise<boolean> {
+    const query = 'DELETE FROM wifi_configurations WHERE id = ?';
+    const result = await executeQuery(query, [id]) as any;
+    return result.affectedRows > 0;
+  }
+}

+ 42 - 0
mqtt-vue-dashboard/server/src/routes/authLogRoutes.ts

@@ -0,0 +1,42 @@
+import { Router } from 'express';
+import { AuthLogController } from '../controllers/authLogController';
+
+const router = Router();
+
+// 获取所有认证日志
+router.get('/', AuthLogController.getAllAuthLogs);
+
+// 根据ID获取认证日志
+router.get('/id/:id', AuthLogController.getAuthLogById);
+
+// 根据客户端ID获取认证日志
+router.get('/clientid/:clientid', AuthLogController.getAuthLogsByClientId);
+
+// 根据用户名获取认证日志
+router.get('/username/:username', AuthLogController.getAuthLogsByUsername);
+
+// 根据IP地址获取认证日志
+router.get('/ip/:ipAddress', AuthLogController.getAuthLogsByIpAddress);
+
+// 根据操作类型获取认证日志
+router.get('/operation/:operationType', AuthLogController.getAuthLogsByOperationType);
+
+// 根据结果获取认证日志
+router.get('/result/:result', AuthLogController.getAuthLogsByResult);
+
+// 根据时间范围获取认证日志
+router.get('/timerange', AuthLogController.getAuthLogsByTimeRange);
+
+// 根据多条件查询认证日志
+router.get('/search', AuthLogController.getAuthLogsByMultipleConditions);
+
+// 获取认证日志统计信息
+router.get('/stats', AuthLogController.getAuthLogStats);
+
+// 获取最近认证日志
+router.get('/recent', AuthLogController.getRecentAuthLogs);
+
+// 清理旧认证日志
+router.delete('/cleanup', AuthLogController.cleanupOldAuthLogs);
+
+export default router;

+ 33 - 0
mqtt-vue-dashboard/server/src/routes/authRoutes.ts

@@ -0,0 +1,33 @@
+import { Router } from 'express';
+import { AuthController } from '../controllers/authController';
+import { authenticateToken } from '../middleware/auth';
+
+const router = Router();
+
+/**
+ * 认证相关路由
+ * 处理用户登录、注册、获取当前用户信息等功能
+ */
+
+// 登录路由
+router.post('/login', AuthController.login);
+
+// 注册路由(可选,根据需要启用)
+router.post('/register', AuthController.register);
+
+// 获取当前用户信息(需要认证)
+router.get('/me', authenticateToken, AuthController.getCurrentUser);
+
+// 刷新token(需要认证)
+router.post('/refresh-token', authenticateToken, AuthController.refreshToken);
+
+// 修改密码(需要认证)
+router.post('/change-password', authenticateToken, AuthController.changePassword);
+
+// 用户管理路由(仅管理员)
+router.get('/users', authenticateToken, AuthController.getUsers);
+router.post('/users', authenticateToken, AuthController.createUser);
+router.put('/users/:id', authenticateToken, AuthController.updateUser);
+router.delete('/users/:id', authenticateToken, AuthController.deleteUser);
+
+export default router;

+ 39 - 0
mqtt-vue-dashboard/server/src/routes/clientAclRoutes.ts

@@ -0,0 +1,39 @@
+import { Router } from 'express';
+import { ClientAclController } from '../controllers/clientAclController';
+
+const router = Router();
+
+// 获取所有客户端授权规则
+router.get('/', ClientAclController.getAllClientAcl);
+
+// 根据ID获取客户端授权规则
+router.get('/id/:id', ClientAclController.getClientAclById);
+
+// 根据用户名获取授权规则
+router.get('/username/:username', ClientAclController.getClientAclByUsername);
+
+// 根据主题获取授权规则
+router.get('/topic/:topic', ClientAclController.getClientAclByTopic);
+
+// 根据用户名和操作类型获取授权规则
+router.get('/username/:username/action/:action', ClientAclController.getClientAclByUsernameAndAction);
+
+// 创建客户端授权规则
+router.post('/', ClientAclController.createClientAcl);
+
+// 更新客户端授权规则
+router.put('/:id', ClientAclController.updateClientAcl);
+
+// 删除客户端授权规则
+router.delete('/:id', ClientAclController.deleteClientAcl);
+
+// 批量删除客户端授权规则
+router.delete('/multiple', ClientAclController.deleteMultipleClientAcl);
+
+// 检查用户权限
+router.post('/check', ClientAclController.checkUserPermission);
+
+// 获取客户端授权统计信息
+router.get('/stats', ClientAclController.getClientAclStats);
+
+export default router;

+ 59 - 0
mqtt-vue-dashboard/server/src/routes/clientAuthRoutes.ts

@@ -0,0 +1,59 @@
+import { Router } from 'express';
+import { ClientAuthController } from '../controllers/clientAuthController';
+
+const router = Router();
+
+// 获取所有客户端认证信息
+router.get('/', ClientAuthController.getAllClientAuth);
+
+// 根据ID获取客户端认证信息
+router.get('/id/:id', ClientAuthController.getClientAuthById);
+
+// 根据用户名获取客户端认证信息
+router.get('/username/:username', ClientAuthController.getClientAuthByUsername);
+
+// 根据客户端ID获取客户端认证信息
+router.get('/clientid/:clientid', ClientAuthController.getClientAuthByClientId);
+
+// 创建客户端认证信息
+router.post('/', ClientAuthController.createClientAuth);
+
+// 更新客户端认证信息
+router.put('/:id', ClientAuthController.updateClientAuth);
+
+// 删除客户端认证信息
+router.delete('/:id', ClientAuthController.deleteClientAuth);
+
+// 验证客户端认证信息
+router.post('/verify', ClientAuthController.verifyClientAuth);
+
+// 获取客户端认证统计信息
+router.get('/stats', ClientAuthController.getClientAuthStats);
+
+// 认证方法相关路由
+router.get('/auth-methods', ClientAuthController.getAuthMethods);
+router.get('/auth-methods/:id', ClientAuthController.getAuthMethodById);
+router.post('/auth-methods', ClientAuthController.createAuthMethod);
+router.put('/auth-methods/:id', ClientAuthController.updateAuthMethod);
+router.delete('/auth-methods/:id', ClientAuthController.deleteAuthMethod);
+
+// 认证策略相关路由
+router.get('/auth-policies', ClientAuthController.getAuthPolicies);
+router.get('/auth-policies/:id', ClientAuthController.getAuthPolicyById);
+router.post('/auth-policies', ClientAuthController.createAuthPolicy);
+router.put('/auth-policies/:id', ClientAuthController.updateAuthPolicy);
+router.delete('/auth-policies/:id', ClientAuthController.deleteAuthPolicy);
+
+// 客户端令牌相关路由
+router.get('/client-tokens/:clientid', ClientAuthController.getClientTokens);
+router.post('/client-tokens', ClientAuthController.createClientToken);
+router.put('/client-tokens/:id', ClientAuthController.updateClientToken);
+router.delete('/client-tokens/:id', ClientAuthController.deleteClientToken);
+
+// 客户端证书相关路由
+router.get('/client-certificates/:clientid', ClientAuthController.getClientCertificates);
+router.post('/client-certificates', ClientAuthController.createClientCertificate);
+router.put('/client-certificates/:id', ClientAuthController.updateClientCertificate);
+router.delete('/client-certificates/:id', ClientAuthController.deleteClientCertificate);
+
+export default router;

+ 24 - 0
mqtt-vue-dashboard/server/src/routes/clientConnectionRoutes.ts

@@ -0,0 +1,24 @@
+import { Router } from 'express';
+import { ClientConnectionController } from '../controllers/clientConnectionController';
+
+const router = Router();
+
+// 获取所有连接记录
+router.get('/', ClientConnectionController.getAllConnections);
+
+// 根据客户端ID获取连接记录
+router.get('/client/:clientid', ClientConnectionController.getConnectionsByClientId);
+
+// 根据事件类型获取连接记录
+router.get('/event/:event', ClientConnectionController.getConnectionsByEvent);
+
+// 获取指定时间范围内的连接记录
+router.get('/timerange', ClientConnectionController.getConnectionsByTimeRange);
+
+// 获取连接事件统计
+router.get('/stats', ClientConnectionController.getConnectionStats);
+
+// 获取每日连接统计
+router.get('/stats/daily', ClientConnectionController.getDailyConnectionStats);
+
+export default router;

+ 15 - 0
mqtt-vue-dashboard/server/src/routes/dashboardRoutes.ts

@@ -0,0 +1,15 @@
+import { Router } from 'express';
+import { DashboardController } from '../controllers/dashboardController';
+
+const router = Router();
+
+// 获取仪表板概览数据
+router.get('/overview', DashboardController.getOverview);
+
+// 获取图表数据
+router.get('/charts', DashboardController.getChartData);
+
+// 获取系统信息
+router.get('/system-info', DashboardController.getSystemInfo);
+
+export default router;

+ 46 - 0
mqtt-vue-dashboard/server/src/routes/deviceBindingRoutes.ts

@@ -0,0 +1,46 @@
+import { Router } from 'express';
+import { DeviceBindingController } from '../controllers/deviceBindingController';
+
+const router = Router();
+
+// 获取所有设备绑定关系
+router.get('/', DeviceBindingController.getAllBindings);
+
+// 调试端点
+router.get('/debug', (req, res) => {
+  console.log('调试端点被调用');
+  res.json({
+    success: true,
+    data: {
+      devices: [
+        { clientid: 'test1', device_name: '测试设备1', status: 'online', room_id: null },
+        { clientid: 'test2', device_name: '测试设备2', status: 'offline', room_id: 1 }
+      ],
+      pagination: { current: 1, pageSize: 10, total: 2 }
+    },
+    message: '调试响应成功'
+  });
+});
+
+// 获取所有设备及其绑定状态
+router.get('/all-devices-status', DeviceBindingController.getAllDevicesWithBindingStatus);
+
+// 获取可绑定的设备(未绑定到任何房间的设备)
+router.get('/available-devices', DeviceBindingController.getAvailableDevices);
+
+// 根据房间ID获取绑定的设备
+router.get('/room/:roomId', DeviceBindingController.getDevicesByRoom);
+
+// 获取房间设备详情(合并devices表和绑定关系)
+router.get('/room/:roomId/details', DeviceBindingController.getRoomDevicesWithDetails);
+
+// 绑定设备到房间
+router.post('/bind', DeviceBindingController.bindDevice);
+
+// 解绑设备
+router.delete('/unbind/:deviceClientId', DeviceBindingController.unbindDevice);
+
+// 更新设备绑定关系
+router.put('/:id', DeviceBindingController.updateBinding);
+
+export default router;

+ 17 - 0
mqtt-vue-dashboard/server/src/routes/deviceLogRoutes.ts

@@ -0,0 +1,17 @@
+import { Router } from 'express';
+import { DeviceLogController } from '../controllers/deviceLogController';
+
+const router = Router();
+
+router.get('/client/:clientid', DeviceLogController.getDeviceLogs);
+router.get('/client/:clientid/connect', DeviceLogController.getDeviceConnectLogs);
+router.get('/client/:clientid/publish', DeviceLogController.getDevicePublishLogs);
+router.get('/client/:clientid/subscribe', DeviceLogController.getDeviceSubscribeLogs);
+router.get('/client/:clientid/stats', DeviceLogController.getDeviceLogStats);
+router.get('/client/:clientid/daily-stats', DeviceLogController.getDeviceDailyStats);
+router.get('/client/:clientid/auth', DeviceLogController.getDeviceAuthLogs);
+router.get('/client/:clientid/system', DeviceLogController.getDeviceSystemLogs);
+router.get('/client/:clientid/overview', DeviceLogController.getDeviceOverview);
+router.get('/client/:clientid/auth-stats', DeviceLogController.getDeviceAuthStats);
+
+export default router;

+ 52 - 0
mqtt-vue-dashboard/server/src/routes/deviceRoutes.ts

@@ -0,0 +1,52 @@
+import { Router } from 'express';
+import { DeviceController } from '../controllers/deviceController';
+
+const router = Router();
+
+// 获取未绑定的设备列表
+console.log('注册 /unbound 路由');
+router.get('/unbound', (req, res, next) => {
+  console.log('接收到 /unbound 请求');
+  next();
+}, DeviceController.getUnboundDevices);
+
+// 获取所有设备
+router.get('/', DeviceController.getAllDevices);
+
+// 根据状态获取设备
+router.get('/status/:status', DeviceController.getDevicesByStatus);
+
+// 根据客户端ID获取设备
+router.get('/client/:clientid', DeviceController.getDeviceByClientId);
+
+// 获取设备状态统计
+router.get('/stats', DeviceController.getDeviceStats);
+
+// 获取设备统计信息
+router.get('/device-stats', DeviceController.getDeviceStats);
+
+// 根据房间ID获取设备列表
+router.get('/room/:room_id', DeviceController.getDevicesByRoomId);
+
+// 根据客户端ID删除设备
+router.delete('/:clientid', DeviceController.deleteDeviceByClientId);
+
+// 根据客户端ID更新设备信息
+router.put('/:clientid', (req, res, next) => {
+  console.log('接收到设备更新请求,URL:', req.originalUrl, '方法:', req.method, '参数:', req.params, '请求体:', req.body);
+  next();
+}, DeviceController.updateDeviceByClientId);
+
+// 控制设备继电器开关
+router.post('/control-relay', DeviceController.controlRelay);
+
+// 配置设备WiFi
+router.post('/configure-wifi', DeviceController.configureWiFi);
+
+// 重启设备
+router.post('/restart', DeviceController.restartDevice);
+
+// 查询设备WiFi信号强度
+router.get('/:clientid/rssi', DeviceController.queryRssi);
+
+export default router;

+ 44 - 0
mqtt-vue-dashboard/server/src/routes/index.ts

@@ -0,0 +1,44 @@
+import { Router } from 'express';
+import deviceRoutes from './deviceRoutes';
+import deviceLogRoutes from './deviceLogRoutes';
+import mqttMessageRoutes from './mqttMessageRoutes';
+import clientConnectionRoutes from './clientConnectionRoutes';
+import dashboardRoutes from './dashboardRoutes';
+import syncRoutes from './syncRoutes';
+import clientAuthRoutes from './clientAuthRoutes';
+import clientAclRoutes from './clientAclRoutes';
+import authLogRoutes from './authLogRoutes';
+import systemLogRoutes from './systemLogRoutes';
+import authRoutes from './authRoutes';
+import permissionRoutes from './permissionRoutes';
+import roomRoutes from './roomRoutes';
+import roomDeviceRoutes from './roomDeviceRoutes';
+import deviceBindingRoutes from './deviceBindingRoutes';
+import otaRoutes from './otaRoutes';
+import sensorDataRoutes from './sensorDataRoutes';
+import wifiConfigRoutes from './wifiConfigRoutes';
+import mqttBrokerRoutes from './mqttBrokerRoutes';
+
+const router = Router();
+
+router.use('/auth', authRoutes);
+router.use('/devices', deviceRoutes);
+router.use('/device-logs', deviceLogRoutes);
+router.use('/messages', mqttMessageRoutes);
+router.use('/connections', clientConnectionRoutes);
+router.use('/dashboard', dashboardRoutes);
+router.use('/sync', syncRoutes);
+router.use('/client-auth', clientAuthRoutes);
+router.use('/client-acl', clientAclRoutes);
+router.use('/auth-logs', authLogRoutes);
+router.use('/system-logs', systemLogRoutes);
+router.use('/permissions', permissionRoutes);
+router.use('/rooms', roomRoutes);
+router.use('/room-devices', roomDeviceRoutes);
+router.use('/device-bindings', deviceBindingRoutes);
+router.use('/ota', otaRoutes);
+router.use('/sensor-data', sensorDataRoutes);
+router.use('/wifi-configs', wifiConfigRoutes);
+router.use('/broker', mqttBrokerRoutes);
+
+export default router;

+ 11 - 0
mqtt-vue-dashboard/server/src/routes/mqttBrokerRoutes.ts

@@ -0,0 +1,11 @@
+import { Router } from 'express';
+import { MqttBrokerController } from '../controllers/mqttBrokerController';
+
+const router = Router();
+
+router.get('/status', MqttBrokerController.getStatus);
+router.get('/clients', MqttBrokerController.getConnectedClients);
+router.post('/clients/:clientId/disconnect', MqttBrokerController.disconnectClient);
+router.post('/publish', MqttBrokerController.publishMessage);
+
+export default router;

+ 40 - 0
mqtt-vue-dashboard/server/src/routes/mqttMessageRoutes.ts

@@ -0,0 +1,40 @@
+import { Router, Request, Response } from 'express';
+import { MqttMessageController } from '../controllers/mqttMessageController';
+import { MqttMessageModel } from '../models/mqttMessage';
+
+const router = Router();
+
+// 获取所有消息
+router.get('/', MqttMessageController.getAllMessages);
+
+// 根据客户端ID获取消息
+router.get('/client/:clientid', MqttMessageController.getMessagesByClientId);
+
+// 根据主题获取消息
+router.get('/topic/:topic', MqttMessageController.getMessagesByTopic);
+
+// 根据消息类型获取消息
+router.get('/type/:type', MqttMessageController.getMessagesByType);
+
+// 获取指定时间范围内的消息
+router.get('/timerange', MqttMessageController.getMessagesByTimeRange);
+
+// 获取消息类型统计
+router.get('/stats/types', MqttMessageController.getMessageTypeStats);
+
+// 获取消息QoS统计
+router.get('/stats/qos', MqttMessageController.getMessageQosStats);
+
+// 获取每小时消息统计
+router.get('/stats/hourly', MqttMessageController.getHourlyMessageStats);
+
+// 获取热门主题
+router.get('/stats/topics', MqttMessageController.getPopularTopics);
+
+// 获取活跃客户端
+router.get('/stats/clients', MqttMessageController.getActiveClients);
+
+// 获取消息热力图数据
+router.get('/stats/heatmap', MqttMessageController.getMessageHeatmapData);
+
+export default router;

+ 24 - 0
mqtt-vue-dashboard/server/src/routes/otaRoutes.ts

@@ -0,0 +1,24 @@
+import express from 'express';
+import { OtaController } from '../controllers/otaController';
+import { authenticateToken } from '../middleware/auth';
+import { upload } from '../middleware/uploadMiddleware';
+
+const router = express.Router();
+
+// 固件文件相关路由
+router.post('/firmware', authenticateToken, upload.single('firmware'), OtaController.uploadFirmware);
+router.get('/firmware', authenticateToken, OtaController.getFirmwareFiles);
+router.delete('/firmware/:id', authenticateToken, OtaController.deleteFirmware);
+router.get('/firmware/:id', OtaController.downloadFirmware); // 无需认证,设备可以直接下载
+
+// OTA任务相关路由
+router.post('/devices/:deviceId/upgrade', authenticateToken, OtaController.createOTATask);
+router.post('/upgrade', authenticateToken, OtaController.bulkCreateOTATask); // 批量OTA升级
+router.get('/devices/:deviceId/tasks', authenticateToken, OtaController.getDeviceOTATasks);
+router.get('/tasks', authenticateToken, OtaController.getAllOTATasks);
+router.put('/tasks/:taskId/cancel', authenticateToken, OtaController.cancelOTATask); // 取消OTA任务
+router.put('/tasks/:taskId/retry', authenticateToken, OtaController.retryOTATask); // 重试OTA任务
+router.delete('/tasks/:taskId', authenticateToken, OtaController.deleteOTATask); // 删除OTA任务
+router.put('/tasks/:taskId/status', OtaController.updateOTATaskStatus); // 无需认证,设备可以直接更新状态
+
+export default router;

+ 30 - 0
mqtt-vue-dashboard/server/src/routes/permissionRoutes.ts

@@ -0,0 +1,30 @@
+import { Router } from 'express';
+import { PermissionController } from '../controllers/permissionController';
+import { authenticateToken } from '../middleware/auth';
+
+const router = Router();
+
+/**
+ * 权限管理相关路由
+ * 处理页面管理和用户权限分配等功能
+ */
+
+// 获取所有页面列表(需要管理员权限)
+router.get('/pages', authenticateToken, PermissionController.getAllPages);
+
+// 获取用户的权限列表(需要管理员权限)
+router.get('/users/:userId/permissions', authenticateToken, PermissionController.getUserPermissions);
+
+// 为用户分配权限(需要管理员权限)
+router.post('/users/:userId/permissions', authenticateToken, PermissionController.assignPermission);
+
+// 批量为用户分配权限(需要管理员权限)
+router.post('/users/:userId/permissions/batch', authenticateToken, PermissionController.assignPermissions);
+
+// 移除用户的权限(需要管理员权限)
+router.delete('/users/:userId/permissions/:pageId', authenticateToken, PermissionController.removePermission);
+
+// 检查用户是否有权限访问某个页面
+router.get('/users/:userId/check/:pagePath', authenticateToken, PermissionController.checkPermission);
+
+export default router;

+ 43 - 0
mqtt-vue-dashboard/server/src/routes/roomDeviceRoutes.ts

@@ -0,0 +1,43 @@
+import { Router } from 'express';
+import { 
+  getAllDevices, 
+  getDeviceById, 
+  getDevicesByRoom, 
+  getDevicesByType, 
+  createDevice, 
+  updateDevice, 
+  updateDeviceStatus, 
+  deleteDevice, 
+  getDeviceStats 
+} from '../controllers/roomDeviceController';
+
+const router = Router();
+
+// 获取所有设备
+router.get('/', getAllDevices);
+
+// 获取设备统计信息
+router.get('/stats', getDeviceStats);
+
+// 根据类型获取设备
+router.get('/type/:type', getDevicesByType);
+
+// 根据房间ID获取设备
+router.get('/room/:roomId', getDevicesByRoom);
+
+// 根据ID获取单个设备
+router.get('/:id', getDeviceById);
+
+// 创建新设备
+router.post('/', createDevice);
+
+// 更新设备信息
+router.put('/:id', updateDevice);
+
+// 更新设备状态
+router.patch('/:id/status', updateDeviceStatus);
+
+// 删除设备
+router.delete('/:id', deleteDevice);
+
+export default router;

+ 35 - 0
mqtt-vue-dashboard/server/src/routes/roomRoutes.ts

@@ -0,0 +1,35 @@
+import { Router } from 'express';
+import { 
+  getAllRooms, 
+  getRoomById, 
+  getRoomsByFloor, 
+  createRoom, 
+  updateRoom, 
+  deleteRoom, 
+  getRoomStats 
+} from '../controllers/roomController';
+
+const router = Router();
+
+// 获取所有房间
+router.get('/', getAllRooms);
+
+// 获取房间统计信息
+router.get('/stats', getRoomStats);
+
+// 根据楼层获取房间
+router.get('/floor/:floorId', getRoomsByFloor);
+
+// 根据ID获取单个房间
+router.get('/:id', getRoomById);
+
+// 创建新房间
+router.post('/', createRoom);
+
+// 更新房间信息
+router.put('/:id', updateRoom);
+
+// 删除房间
+router.delete('/:id', deleteRoom);
+
+export default router;

+ 16 - 0
mqtt-vue-dashboard/server/src/routes/sensorDataRoutes.ts

@@ -0,0 +1,16 @@
+import { Router } from 'express';
+import { SensorDataController } from '../controllers/sensorDataController';
+
+const router = Router();
+
+router.get('/', SensorDataController.getAllSensorData);
+
+router.get('/device/:deviceId', SensorDataController.getSensorDataByDevice);
+
+router.get('/type/:dataType', SensorDataController.getSensorDataByType);
+
+router.get('/device/:deviceId/type/:dataType', SensorDataController.getSensorDataByDeviceAndType);
+
+router.get('/device/:deviceId/latest', SensorDataController.getLatestSensorData);
+
+export default router;

+ 69 - 0
mqtt-vue-dashboard/server/src/routes/syncRoutes.ts

@@ -0,0 +1,69 @@
+import express, { Request, Response } from 'express';
+import dataSyncService from '../services/dataSyncService';
+
+const router = express.Router();
+
+/**
+ * 获取数据同步服务状态
+ */
+router.get('/status', async (req: Request, res: Response) => {
+  try {
+    const status = dataSyncService.getStatus();
+    
+    res.status(200).json({
+      success: true,
+      data: status,
+      message: status.isRunning ? '数据同步服务正在运行' : '数据同步服务已停止'
+    });
+  } catch (error) {
+    res.status(500).json({
+      success: false,
+      message: '获取数据同步服务状态失败',
+      error: error instanceof Error ? error.message : String(error)
+    });
+  }
+});
+
+/**
+ * 启动数据同步服务
+ */
+router.post('/start', async (req: Request, res: Response) => {
+  try {
+    const { intervalMs } = req.body;
+    
+    dataSyncService.start(intervalMs);
+    
+    res.status(200).json({
+      success: true,
+      message: '数据同步服务已启动'
+    });
+  } catch (error) {
+    res.status(500).json({
+      success: false,
+      message: '启动数据同步服务失败',
+      error: error instanceof Error ? error.message : String(error)
+    });
+  }
+});
+
+/**
+ * 停止数据同步服务
+ */
+router.post('/stop', async (req: Request, res: Response) => {
+  try {
+    dataSyncService.stop();
+    
+    res.status(200).json({
+      success: true,
+      message: '数据同步服务已停止'
+    });
+  } catch (error) {
+    res.status(500).json({
+      success: false,
+      message: '停止数据同步服务失败',
+      error: error instanceof Error ? error.message : String(error)
+    });
+  }
+});
+
+export default router;

+ 39 - 0
mqtt-vue-dashboard/server/src/routes/systemLogRoutes.ts

@@ -0,0 +1,39 @@
+import { Router } from 'express';
+import { SystemLogController } from '../controllers/systemLogController';
+
+const router = Router();
+
+// 获取所有系统日志
+router.get('/', SystemLogController.getAllSystemLogs);
+
+// 根据ID获取系统日志
+router.get('/id/:id', SystemLogController.getSystemLogById);
+
+// 根据日志级别获取系统日志
+router.get('/level/:level', SystemLogController.getSystemLogsByLevel);
+
+// 根据来源获取系统日志
+router.get('/source/:source', SystemLogController.getSystemLogsBySource);
+
+// 根据模块获取系统日志
+router.get('/module/:module', SystemLogController.getSystemLogsByModule);
+
+// 根据时间范围获取系统日志
+router.get('/timerange', SystemLogController.getSystemLogsByTimeRange);
+
+// 根据多条件查询系统日志
+router.get('/search', SystemLogController.getSystemLogsByMultipleConditions);
+
+// 获取系统日志统计信息
+router.get('/stats', SystemLogController.getSystemLogStats);
+
+// 获取最近系统日志
+router.get('/recent', SystemLogController.getRecentSystemLogs);
+
+// 创建系统日志
+router.post('/', SystemLogController.createSystemLog);
+
+// 清理旧系统日志
+router.delete('/cleanup', SystemLogController.cleanupOldSystemLogs);
+
+export default router;

+ 119 - 0
mqtt-vue-dashboard/server/src/routes/wifiConfigRoutes.ts

@@ -0,0 +1,119 @@
+import { Router, Request, Response } from 'express';
+import { WiFiConfigModel } from '../models/wifiConfig';
+
+const router = Router();
+
+router.get('/device/:clientid', async (req: Request, res: Response) => {
+  try {
+    const clientid = req.params.clientid as string;
+    const limit = parseInt(req.query.limit as string) || 10;
+    
+    if (!clientid) {
+      res.status(400).json({
+        success: false,
+        message: '设备客户端ID不能为空'
+      });
+      return;
+    }
+    
+    const configs = await WiFiConfigModel.getByDeviceId(clientid, limit);
+    
+    res.status(200).json({
+      success: true,
+      data: configs
+    });
+  } catch (error) {
+    console.error('获取设备WiFi配置失败:', error);
+    res.status(500).json({
+      success: false,
+      message: '获取设备WiFi配置失败',
+      error: error instanceof Error ? error.message : '未知错误'
+    });
+  }
+});
+
+router.get('/device/:clientid/latest', async (req: Request, res: Response) => {
+  try {
+    const clientid = req.params.clientid as string;
+    
+    if (!clientid) {
+      res.status(400).json({
+        success: false,
+        message: '设备客户端ID不能为空'
+      });
+      return;
+    }
+    
+    const config = await WiFiConfigModel.getLatestByDeviceId(clientid);
+    
+    res.status(200).json({
+      success: true,
+      data: config
+    });
+  } catch (error) {
+    console.error('获取设备最新WiFi配置失败:', error);
+    res.status(500).json({
+      success: false,
+      message: '获取设备最新WiFi配置失败',
+      error: error instanceof Error ? error.message : '未知错误'
+    });
+  }
+});
+
+router.get('/all', async (req: Request, res: Response) => {
+  try {
+    const limit = parseInt(req.query.limit as string) || 50;
+    const offset = parseInt(req.query.offset as string) || 0;
+    
+    const configs = await WiFiConfigModel.getAll(limit, offset);
+    
+    res.status(200).json({
+      success: true,
+      data: configs
+    });
+  } catch (error) {
+    console.error('获取所有WiFi配置失败:', error);
+    res.status(500).json({
+      success: false,
+      message: '获取所有WiFi配置失败',
+      error: error instanceof Error ? error.message : '未知错误'
+    });
+  }
+});
+
+router.delete('/:id', async (req: Request, res: Response) => {
+  try {
+    const id = req.params.id as string;
+    
+    if (!id) {
+      res.status(400).json({
+        success: false,
+        message: '配置ID不能为空'
+      });
+      return;
+    }
+    
+    const success = await WiFiConfigModel.delete(parseInt(id));
+    
+    if (success) {
+      res.status(200).json({
+        success: true,
+        message: 'WiFi配置删除成功'
+      });
+    } else {
+      res.status(404).json({
+        success: false,
+        message: 'WiFi配置不存在'
+      });
+    }
+  } catch (error) {
+    console.error('删除WiFi配置失败:', error);
+    res.status(500).json({
+      success: false,
+      message: '删除WiFi配置失败',
+      error: error instanceof Error ? error.message : '未知错误'
+    });
+  }
+});
+
+export default router;

+ 162 - 0
mqtt-vue-dashboard/server/src/services/dataSyncService.ts

@@ -0,0 +1,162 @@
+import { DeviceModel } from '../models/device';
+import { ClientConnectionModel } from '../models/clientConnection';
+import { MqttMessageModel } from '../models/mqttMessage';
+
+/**
+ * 数据同步服务
+ * 负责从数据库读取数据并更新缓存
+ */
+export class DataSyncService {
+  private isRunning: boolean = false;
+  private syncInterval: NodeJS.Timeout | null = null;
+  private syncIntervalMs: number = 60000; // 默认1分钟同步一次
+
+  /**
+   * 启动数据同步服务
+   */
+  start(intervalMs?: number): void {
+    if (this.isRunning) {
+      console.log('数据同步服务已在运行中');
+      return;
+    }
+
+    if (intervalMs) {
+      this.syncIntervalMs = intervalMs;
+    }
+
+    this.isRunning = true;
+    console.log(`数据同步服务已启动,同步间隔: ${this.syncIntervalMs}ms`);
+
+    // 立即执行一次同步
+    this.performSync().catch(error => {
+      console.error('初始数据同步失败:', error);
+    });
+
+    // 设置定时同步
+    this.syncInterval = setInterval(() => {
+      this.performSync().catch(error => {
+        console.error('定时数据同步失败:', error);
+      });
+    }, this.syncIntervalMs);
+  }
+
+  /**
+   * 停止数据同步服务
+   */
+  stop(): void {
+    if (!this.isRunning) {
+      console.log('数据同步服务未在运行');
+      return;
+    }
+
+    this.isRunning = false;
+    
+    if (this.syncInterval) {
+      clearInterval(this.syncInterval);
+      this.syncInterval = null;
+    }
+
+    console.log('数据同步服务已停止');
+  }
+
+  /**
+   * 获取同步状态
+   */
+  getStatus(): { isRunning: boolean; intervalMs: number } {
+    return {
+      isRunning: this.isRunning,
+      intervalMs: this.syncIntervalMs
+    };
+  }
+
+  /**
+   * 执行数据同步
+   */
+  private async performSync(): Promise<void> {
+    if (!this.isRunning) {
+      return;
+    }
+
+    try {
+      console.log('开始执行数据同步...');
+      
+      // 从数据库读取设备数据
+      await this.readDevicesFromDatabase();
+      
+      // 从数据库读取连接数据
+      await this.readConnectionsFromDatabase();
+      
+      // 从数据库读取消息数据
+      await this.readMessagesFromDatabase();
+      
+      console.log('数据同步完成');
+    } catch (error) {
+      console.error('数据同步过程中发生错误:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 从数据库读取设备数据
+   */
+  private async readDevicesFromDatabase(): Promise<void> {
+    try {
+      console.log('开始从数据库读取设备数据...');
+      
+      // 从数据库获取设备列表
+      const devices = await DeviceModel.getAll();
+      console.log(`从数据库读取到 ${devices.length} 个设备数据`);
+      
+      // 这里可以添加数据缓存或处理逻辑
+      // 例如:将数据存储在内存中,或者触发前端更新事件
+      
+      console.log('设备数据读取完成');
+    } catch (error) {
+      console.error('从数据库读取设备数据时出错:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 从数据库读取连接数据
+   */
+  private async readConnectionsFromDatabase(): Promise<void> {
+    try {
+      console.log('开始从数据库读取连接数据...');
+      
+      // 从数据库获取连接信息
+      const connections = await ClientConnectionModel.getAll();
+      console.log(`从数据库读取到 ${connections.length} 个连接数据`);
+      
+      // 这里可以添加数据缓存或处理逻辑
+      
+      console.log('连接数据读取完成');
+    } catch (error) {
+      console.error('从数据库读取连接数据时出错:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 从数据库读取消息数据
+   */
+  private async readMessagesFromDatabase(): Promise<void> {
+    try {
+      console.log('开始从数据库读取消息数据...');
+      
+      // 从数据库获取消息信息
+      const messages = await MqttMessageModel.getAll();
+      console.log(`从数据库读取到 ${messages.length} 个消息数据`);
+      
+      // 这里可以添加数据缓存或处理逻辑
+      
+      console.log('消息数据读取完成');
+    } catch (error) {
+      console.error('从数据库读取消息数据时出错:', error);
+      throw error;
+    }
+  }
+}
+
+// 导出单例实例
+export default new DataSyncService();

+ 97 - 0
mqtt-vue-dashboard/server/src/services/loggerService.ts

@@ -0,0 +1,97 @@
+import { SystemLogModel, SystemLog } from '../models/systemLog';
+
+// 日志服务类,用于生成系统日志
+export class LoggerService {
+  // 记录info级别日志
+  static async info(
+    message: string,
+    options: {
+      source: string;
+      module?: string;
+      user_id?: number;
+      username?: string;
+      ip_address?: string;
+      details?: string;
+    }
+  ): Promise<SystemLog | null> {
+    return this.log('info', message, options);
+  }
+
+  // 记录warn级别日志
+  static async warn(
+    message: string,
+    options: {
+      source: string;
+      module?: string;
+      user_id?: number;
+      username?: string;
+      ip_address?: string;
+      details?: string;
+    }
+  ): Promise<SystemLog | null> {
+    return this.log('warn', message, options);
+  }
+
+  // 记录error级别日志
+  static async error(
+    message: string,
+    options: {
+      source: string;
+      module?: string;
+      user_id?: number;
+      username?: string;
+      ip_address?: string;
+      details?: string;
+    }
+  ): Promise<SystemLog | null> {
+    return this.log('error', message, options);
+  }
+
+  // 记录debug级别日志
+  static async debug(
+    message: string,
+    options: {
+      source: string;
+      module?: string;
+      user_id?: number;
+      username?: string;
+      ip_address?: string;
+      details?: string;
+    }
+  ): Promise<SystemLog | null> {
+    return this.log('debug', message, options);
+  }
+
+  // 通用日志记录方法
+  private static async log(
+    level: 'info' | 'warn' | 'error' | 'debug',
+    message: string,
+    options: {
+      source: string;
+      module?: string;
+      user_id?: number;
+      username?: string;
+      ip_address?: string;
+      details?: string;
+    }
+  ): Promise<SystemLog | null> {
+    try {
+      const logData = {
+        level,
+        message,
+        source: options.source,
+        module: options.module || 'general',
+        user_id: options.user_id,
+        username: options.username,
+        ip_address: options.ip_address,
+        details: options.details
+      };
+
+      return await SystemLogModel.create(logData);
+    } catch (error) {
+      // 如果日志写入失败,控制台记录错误,避免影响主业务流程
+      console.error('日志写入失败:', error);
+      return null;
+    }
+  }
+}

+ 852 - 0
mqtt-vue-dashboard/server/src/services/mqttBrokerService.ts

@@ -0,0 +1,852 @@
+import Aedes from 'aedes';
+import { createServer, Server as NetServer } from 'net';
+import { DeviceModel } from '../models/device';
+import { MqttMessageModel } from '../models/mqttMessage';
+import { ClientConnectionModel } from '../models/clientConnection';
+import { ClientAuthModel } from '../models/clientAuth';
+import { ClientAclModel } from '../models/clientAcl';
+import { AuthLogModel } from '../models/authLog';
+import { LoggerService } from './loggerService';
+import { getWebSocketService } from './websocketService';
+import { OTATaskModel } from '../models/ota';
+import { FirmwareFileModel } from '../models/firmware';
+
+interface BrokerClient {
+  id: string;
+  username?: string;
+  ip?: string;
+  port?: number;
+  connectedAt?: Date;
+}
+
+export class MqttBrokerService {
+  private static instance: MqttBrokerService;
+  private broker: InstanceType<typeof Aedes>;
+  private server: NetServer;
+  private port: number;
+  private connectedClients: Map<string, BrokerClient> = new Map();
+  private syncInterval: NodeJS.Timeout | null = null;
+
+  private constructor() {
+    this.port = parseInt(process.env.MQTT_BROKER_PORT || '1883', 10);
+    this.broker = new Aedes({
+      id: 'mqtt-vue-dashboard-broker',
+      concurrency: 100,
+      heartbeatInterval: 60000,
+      connectTimeout: 30000,
+    });
+
+    this.server = createServer(this.broker.handle);
+    this.setupBrokerEvents();
+  }
+
+  static getInstance(): MqttBrokerService {
+    if (!MqttBrokerService.instance) {
+      MqttBrokerService.instance = new MqttBrokerService();
+    }
+    return MqttBrokerService.instance;
+  }
+
+  start(): Promise<void> {
+    return new Promise((resolve, reject) => {
+      this.server.listen(this.port, () => {
+        console.log(`MQTT Broker 已启动,监听端口: ${this.port}`);
+        LoggerService.info('MQTT Broker 已启动', {
+          source: 'mqtt_broker',
+          module: 'startup',
+          details: JSON.stringify({ port: this.port })
+        }).catch(() => {});
+        this.startDeviceStatusSync();
+        resolve();
+      });
+
+      this.server.on('error', (err) => {
+        console.error('MQTT Broker 启动失败:', err.message);
+        LoggerService.error('MQTT Broker 启动失败', {
+          source: 'mqtt_broker',
+          module: 'startup',
+          details: JSON.stringify({ error: err.message })
+        }).catch(() => {});
+        reject(err);
+      });
+    });
+  }
+
+  stop(): Promise<void> {
+    return new Promise((resolve) => {
+      if (this.syncInterval) {
+        clearInterval(this.syncInterval);
+        this.syncInterval = null;
+      }
+      this.server.close(() => {
+        this.broker.close(() => {
+          console.log('MQTT Broker 已停止');
+          resolve();
+        });
+      });
+    });
+  }
+
+  private setupBrokerEvents(): void {
+    this.broker.authenticate = ((client: any, username: string | undefined, password: Buffer | undefined, done: any) => {
+      this.authenticateClient(client, username, password, done);
+    }) as any;
+    this.broker.authorizePublish = this.authorizePublish.bind(this);
+    this.broker.authorizeSubscribe = this.authorizeSubscribe.bind(this);
+
+    this.broker.on('client', (client: any) => {
+      this.handleClientConnect(client);
+    });
+
+    this.broker.on('clientDisconnect', (client: any) => {
+      this.handleClientDisconnect(client);
+    });
+
+    this.broker.on('publish', (packet: any, client: any) => {
+      if (client) {
+        this.handleMessagePublish(packet, client);
+      }
+    });
+
+    this.broker.on('subscribe', (subscriptions: any, client: any) => {
+      if (client) {
+        this.handleClientSubscribe(subscriptions, client);
+      }
+    });
+
+    this.broker.on('unsubscribe', (subscriptions: any, client: any) => {
+      if (client) {
+        this.handleClientUnsubscribe(subscriptions, client);
+      }
+    });
+
+    this.broker.on('clientError', (client: any, err: any) => {
+      console.error(`客户端错误 [${client.id}]:`, err.message);
+    });
+  }
+
+  private async authenticateClient(
+    client: any,
+    username: string | undefined,
+    password: Buffer | undefined,
+    callback: (err: Error | null, success: boolean | undefined) => void
+  ): Promise<void> {
+    const clientId = client.id;
+    const ip = this.getClientIp(client);
+
+    if (!username || !password) {
+      const allowAnonymous = process.env.MQTT_ALLOW_ANONYMOUS === 'true';
+      if (allowAnonymous) {
+        this.logAuth(clientId, username || '', ip, 'connect', 'success', '');
+        callback(null, true);
+        return;
+      }
+      this.logAuth(clientId, username || '', ip, 'connect', 'failure', '缺少用户名或密码');
+      callback(null, false);
+      return;
+    }
+
+    try {
+      let authRecord = await ClientAuthModel.findByUsernameAndClientId(username, clientId);
+
+      if (!authRecord) {
+        authRecord = await ClientAuthModel.findByUsername(username);
+      }
+
+      if (!authRecord) {
+        this.logAuth(clientId, username, ip, 'connect', 'failure', '认证记录不存在');
+        callback(null, false);
+        return;
+      }
+
+      if (authRecord.status === 'disabled') {
+        this.logAuth(clientId, username, ip, 'connect', 'failure', '客户端已禁用');
+        callback(null, false);
+        return;
+      }
+
+      const passwordStr = password.toString();
+      const isMatch = ClientAuthModel.verifyPassword(
+        passwordStr,
+        authRecord.salt || '',
+        authRecord.password_hash,
+        authRecord.use_salt
+      );
+
+      if (!isMatch) {
+        this.logAuth(clientId, username, ip, 'connect', 'failure', '密码错误');
+        callback(null, false);
+        return;
+      }
+
+      await ClientAuthModel.updateLastLogin(username, clientId);
+
+      this.logAuth(clientId, username, ip, 'connect', 'success', '');
+      callback(null, true);
+    } catch (error) {
+      console.error('认证过程出错:', error);
+      this.logAuth(clientId, username || '', ip, 'connect', 'failure', '认证服务错误');
+      callback(null, false);
+    }
+  }
+
+  private async authorizePublish(
+    client: any,
+    packet: any,
+    callback: (err: Error | null) => void
+  ): Promise<void> {
+    const username = client.username;
+    const topic = packet.topic;
+
+    try {
+      if (topic && topic.startsWith('$SYS/')) {
+        callback(null);
+        return;
+      }
+
+      const isSuperuser = await this.isSuperuser(username);
+      if (isSuperuser) {
+        callback(null);
+        return;
+      }
+
+      const hasPermission = await this.checkAclPermission(username, topic, 'publish');
+      if (hasPermission) {
+        callback(null);
+      } else {
+        this.logAuth(client.id, username || '', this.getClientIp(client), 'publish', 'failure', `无权发布到主题: ${topic}`);
+        callback(new Error(`无权发布到主题: ${topic}`));
+      }
+    } catch (error) {
+      console.error('发布授权检查出错:', error);
+      callback(null);
+    }
+  }
+
+  private async authorizeSubscribe(
+    client: any,
+    subscription: any,
+    callback: (err: Error | null, subscription?: any) => void
+  ): Promise<void> {
+    const username = client.username;
+    const topic = subscription.topic;
+    
+    try {
+      const isSuperuser = await this.isSuperuser(username);
+      if (isSuperuser) {
+        callback(null, subscription);
+        return;
+      }
+
+      if (!topic) {
+        callback(null, subscription);
+        return;
+      }
+
+      const hasPermission = await this.checkAclPermission(username, topic, 'subscribe');
+      if (!hasPermission) {
+        this.logAuth(client.id, username || '', this.getClientIp(client), 'subscribe', 'failure', `无权订阅主题: ${topic}`);
+        callback(new Error(`无权订阅主题: ${topic}`));
+        return;
+      }
+
+      callback(null, subscription);
+    } catch (error) {
+      console.error('订阅授权检查出错:', error);
+      callback(null, subscription);
+    }
+  }
+
+  private async isSuperuser(username: string | undefined): Promise<boolean> {
+    if (!username) return false;
+    try {
+      const authRecord = await ClientAuthModel.findByUsername(username);
+      return authRecord ? (authRecord.is_superuser === true || authRecord.is_superuser === 1 as any) : false;
+    } catch {
+      return false;
+    }
+  }
+
+  private async checkAclPermission(username: string | undefined, topic: string, action: 'publish' | 'subscribe'): Promise<boolean> {
+    if (!username) return true;
+    try {
+      const aclRules = await ClientAclModel.getByUsername(username);
+      if (!aclRules || aclRules.length === 0) return true;
+
+      for (const rule of aclRules) {
+        if (this.topicMatches(topic, rule.topic)) {
+          if (rule.action === 'pubsub' || rule.action === action) {
+            return rule.permission === 'allow';
+          }
+        }
+      }
+      return true;
+    } catch {
+      return true;
+    }
+  }
+
+  private topicMatches(topic: string, pattern: string): boolean {
+    if (pattern === '#') return true;
+    if (pattern === topic) return true;
+
+    const patternParts = pattern.split('/');
+    const topicParts = topic.split('/');
+
+    for (let i = 0; i < patternParts.length; i++) {
+      if (patternParts[i] === '#') return true;
+      if (i >= topicParts.length) return false;
+      if (patternParts[i] !== '+' && patternParts[i] !== topicParts[i]) return false;
+    }
+
+    return patternParts.length === topicParts.length;
+  }
+
+  private getClientIp(client: any): string {
+    if (client.conn && client.conn.remoteAddress) {
+      const remoteAddress = client.conn.remoteAddress as string;
+      if (remoteAddress === '::1') {
+        return '127.0.0.1';
+      }
+      if (remoteAddress.startsWith('::ffff:')) {
+        return remoteAddress.substring(7);
+      }
+      return remoteAddress;
+    }
+    return 'unknown';
+  }
+
+  private async handleClientConnect(client: any): Promise<void> {
+    const clientId = client.id;
+    const username = client.username || '';
+    const ip = this.getClientIp(client);
+
+    this.connectedClients.set(clientId, {
+      id: clientId,
+      username,
+      ip,
+      connectedAt: new Date()
+    });
+
+    console.log(`客户端已连接: ${clientId} (${username}) from ${ip}`);
+
+    try {
+      let device = await DeviceModel.getByClientId(clientId);
+      if (device) {
+        await DeviceModel.update(clientId, {
+          status: 'online',
+          last_event_time: new Date(),
+          last_online_time: new Date(),
+          device_ip_port: ip,
+          last_ip_port: ip,
+          connect_count: (device.connect_count || 0) + 1
+        });
+      } else {
+        await DeviceModel.create({
+          clientid: clientId,
+          device_name: clientId,
+          username,
+          status: 'online',
+          last_event_time: new Date(),
+          last_online_time: new Date(),
+          device_ip_port: ip,
+          last_ip_port: ip,
+          connect_count: 1
+        });
+      }
+
+      await ClientConnectionModel.create({
+        clientid: clientId,
+        username,
+        event: 'client.connected',
+        timestamp: new Date(),
+        connected_at: new Date(),
+        node: 'mqtt-vue-dashboard',
+        peername: ip,
+        sockname: `0.0.0.0:${this.port}`,
+        proto_name: 'MQTT',
+        proto_ver: 4,
+        keepalive: 60,
+        clean_start: 1
+      });
+
+      this.broadcastToWebSocket('device_connected', {
+        clientid: clientId,
+        username,
+        ip,
+        timestamp: new Date().toISOString()
+      });
+
+      await this.executePendingOTATasks(clientId);
+    } catch (error) {
+      console.error('处理客户端连接事件出错:', error);
+    }
+  }
+
+  private async handleClientDisconnect(client: any): Promise<void> {
+    const clientId = client.id;
+    const clientInfo = this.connectedClients.get(clientId);
+    const username = clientInfo?.username || '';
+    const ip = clientInfo?.ip || 'unknown';
+    const connectedAt = clientInfo?.connectedAt;
+
+    this.connectedClients.delete(clientId);
+
+    console.log(`客户端已断开: ${clientId}`);
+
+    try {
+      await DeviceModel.update(clientId, {
+        status: 'offline',
+        last_event_time: new Date(),
+        last_offline_time: new Date()
+      });
+
+      let connectionDuration: number | undefined;
+      if (connectedAt) {
+        connectionDuration = Math.floor((Date.now() - connectedAt.getTime()) / 1000);
+        await DeviceModel.updateOnlineDuration(clientId, connectionDuration);
+      }
+
+      await ClientConnectionModel.create({
+        clientid: clientId,
+        username,
+        event: 'client.disconnected',
+        timestamp: new Date(),
+        connected_at: connectedAt || undefined,
+        node: 'mqtt-vue-dashboard',
+        peername: ip,
+        sockname: `0.0.0.0:${this.port}`,
+        proto_name: 'MQTT',
+        proto_ver: 4,
+        keepalive: 60,
+        clean_start: 1,
+        reason: 'normal',
+        connection_duration: connectionDuration
+      });
+
+      this.broadcastToWebSocket('device_disconnected', {
+        clientid: clientId,
+        username,
+        timestamp: new Date().toISOString()
+      });
+    } catch (error) {
+      console.error('处理客户端断开事件出错:', error);
+    }
+  }
+
+  private async handleMessagePublish(packet: any, client: any): Promise<void> {
+    const clientId = client.id;
+    const username = client.username || '';
+    const topic = packet.topic;
+    const payload = packet.payload?.toString() || '';
+    const qosValue = typeof packet.qos === 'number' ? Math.min(packet.qos, 2) : 0;
+
+    try {
+      if (topic.startsWith('$SYS/')) return;
+
+      await MqttMessageModel.create({
+        clientid: clientId,
+        topic,
+        payload,
+        qos: qosValue,
+        retain: packet.retain ? 1 : 0,
+        message_type: 'publish',
+        timestamp: Date.now(),
+        node: 'mqtt-vue-dashboard',
+        username,
+        proto_ver: 4,
+        payload_format: this.detectPayloadFormat(payload),
+        message_time: new Date()
+      });
+
+      this.broadcastToWebSocket('mqtt_message', {
+        clientid: clientId,
+        topic,
+        payload,
+        qos: qosValue,
+        retain: packet.retain,
+        timestamp: Date.now()
+      });
+
+      await this.handleDeviceMessage(clientId, topic, payload);
+    } catch (error) {
+      console.error('处理消息发布事件出错:', error);
+    }
+  }
+
+  private async handleClientSubscribe(subscriptions: any, client: any): Promise<void> {
+    const clientId = client.id;
+    const username = client.username || '';
+    const subsList = Array.isArray(subscriptions) ? subscriptions : [subscriptions];
+
+    for (const sub of subsList) {
+      try {
+        const qosValue = typeof sub.qos === 'number' ? Math.min(sub.qos, 2) : 0;
+        await MqttMessageModel.create({
+          clientid: clientId,
+          topic: sub.topic,
+          payload: '',
+          qos: qosValue,
+          retain: 0,
+          message_type: 'subscribe',
+          timestamp: Date.now(),
+          node: 'mqtt-vue-dashboard',
+          username,
+          proto_ver: 4,
+          payload_format: 'text',
+          message_time: new Date()
+        });
+
+        this.broadcastToWebSocket('mqtt_subscribe', {
+          clientid: clientId,
+          topic: sub.topic,
+          qos: qosValue,
+          timestamp: Date.now()
+        });
+      } catch (error) {
+        console.error('处理订阅事件出错:', error);
+      }
+    }
+  }
+
+  private async handleClientUnsubscribe(subscriptions: any, client: any): Promise<void> {
+    const clientId = client.id;
+    const username = client.username || '';
+    const subsList = Array.isArray(subscriptions) ? subscriptions : [subscriptions];
+
+    for (const topic of subsList) {
+      try {
+        await MqttMessageModel.create({
+          clientid: clientId,
+          topic: typeof topic === 'string' ? topic : topic.topic,
+          payload: '',
+          qos: 0,
+          retain: 0,
+          message_type: 'unsubscribe',
+          timestamp: Date.now(),
+          node: 'mqtt-vue-dashboard',
+          username,
+          proto_ver: 4,
+          payload_format: 'text',
+          message_time: new Date()
+        });
+      } catch (error) {
+        console.error('处理取消订阅事件出错:', error);
+      }
+    }
+  }
+
+  private async handleDeviceMessage(clientId: string, topic: string, payload: string): Promise<void> {
+    try {
+      const topicParts = topic.split('/');
+      if (topicParts.length < 2) return;
+
+      const deviceId = topicParts[1];
+      const messageType = topicParts.slice(2).join('/');
+
+      if (messageType === 'ota/status' || messageType === 'ota/progress') {
+        await this.handleOtaMessage(deviceId, payload, messageType);
+      } else if (messageType === 'relay/state') {
+        await this.handleRelayStateMessage(deviceId, payload);
+      } else if (messageType === 'wifi/rssi') {
+        await this.handleRssiMessage(deviceId, payload);
+      } else if (messageType === 'wifi/info') {
+        await this.handleWifiInfoMessage(deviceId, payload);
+      } else if (messageType === 'wifi/status') {
+        await this.handleWifiStatusMessage(deviceId, payload);
+      } else if (messageType === 'sensor/data' || messageType === 'data') {
+        await this.handleSensorData(deviceId, topic, payload);
+      }
+    } catch (error) {
+      console.error('处理设备消息出错:', error);
+    }
+  }
+
+  private async handleOtaMessage(deviceId: string, payload: string, messageType: string): Promise<void> {
+    try {
+      const data = JSON.parse(payload);
+      let taskId = data.task_id || data.tid;
+
+      if (taskId === undefined || taskId === null) return;
+
+      let task = await OTATaskModel.getById(taskId);
+      if (!task) return;
+
+      if (messageType === 'ota/progress') {
+        await OTATaskModel.updateStatusAndProgress(taskId, task.status, data.progress || 0);
+      } else if (messageType === 'ota/status') {
+        const validStatuses = ['pending', 'downloading', 'installing', 'success', 'failed', 'ready'];
+        const status = data.status;
+        if (!validStatuses.includes(status)) return;
+
+        const progress = data.progress !== undefined ? data.progress : (status === 'success' || status === 'ready' ? 100 : task.progress);
+
+        if (status === 'ready') {
+          const firmware = await FirmwareFileModel.getById(task.firmware_id);
+          if (firmware && data.firmware_version) {
+            if (data.firmware_version === firmware.version) {
+              await OTATaskModel.updateStatusAndProgress(taskId, 'success', 100);
+            } else {
+              await DeviceModel.update(deviceId, { firmware_version: data.firmware_version });
+              await this.publishOtaCommand(taskId);
+              return;
+            }
+          } else {
+            await OTATaskModel.updateStatusAndProgress(taskId, 'success', 100);
+          }
+        } else {
+          await OTATaskModel.updateStatusAndProgress(taskId, status, progress);
+        }
+
+        if (status === 'success' || status === 'ready') {
+          if (data.firmware_version) {
+            await DeviceModel.update(deviceId, { firmware_version: data.firmware_version });
+          }
+        }
+
+        if (status === 'success' || status === 'failed') {
+          await OTATaskModel.updateResult(taskId, status as 'success' | 'failed', data.error_message);
+        }
+      }
+    } catch (error) {
+      console.error('处理OTA消息出错:', error);
+    }
+  }
+
+  private async handleRelayStateMessage(deviceId: string, payload: string): Promise<void> {
+    try {
+      const data = JSON.parse(payload);
+      this.broadcastToWebSocket('device_relay_state', {
+        deviceId,
+        state: data.state || data,
+        timestamp: new Date().toISOString()
+      });
+    } catch (error) {
+      console.error('处理继电器状态消息出错:', error);
+    }
+  }
+
+  private async handleRssiMessage(deviceId: string, payload: string): Promise<void> {
+    try {
+      const rssi = parseInt(payload, 10);
+      if (!isNaN(rssi)) {
+        await DeviceModel.update(deviceId, { rssi } as any);
+        this.broadcastToWebSocket('device_rssi', {
+          deviceId,
+          rssi,
+          timestamp: new Date().toISOString()
+        });
+      }
+    } catch (error) {
+      console.error('处理RSSI消息出错:', error);
+    }
+  }
+
+  private async handleWifiInfoMessage(deviceId: string, payload: string): Promise<void> {
+    try {
+      const data = JSON.parse(payload);
+      this.broadcastToWebSocket('device_wifi_info', {
+        deviceId,
+        ...data,
+        timestamp: new Date().toISOString()
+      });
+    } catch (error) {
+      console.error('处理WiFi信息消息出错:', error);
+    }
+  }
+
+  private async handleWifiStatusMessage(deviceId: string, payload: string): Promise<void> {
+    try {
+      const data = JSON.parse(payload);
+      this.broadcastToWebSocket('device_wifi_status', {
+        deviceId,
+        ...data,
+        timestamp: new Date().toISOString()
+      });
+    } catch (error) {
+      console.error('处理WiFi状态消息出错:', error);
+    }
+  }
+
+  private async handleSensorData(deviceId: string, topic: string, payload: string): Promise<void> {
+    try {
+      const { SensorDataModel } = require('../models/sensorData');
+      const data = JSON.parse(payload);
+
+      await SensorDataModel.create({
+        device_id: deviceId,
+        topic,
+        data_type: data.type || 'unknown',
+        value: payload,
+        timestamp: new Date()
+      });
+
+      this.broadcastToWebSocket('sensor_data', {
+        deviceId,
+        topic,
+        data,
+        timestamp: new Date().toISOString()
+      });
+    } catch (error) {
+      console.error('处理传感器数据出错:', error);
+    }
+  }
+
+  async publishOtaCommand(taskId: number): Promise<void> {
+    try {
+      const task = await OTATaskModel.getById(taskId);
+      if (!task) return;
+
+      const firmware = await FirmwareFileModel.getById(task.firmware_id);
+      if (!firmware) {
+        await OTATaskModel.updateResult(taskId, 'failed', '固件不存在');
+        return;
+      }
+
+      let otaServerUrl = process.env.OTA_SERVER_URL || process.env.BACKEND_URL || `http://localhost:${process.env.PORT || 3002}`;
+      otaServerUrl = otaServerUrl.replace(/\/$/, '');
+
+      const otaCommand = {
+        act: 'upgrade',
+        ver: firmware.version,
+        url: `${otaServerUrl}/api/ota/firmware/${firmware.id}`,
+        md5: firmware.md5sum,
+        tid: taskId,
+        rc: 3,
+        ri: 10000,
+        to: 30000
+      };
+
+      this.broker.publish({
+        topic: `device/${task.device_id}/ota`,
+        payload: Buffer.from(JSON.stringify(otaCommand)),
+        qos: 1,
+        retain: false
+      } as any, (err: any) => {
+        if (err) {
+          console.error(`OTA指令发布失败,任务ID: ${taskId}`, err);
+          OTATaskModel.updateResult(taskId, 'failed', `OTA指令发送失败: ${err.message}`);
+        } else {
+          console.log(`OTA指令已发布,任务ID: ${taskId}, 设备: ${task.device_id}`);
+        }
+      });
+    } catch (error) {
+      console.error('发布OTA指令出错:', error);
+    }
+  }
+
+  private async executePendingOTATasks(deviceId: string): Promise<void> {
+    try {
+      const incompleteTasks = await OTATaskModel.getIncompleteTasksByDeviceId(deviceId);
+      if (incompleteTasks.length === 0) return;
+
+      console.log(`设备 ${deviceId} 上线,发现 ${incompleteTasks.length} 个未完成OTA任务`);
+
+      for (const task of incompleteTasks) {
+        if (task.id) {
+          await this.publishOtaCommand(task.id);
+        }
+      }
+    } catch (error) {
+      console.error(`执行设备 ${deviceId} 的待处理OTA任务出错:`, error);
+    }
+  }
+
+  private startDeviceStatusSync(): void {
+    this.syncInterval = setInterval(async () => {
+      try {
+        const allDevices = await DeviceModel.getAll();
+        const onlineClientIds = new Set(this.connectedClients.keys());
+
+        for (const device of allDevices) {
+          const isOnline = onlineClientIds.has(device.clientid);
+          const newStatus = isOnline ? 'online' : 'offline';
+
+          if (device.status !== newStatus) {
+            await DeviceModel.update(device.clientid, {
+              status: newStatus,
+              last_event_time: new Date(),
+              last_online_time: isOnline ? new Date() : device.last_online_time,
+              last_offline_time: !isOnline ? new Date() : device.last_offline_time
+            });
+          }
+        }
+      } catch (error) {
+        console.error('设备状态同步出错:', error);
+      }
+    }, 30000);
+  }
+
+  getConnectedClients(): BrokerClient[] {
+    return Array.from(this.connectedClients.values());
+  }
+
+  getConnectedClientCount(): number {
+    return this.connectedClients.size;
+  }
+
+  getBroker(): InstanceType<typeof Aedes> {
+    return this.broker;
+  }
+
+  disconnectClient(clientId: string): boolean {
+    const clients = (this.broker as any).clients;
+    const client = clients ? clients[clientId] : null;
+    if (client) {
+      client.close();
+      return true;
+    }
+    return false;
+  }
+
+  publish(topic: string, payload: string | Buffer, options?: { qos?: number; retain?: boolean }): Promise<void> {
+    return new Promise((resolve, reject) => {
+      this.broker.publish({
+        topic,
+        payload: Buffer.isBuffer(payload) ? payload : Buffer.from(payload),
+        qos: options?.qos || 0,
+        retain: options?.retain || false
+      } as any, (err: any) => {
+        if (err) reject(err);
+        else resolve();
+      });
+    });
+  }
+
+  private detectPayloadFormat(payload: string): string {
+    try {
+      JSON.parse(payload);
+      return 'json';
+    } catch {
+      if (/^[0-9a-fA-F]+$/.test(payload)) return 'bin';
+      return 'text';
+    }
+  }
+
+  private logAuth(clientId: string, username: string, ip: string, operationType: 'connect' | 'publish' | 'subscribe' | 'disconnect', result: 'success' | 'failure', reason: string): void {
+    AuthLogModel.create({
+      clientid: clientId,
+      username,
+      ip_address: ip,
+      operation_type: operationType,
+      result,
+      reason
+    }).catch((err: Error) => {
+      console.error('写入认证日志失败:', err);
+    });
+  }
+
+  private broadcastToWebSocket(event: string, data: any): void {
+    try {
+      const wsService = getWebSocketService();
+      if (wsService) {
+        const io = (wsService as any).io;
+        if (io) {
+          io.emit(event, data);
+        }
+      }
+    } catch (error) {
+      // WebSocket服务可能还未初始化
+    }
+  }
+}

+ 1174 - 0
mqtt-vue-dashboard/server/src/services/websocketService.ts

@@ -0,0 +1,1174 @@
+// WebSocket服务,用于处理实时数据推送
+import { Server as SocketIOServer } from 'socket.io';
+import { Server as HTTPServer } from 'http';
+import { DeviceModel } from '../models/device';
+import { MqttMessageModel } from '../models/mqttMessage';
+import { ClientConnectionModel } from '../models/clientConnection';
+import { LoggerService } from './loggerService';
+import { MqttBrokerService } from './mqttBrokerService';
+
+interface DeviceData {
+  devices: any[];
+  stats: {
+    total: number;
+    online: number;
+    offline: number;
+    onlineRate: number;
+  };
+  timestamp: number;
+}
+
+interface ConnectionStats {
+  totalConnections: number;
+  activeConnections: number;
+  timestamp: number;
+}
+
+interface MessageStats {
+  messagesReceived: number;
+  messagesSent: number;
+  totalMessages: number;
+  timestamp: number;
+}
+
+interface RecentConnections {
+  connections: any[];
+  timestamp: number;
+}
+
+interface RecentMessages {
+  messages: any[];
+  timestamp: number;
+}
+
+interface DeviceStatusDistribution {
+  online: number;
+  offline: number;
+  unknown: number;
+  timestamp: number;
+}
+
+class WebSocketService {
+  private io: SocketIOServer;
+  private dataUpdateIntervals: NodeJS.Timeout[] = [];
+  private lastDeviceData: DeviceData | null = null;
+  private lastConnectionStats: ConnectionStats | null = null;
+  private lastMessageStats: MessageStats | null = null;
+  private lastRecentConnections: RecentConnections | null = null;
+  private lastRecentMessages: RecentMessages | null = null;
+  private lastDeviceStatusDistribution: DeviceStatusDistribution | null = null;
+
+  constructor(server: HTTPServer) {
+    this.io = new SocketIOServer(server, {
+      cors: {
+        origin: process.env.NODE_ENV === 'production' ? true : ["http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:3002", "http://127.0.0.1:3002"],
+        methods: ["GET", "POST"]
+      }
+    });
+
+    // 记录WebSocket服务初始化
+    LoggerService.info('WebSocket服务初始化', {
+      source: 'websocket',
+      module: 'init',
+      details: JSON.stringify({
+        environment: process.env.NODE_ENV || 'development'
+      })
+    }).catch(err => {
+      console.error('WebSocket初始化日志写入失败:', err);
+    });
+
+    this.initializeSocketEvents();
+  }
+
+  // 初始化Socket事件监听
+  private initializeSocketEvents() {
+    this.io.on('connection', (socket) => {
+      console.log('客户端已连接到WebSocket服务器:', socket.id);
+      
+      // 记录客户端连接日志
+      LoggerService.info('客户端已连接到WebSocket服务器', {
+        source: 'websocket',
+        module: 'connection',
+        details: JSON.stringify({
+          socketId: socket.id,
+          clientIp: socket.handshake.address,
+          connectedClients: this.getConnectedClientsCount()
+        })
+      }).catch(err => {
+        console.error('WebSocket连接日志写入失败:', err);
+      });
+
+      // 处理客户端请求特定数据
+      socket.on('request_device_data', async () => {
+        try {
+          console.log('[WebSocket] 收到设备数据请求,来自客户端:', socket.id);
+          
+          // 记录数据请求日志
+          LoggerService.info('收到设备数据请求', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_device_data'
+            })
+          }).catch(err => {
+            console.error('WebSocket设备数据请求日志写入失败:', err);
+          });
+          
+          const deviceData = await this.getDeviceData();
+          socket.emit('device_data', deviceData);
+          
+          // 记录数据响应日志
+          LoggerService.info('发送设备数据响应', {
+            source: 'websocket',
+            module: 'response',
+            details: JSON.stringify({
+              socketId: socket.id,
+              responseType: 'device_data',
+              deviceCount: deviceData.devices.length
+            })
+          }).catch(err => {
+            console.error('WebSocket设备数据响应日志写入失败:', err);
+          });
+        } catch (error) {
+          console.error('获取设备数据时出错:', error);
+          socket.emit('error', { message: '获取设备数据失败' });
+          
+          // 记录数据请求错误日志
+          LoggerService.error('获取设备数据时出错', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_device_data',
+              error: error instanceof Error ? error.message : '未知错误'
+            })
+          }).catch(err => {
+            console.error('WebSocket设备数据请求错误日志写入失败:', err);
+          });
+        }
+      });
+
+      socket.on('request_connection_stats', async () => {
+        try {
+          console.log('[WebSocket] 收到连接统计请求,来自客户端:', socket.id);
+          
+          // 记录数据请求日志
+          LoggerService.info('收到连接统计请求', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_connection_stats'
+            })
+          }).catch(err => {
+            console.error('WebSocket连接统计请求日志写入失败:', err);
+          });
+          
+          const connectionStats = await this.getConnectionStats();
+          socket.emit('connection_stats', connectionStats);
+          
+          // 记录数据响应日志
+          LoggerService.info('发送连接统计响应', {
+            source: 'websocket',
+            module: 'response',
+            details: JSON.stringify({
+              socketId: socket.id,
+              responseType: 'connection_stats',
+              activeConnections: connectionStats.activeConnections
+            })
+          }).catch(err => {
+            console.error('WebSocket连接统计响应日志写入失败:', err);
+          });
+        } catch (error) {
+          console.error('获取连接统计时出错:', error);
+          socket.emit('error', { message: '获取连接统计失败' });
+          
+          // 记录数据请求错误日志
+          LoggerService.error('获取连接统计时出错', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_connection_stats',
+              error: error instanceof Error ? error.message : '未知错误'
+            })
+          }).catch(err => {
+            console.error('WebSocket连接统计请求错误日志写入失败:', err);
+          });
+        }
+      });
+
+      socket.on('request_message_stats', async () => {
+        try {
+          console.log('[WebSocket] 收到消息统计请求,来自客户端:', socket.id);
+          
+          // 记录数据请求日志
+          LoggerService.info('收到消息统计请求', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_message_stats'
+            })
+          }).catch(err => {
+            console.error('WebSocket消息统计请求日志写入失败:', err);
+          });
+          
+          const messageStats = await this.getMessageStats();
+          socket.emit('message_stats', messageStats);
+          
+          // 记录数据响应日志
+          LoggerService.info('发送消息统计响应', {
+            source: 'websocket',
+            module: 'response',
+            details: JSON.stringify({
+              socketId: socket.id,
+              responseType: 'message_stats',
+              totalMessages: messageStats.totalMessages
+            })
+          }).catch(err => {
+            console.error('WebSocket消息统计响应日志写入失败:', err);
+          });
+        } catch (error) {
+          console.error('获取消息统计时出错:', error);
+          socket.emit('error', { message: '获取消息统计失败' });
+          
+          // 记录数据请求错误日志
+          LoggerService.error('获取消息统计时出错', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_message_stats',
+              error: error instanceof Error ? error.message : '未知错误'
+            })
+          }).catch(err => {
+            console.error('WebSocket消息统计请求错误日志写入失败:', err);
+          });
+        }
+      });
+
+      socket.on('request_recent_connections', async () => {
+        try {
+          console.log('[WebSocket] 收到最近连接请求,来自客户端:', socket.id);
+          
+          // 记录数据请求日志
+          LoggerService.info('收到最近连接请求', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_recent_connections'
+            })
+          }).catch(err => {
+            console.error('WebSocket最近连接请求日志写入失败:', err);
+          });
+          
+          const recentConnections = await this.getRecentConnections();
+          socket.emit('recent_connections', recentConnections);
+          
+          // 记录数据响应日志
+          LoggerService.info('发送最近连接响应', {
+            source: 'websocket',
+            module: 'response',
+            details: JSON.stringify({
+              socketId: socket.id,
+              responseType: 'recent_connections',
+              connectionCount: recentConnections.connections.length
+            })
+          }).catch(err => {
+            console.error('WebSocket最近连接响应日志写入失败:', err);
+          });
+        } catch (error) {
+          console.error('获取最近连接时出错:', error);
+          socket.emit('error', { message: '获取最近连接失败' });
+          
+          // 记录数据请求错误日志
+          LoggerService.error('获取最近连接时出错', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_recent_connections',
+              error: error instanceof Error ? error.message : '未知错误'
+            })
+          }).catch(err => {
+            console.error('WebSocket最近连接请求错误日志写入失败:', err);
+          });
+        }
+      });
+
+      socket.on('request_recent_messages', async () => {
+        try {
+          console.log('[WebSocket] 收到最近消息请求,来自客户端:', socket.id);
+          
+          // 记录数据请求日志
+          LoggerService.info('收到最近消息请求', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_recent_messages'
+            })
+          }).catch(err => {
+            console.error('WebSocket最近消息请求日志写入失败:', err);
+          });
+          
+          const recentMessages = await this.getRecentMessages();
+          socket.emit('recent_messages', recentMessages);
+          
+          // 记录数据响应日志
+          LoggerService.info('发送最近消息响应', {
+            source: 'websocket',
+            module: 'response',
+            details: JSON.stringify({
+              socketId: socket.id,
+              responseType: 'recent_messages',
+              messageCount: recentMessages.messages.length
+            })
+          }).catch(err => {
+            console.error('WebSocket最近消息响应日志写入失败:', err);
+          });
+        } catch (error) {
+          console.error('获取最近消息时出错:', error);
+          socket.emit('error', { message: '获取最近消息失败' });
+          
+          // 记录数据请求错误日志
+          LoggerService.error('获取最近消息时出错', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_recent_messages',
+              error: error instanceof Error ? error.message : '未知错误'
+            })
+          }).catch(err => {
+            console.error('WebSocket最近消息请求错误日志写入失败:', err);
+          });
+        }
+      });
+
+      socket.on('request_device_status_distribution', async () => {
+        console.log('[WebSocket] 收到设备状态分布请求,来自客户端:', socket.id);
+        
+        // 记录数据请求日志
+        LoggerService.info('收到设备状态分布请求', {
+          source: 'websocket',
+          module: 'request',
+          details: JSON.stringify({
+            socketId: socket.id,
+            requestType: 'request_device_status_distribution'
+          })
+        }).catch(err => {
+          console.error('WebSocket设备状态分布请求日志写入失败:', err);
+        });
+        
+        try {
+          const deviceStatusDistribution = await this.getDeviceStatusDistribution();
+          console.log('[WebSocket] 发送设备状态分布数据给客户端:', socket.id, deviceStatusDistribution);
+          socket.emit('device_status_distribution', deviceStatusDistribution);
+          
+          // 记录数据响应日志
+          LoggerService.info('发送设备状态分布响应', {
+            source: 'websocket',
+            module: 'response',
+            details: JSON.stringify({
+              socketId: socket.id,
+              responseType: 'device_status_distribution',
+              online: deviceStatusDistribution.online,
+              offline: deviceStatusDistribution.offline,
+              unknown: deviceStatusDistribution.unknown
+            })
+          }).catch(err => {
+            console.error('WebSocket设备状态分布响应日志写入失败:', err);
+          });
+        } catch (error) {
+          console.error('获取设备状态分布时出错:', error);
+          socket.emit('error', { message: '获取设备状态分布失败' });
+          
+          // 记录数据请求错误日志
+          LoggerService.error('获取设备状态分布时出错', {
+            source: 'websocket',
+            module: 'request',
+            details: JSON.stringify({
+              socketId: socket.id,
+              requestType: 'request_device_status_distribution',
+              error: error instanceof Error ? error.message : '未知错误'
+            })
+          }).catch(err => {
+            console.error('WebSocket设备状态分布请求错误日志写入失败:', err);
+          });
+        }
+      });
+
+      socket.on('disconnect', () => {
+        console.log('客户端已断开连接:', socket.id);
+        
+        // 记录客户端断开连接日志
+        LoggerService.info('客户端已断开连接', {
+          source: 'websocket',
+          module: 'connection',
+          details: JSON.stringify({
+            socketId: socket.id,
+            connectedClients: this.getConnectedClientsCount()
+          })
+        }).catch(err => {
+          console.error('WebSocket断开连接日志写入失败:', err);
+        });
+      });
+    });
+  }
+
+  // 获取设备数据
+  private async getDeviceData(): Promise<DeviceData> {
+    try {
+      const devices = await DeviceModel.getAll();
+      const stats = await DeviceModel.getDeviceStats();
+      
+      return {
+        devices,
+        stats,
+        timestamp: Date.now()
+      };
+    } catch (error) {
+      console.error('获取设备数据时出错:', error);
+      throw error;
+    }
+  }
+
+  // 获取连接统计数据
+  private async getConnectionStats(): Promise<ConnectionStats> {
+    try {
+      const mqttBroker = MqttBrokerService.getInstance();
+      const connectedClientCount = mqttBroker.getConnectedClientCount();
+      const stats = await DeviceModel.getDeviceStats();
+      
+      return {
+        totalConnections: stats.total || 0,
+        activeConnections: connectedClientCount,
+        timestamp: Date.now()
+      };
+    } catch (error) {
+      console.error('获取连接统计时出错:', error);
+      throw error;
+    }
+  }
+
+  // 获取消息统计数据
+  private async getMessageStats(): Promise<MessageStats> {
+    try {
+      const typeStats = await MqttMessageModel.getTypeStats();
+      const totalMessages = await MqttMessageModel.getCount();
+      
+      let publishCount = 0;
+      let subscribeCount = 0;
+      
+      typeStats.forEach(stat => {
+        if (stat.message_type === 'publish') {
+          publishCount = stat.count;
+        } else if (stat.message_type === 'subscribe') {
+          subscribeCount = stat.count;
+        }
+      });
+      
+      return {
+        messagesReceived: publishCount,
+        messagesSent: subscribeCount,
+        totalMessages,
+        timestamp: Date.now()
+      };
+    } catch (error) {
+      console.error('获取消息统计时出错:', error);
+      throw error;
+    }
+  }
+
+  // 获取最近连接数据
+  private async getRecentConnections(): Promise<RecentConnections> {
+    try {
+      const connections = await ClientConnectionModel.getAll(10);
+      
+      return {
+        connections,
+        timestamp: Date.now()
+      };
+    } catch (error) {
+      console.error('获取最近连接时出错:', error);
+      throw error;
+    }
+  }
+
+  // 获取最近消息数据
+  private async getRecentMessages(): Promise<RecentMessages> {
+    try {
+      const messages = await MqttMessageModel.getAll(10);
+      
+      return {
+        messages,
+        timestamp: Date.now()
+      };
+    } catch (error) {
+      console.error('获取最近消息时出错:', error);
+      throw error;
+    }
+  }
+
+  // 获取设备状态分布数据
+  private async getDeviceStatusDistribution(): Promise<DeviceStatusDistribution> {
+    try {
+      const mqttBroker = MqttBrokerService.getInstance();
+      const connectedClients = mqttBroker.getConnectedClients();
+      const connectedClientIds = new Set(connectedClients.map(c => c.id));
+      
+      const allDevices = await DeviceModel.getAll();
+      
+      let online = 0;
+      let offline = 0;
+      let unknown = 0;
+      
+      allDevices.forEach(device => {
+        if (connectedClientIds.has(device.clientid)) {
+          online++;
+        } else if (device.status === 'offline') {
+          offline++;
+        } else {
+          unknown++;
+        }
+      });
+      
+      const result = {
+        online,
+        offline,
+        unknown,
+        timestamp: Date.now()
+      };
+      
+      console.log('计算得到的设备状态分布:', result);
+      return result;
+    } catch (error) {
+      console.error('获取设备状态分布时出错:', error);
+      throw error;
+    }
+  }
+
+  // 检查设备数据是否发生变化
+  private hasDeviceDataChanged(currentData: DeviceData): boolean {
+    if (!this.lastDeviceData) return true;
+    
+    // 检查设备数量和状态是否变化
+    if (currentData.stats.total !== this.lastDeviceData.stats.total ||
+        currentData.stats.online !== this.lastDeviceData.stats.online ||
+        currentData.stats.offline !== this.lastDeviceData.stats.offline) {
+      return true;
+    }
+    
+    // 检查设备列表是否变化
+    if (currentData.devices.length !== this.lastDeviceData.devices.length) {
+      return true;
+    }
+    
+    // 检查每个设备的状态是否变化
+    for (let i = 0; i < currentData.devices.length; i++) {
+      const currentDevice = currentData.devices[i];
+      const lastDevice = this.lastDeviceData.devices.find(d => d.clientid === currentDevice.clientid);
+      
+      if (!lastDevice || lastDevice.status !== currentDevice.status) {
+        return true;
+      }
+    }
+    
+    return false;
+  }
+
+  // 检查连接统计是否发生变化
+  private hasConnectionStatsChanged(currentStats: ConnectionStats): boolean {
+    if (!this.lastConnectionStats) return true;
+    
+    // 如果连接数变化超过5%,认为有变化
+    const totalChangePercent = Math.abs(
+      (currentStats.totalConnections - this.lastConnectionStats.totalConnections) / 
+      (this.lastConnectionStats.totalConnections || 1)
+    );
+    
+    const activeChangePercent = Math.abs(
+      (currentStats.activeConnections - this.lastConnectionStats.activeConnections) / 
+      (this.lastConnectionStats.activeConnections || 1)
+    );
+    
+    return totalChangePercent > 0.05 || activeChangePercent > 0.05;
+  }
+
+  // 检查消息统计是否发生变化
+  private hasMessageStatsChanged(currentStats: MessageStats): boolean {
+    if (!this.lastMessageStats) return true;
+    
+    // 如果消息数变化超过1%,认为有变化
+    const totalChangePercent = Math.abs(
+      (currentStats.totalMessages - this.lastMessageStats.totalMessages) / 
+      (this.lastMessageStats.totalMessages || 1)
+    );
+    
+    return totalChangePercent > 0.01;
+  }
+
+  // 检查最近连接是否发生变化
+  private hasRecentConnectionsChanged(currentConnections: RecentConnections): boolean {
+    if (!this.lastRecentConnections) return true;
+    
+    // 如果连接列表长度不同,认为有变化
+    if (currentConnections.connections.length !== this.lastRecentConnections.connections.length) {
+      return true;
+    }
+    
+    // 如果连接列表为空,认为无变化
+    if (currentConnections.connections.length === 0) {
+      return false;
+    }
+    
+    // 检查前5条连接的ID和时间戳是否完全相同
+    const checkCount = Math.min(5, currentConnections.connections.length);
+    for (let i = 0; i < checkCount; i++) {
+      const currentConn = currentConnections.connections[i];
+      const lastConn = this.lastRecentConnections.connections[i];
+      
+      // 如果ID或时间戳不同,认为有变化
+      if (currentConn.id !== lastConn.id || currentConn.timestamp !== lastConn.timestamp) {
+        return true;
+      }
+    }
+    
+    // 前5条连接完全相同,认为无变化
+    return false;
+  }
+
+  // 检查最近消息是否发生变化
+  private hasRecentMessagesChanged(currentMessages: RecentMessages): boolean {
+    if (!this.lastRecentMessages) return true;
+    
+    // 如果消息列表长度不同,认为有变化
+    if (currentMessages.messages.length !== this.lastRecentMessages.messages.length) {
+      return true;
+    }
+    
+    // 如果消息列表为空,认为无变化
+    if (currentMessages.messages.length === 0) {
+      return false;
+    }
+    
+    // 检查前5条消息的ID和时间戳是否完全相同
+    const checkCount = Math.min(5, currentMessages.messages.length);
+    for (let i = 0; i < checkCount; i++) {
+      const currentMsg = currentMessages.messages[i];
+      const lastMsg = this.lastRecentMessages.messages[i];
+      
+      // 如果ID或时间戳不同,认为有变化
+      if (currentMsg.id !== lastMsg.id || currentMsg.timestamp !== lastMsg.timestamp) {
+        return true;
+      }
+    }
+    
+    // 前5条消息完全相同,认为无变化
+    return false;
+  }
+
+  // 检查设备状态分布是否发生变化
+  private hasDeviceStatusDistributionChanged(currentDistribution: DeviceStatusDistribution): boolean {
+    if (!this.lastDeviceStatusDistribution) {
+      console.log('首次获取设备状态分布数据,标记为已变化');
+      return true;
+    }
+    
+    // 如果任何状态数量发生变化,认为有变化
+    const hasChanged = (
+      currentDistribution.online !== this.lastDeviceStatusDistribution.online ||
+      currentDistribution.offline !== this.lastDeviceStatusDistribution.offline ||
+      currentDistribution.unknown !== this.lastDeviceStatusDistribution.unknown
+    );
+    
+    if (hasChanged) {
+      console.log('设备状态分布发生变化:', {
+        current: currentDistribution,
+        last: this.lastDeviceStatusDistribution
+      });
+    }
+    
+    return hasChanged;
+  }
+
+  // 启动实时数据更新
+  startRealDataUpdates() {
+    console.log('启动WebSocket实时数据更新服务');
+    
+    // 记录实时数据更新启动日志
+    LoggerService.info('启动WebSocket实时数据更新服务', {
+      source: 'websocket',
+      module: 'realtime',
+      details: JSON.stringify({
+        status: 'started'
+      })
+    }).catch(err => {
+      console.error('WebSocket实时数据更新启动日志写入失败:', err);
+    });
+
+    // 清除之前的定时器
+    this.stopDataUpdates();
+
+    // 设备数据更新 - 每3秒检查一次
+    const deviceInterval = setInterval(async () => {
+      try {
+        if (this.getConnectedClientsCount() === 0) return;
+        
+        const deviceData = await this.getDeviceData();
+        
+        // 只有数据变化时才推送
+        if (this.hasDeviceDataChanged(deviceData)) {
+          console.log('设备数据发生变化,推送更新');
+          
+          // 记录数据推送日志
+          LoggerService.info('设备数据发生变化,推送更新', {
+            source: 'websocket',
+            module: 'push',
+            details: JSON.stringify({
+              pushType: 'device_data',
+              deviceCount: deviceData.devices.length,
+              onlineDevices: deviceData.stats.online,
+              timestamp: deviceData.timestamp
+            })
+          }).catch(err => {
+            console.error('WebSocket设备数据推送日志写入失败:', err);
+          });
+          
+          this.io.emit('device_data', deviceData);
+          this.lastDeviceData = deviceData;
+          
+          // 检查设备状态变化并推送device_status消息
+          this.checkDeviceStatusChanges(deviceData);
+          // 检查设备连接变化并推送device_connection消息
+          this.checkDeviceConnectionChanges(deviceData);
+        }
+      } catch (error) {
+        console.error('获取设备数据时出错:', error);
+        
+        // 记录数据获取错误日志
+        LoggerService.error('获取设备数据时出错', {
+          source: 'websocket',
+          module: 'realtime',
+          details: JSON.stringify({
+            error: error instanceof Error ? error.message : '未知错误',
+            intervalType: 'device_data'
+          })
+        }).catch(err => {
+          console.error('WebSocket设备数据获取错误日志写入失败:', err);
+        });
+      }
+    }, 3000);
+
+    // 连接统计更新 - 每5秒检查一次
+    const connectionInterval = setInterval(async () => {
+      try {
+        if (this.getConnectedClientsCount() === 0) return;
+        
+        const connectionStats = await this.getConnectionStats();
+        
+        // 只有数据变化时才推送
+        if (this.hasConnectionStatsChanged(connectionStats)) {
+          console.log('连接统计发生变化,推送更新');
+          
+          // 记录数据推送日志
+          LoggerService.info('连接统计发生变化,推送更新', {
+            source: 'websocket',
+            module: 'push',
+            details: JSON.stringify({
+              pushType: 'connection_stats',
+              totalConnections: connectionStats.totalConnections,
+              activeConnections: connectionStats.activeConnections,
+              timestamp: connectionStats.timestamp
+            })
+          }).catch(err => {
+            console.error('WebSocket连接统计推送日志写入失败:', err);
+          });
+          
+          this.io.emit('connection_stats', connectionStats);
+          this.lastConnectionStats = connectionStats;
+        }
+      } catch (error) {
+        console.error('获取连接统计时出错:', error);
+        
+        // 记录数据获取错误日志
+        LoggerService.error('获取连接统计时出错', {
+          source: 'websocket',
+          module: 'realtime',
+          details: JSON.stringify({
+            error: error instanceof Error ? error.message : '未知错误',
+            intervalType: 'connection_stats'
+          })
+        }).catch(err => {
+          console.error('WebSocket连接统计获取错误日志写入失败:', err);
+        });
+      }
+    }, 5000);
+
+    // 消息统计更新 - 每4秒检查一次
+    const messageInterval = setInterval(async () => {
+      try {
+        if (this.getConnectedClientsCount() === 0) return;
+        
+        const messageStats = await this.getMessageStats();
+        
+        // 只有数据变化时才推送
+        if (this.hasMessageStatsChanged(messageStats)) {
+          console.log('消息统计发生变化,推送更新');
+          
+          // 记录数据推送日志
+          LoggerService.info('消息统计发生变化,推送更新', {
+            source: 'websocket',
+            module: 'push',
+            details: JSON.stringify({
+              pushType: 'message_stats',
+              totalMessages: messageStats.totalMessages,
+              messagesReceived: messageStats.messagesReceived,
+              messagesSent: messageStats.messagesSent,
+              timestamp: messageStats.timestamp
+            })
+          }).catch(err => {
+            console.error('WebSocket消息统计推送日志写入失败:', err);
+          });
+          
+          this.io.emit('message_stats', messageStats);
+          this.lastMessageStats = messageStats;
+        }
+      } catch (error) {
+        console.error('获取消息统计时出错:', error);
+        
+        // 记录数据获取错误日志
+        LoggerService.error('获取消息统计时出错', {
+          source: 'websocket',
+          module: 'realtime',
+          details: JSON.stringify({
+            error: error instanceof Error ? error.message : '未知错误',
+            intervalType: 'message_stats'
+          })
+        }).catch(err => {
+          console.error('WebSocket消息统计获取错误日志写入失败:', err);
+        });
+      }
+    }, 4000);
+
+    // 最近连接更新 - 每2秒检查一次
+    const recentConnectionsInterval = setInterval(async () => {
+      try {
+        if (this.getConnectedClientsCount() === 0) return;
+        
+        const recentConnections = await this.getRecentConnections();
+        
+        // 只有数据变化时才推送
+        if (this.hasRecentConnectionsChanged(recentConnections)) {
+          console.log('最近连接发生变化,推送更新');
+          
+          // 记录数据推送日志
+          LoggerService.info('最近连接发生变化,推送更新', {
+            source: 'websocket',
+            module: 'push',
+            details: JSON.stringify({
+              pushType: 'recent_connections',
+              connectionCount: recentConnections.connections.length,
+              timestamp: recentConnections.timestamp
+            })
+          }).catch(err => {
+            console.error('WebSocket最近连接推送日志写入失败:', err);
+          });
+          
+          this.io.emit('recent_connections', recentConnections);
+          this.lastRecentConnections = recentConnections;
+        }
+      } catch (error) {
+        console.error('获取最近连接时出错:', error);
+        
+        // 记录数据获取错误日志
+        LoggerService.error('获取最近连接时出错', {
+          source: 'websocket',
+          module: 'realtime',
+          details: JSON.stringify({
+            error: error instanceof Error ? error.message : '未知错误',
+            intervalType: 'recent_connections'
+          })
+        }).catch(err => {
+          console.error('WebSocket最近连接获取错误日志写入失败:', err);
+        });
+      }
+    }, 2000);
+
+    // 最近消息更新 - 每2秒检查一次
+    const recentMessagesInterval = setInterval(async () => {
+      try {
+        if (this.getConnectedClientsCount() === 0) return;
+        
+        const recentMessages = await this.getRecentMessages();
+        
+        // 只有数据变化时才推送
+        if (this.hasRecentMessagesChanged(recentMessages)) {
+          console.log('最近消息发生变化,推送更新');
+          
+          // 记录数据推送日志
+          LoggerService.info('最近消息发生变化,推送更新', {
+            source: 'websocket',
+            module: 'push',
+            details: JSON.stringify({
+              pushType: 'recent_messages',
+              messageCount: recentMessages.messages.length,
+              timestamp: recentMessages.timestamp
+            })
+          }).catch(err => {
+            console.error('WebSocket最近消息推送日志写入失败:', err);
+          });
+          
+          this.io.emit('recent_messages', recentMessages);
+          this.lastRecentMessages = recentMessages;
+        }
+      } catch (error) {
+        console.error('获取最近消息时出错:', error);
+        
+        // 记录数据获取错误日志
+        LoggerService.error('获取最近消息时出错', {
+          source: 'websocket',
+          module: 'realtime',
+          details: JSON.stringify({
+            error: error instanceof Error ? error.message : '未知错误',
+            intervalType: 'recent_messages'
+          })
+        }).catch(err => {
+          console.error('WebSocket最近消息获取错误日志写入失败:', err);
+        });
+      }
+    }, 2000);
+
+    // 设备状态分布更新 - 每3秒检查一次
+    const deviceStatusDistributionInterval = setInterval(async () => {
+      try {
+        if (this.getConnectedClientsCount() === 0) return;
+        
+        const deviceStatusDistribution = await this.getDeviceStatusDistribution();
+        
+        // 只有数据变化时才推送
+        if (this.hasDeviceStatusDistributionChanged(deviceStatusDistribution)) {
+          console.log('设备状态分布发生变化,推送更新');
+          
+          // 记录数据推送日志
+          LoggerService.info('设备状态分布发生变化,推送更新', {
+            source: 'websocket',
+            module: 'push',
+            details: JSON.stringify({
+              pushType: 'device_status_distribution',
+              online: deviceStatusDistribution.online,
+              offline: deviceStatusDistribution.offline,
+              unknown: deviceStatusDistribution.unknown,
+              timestamp: deviceStatusDistribution.timestamp
+            })
+          }).catch(err => {
+            console.error('WebSocket设备状态分布推送日志写入失败:', err);
+          });
+          
+          this.io.emit('device_status_distribution', deviceStatusDistribution);
+          this.lastDeviceStatusDistribution = deviceStatusDistribution;
+        }
+      } catch (error) {
+        console.error('获取设备状态分布时出错:', error);
+        
+        // 记录数据获取错误日志
+        LoggerService.error('获取设备状态分布时出错', {
+          source: 'websocket',
+          module: 'realtime',
+          details: JSON.stringify({
+            error: error instanceof Error ? error.message : '未知错误',
+            intervalType: 'device_status_distribution'
+          })
+        }).catch(err => {
+          console.error('WebSocket设备状态分布获取错误日志写入失败:', err);
+        });
+      }
+    }, 3000);
+
+    // 保存定时器引用
+    this.dataUpdateIntervals.push(deviceInterval, connectionInterval, messageInterval, recentConnectionsInterval, recentMessagesInterval, deviceStatusDistributionInterval);
+  }
+
+  // 停止数据更新
+  stopDataUpdates() {
+    if (this.dataUpdateIntervals.length > 0) {
+      console.log('停止WebSocket数据更新');
+      
+      // 记录实时数据更新停止日志
+      LoggerService.info('停止WebSocket数据更新', {
+        source: 'websocket',
+        module: 'realtime',
+        details: JSON.stringify({
+          status: 'stopped',
+          intervalCount: this.dataUpdateIntervals.length
+        })
+      }).catch(err => {
+        console.error('WebSocket实时数据更新停止日志写入失败:', err);
+      });
+      
+      this.dataUpdateIntervals.forEach(interval => clearInterval(interval));
+      this.dataUpdateIntervals = [];
+    }
+  }
+
+  // 检查设备状态变化并推送device_status消息
+  private checkDeviceStatusChanges(currentData: DeviceData) {
+    if (!this.lastDeviceData) return;
+    
+    // 检查每个设备的状态是否变化
+    currentData.devices.forEach(currentDevice => {
+      const lastDevice = this.lastDeviceData!.devices.find(d => d.clientid === currentDevice.clientid);
+      
+      if (lastDevice && lastDevice.status !== currentDevice.status) {
+        console.log(`设备${currentDevice.clientid}状态变化: ${lastDevice.status} -> ${currentDevice.status}`);
+        
+        // 记录设备状态变化日志
+        LoggerService.info(`设备状态变化: ${lastDevice.status} -> ${currentDevice.status}`, {
+          source: 'websocket',
+          module: 'device',
+          details: JSON.stringify({
+            clientid: currentDevice.clientid,
+            deviceName: currentDevice.device_name,
+            oldStatus: lastDevice.status,
+            newStatus: currentDevice.status
+          })
+        }).catch(err => {
+          console.error('WebSocket设备状态变化日志写入失败:', err);
+        });
+        
+        this.io.emit('device_status', {
+          clientid: currentDevice.clientid,
+          status: currentDevice.status,
+          device_name: currentDevice.device_name,
+          timestamp: Date.now()
+        });
+      }
+    });
+  }
+  
+  // 检查设备连接变化并推送device_connection消息
+  private checkDeviceConnectionChanges(currentData: DeviceData) {
+    if (!this.lastDeviceData) return;
+    
+    // 检查新连接的设备
+    currentData.devices.forEach(currentDevice => {
+      const lastDevice = this.lastDeviceData!.devices.find(d => d.clientid === currentDevice.clientid);
+      
+      // 新设备或从离线变为在线
+      if ((!lastDevice && currentDevice.status === 'online') || 
+          (lastDevice && lastDevice.status === 'offline' && currentDevice.status === 'online')) {
+        console.log(`设备${currentDevice.clientid}连接`);
+        
+        // 记录设备连接日志
+        LoggerService.info('设备连接', {
+          source: 'websocket',
+          module: 'device',
+          details: JSON.stringify({
+            clientid: currentDevice.clientid,
+            deviceName: currentDevice.device_name,
+            status: 'connected'
+          })
+        }).catch(err => {
+          console.error('WebSocket设备连接日志写入失败:', err);
+        });
+        
+        this.io.emit('device_connection', {
+          clientid: currentDevice.clientid,
+          status: 'connected',
+          device_name: currentDevice.device_name,
+          timestamp: Date.now()
+        });
+      }
+      // 从在线变为离线
+      else if (lastDevice && lastDevice.status === 'online' && currentDevice.status === 'offline') {
+        console.log(`设备${currentDevice.clientid}断开连接`);
+        
+        // 记录设备断开连接日志
+        LoggerService.info('设备断开连接', {
+          source: 'websocket',
+          module: 'device',
+          details: JSON.stringify({
+            clientid: currentDevice.clientid,
+            deviceName: currentDevice.device_name,
+            status: 'disconnected'
+          })
+        }).catch(err => {
+          console.error('WebSocket设备断开连接日志写入失败:', err);
+        });
+        
+        this.io.emit('device_connection', {
+          clientid: currentDevice.clientid,
+          status: 'disconnected',
+          device_name: currentDevice.device_name,
+          timestamp: Date.now()
+        });
+      }
+    });
+  }
+  
+  // 广播继电器状态更新
+  broadcastRelayState(deviceId: string, status: string) {
+    console.log(`[WebSocket] 广播继电器状态更新: 设备ID=${deviceId}, 状态=${status}`);
+    
+    this.io.emit('relay_state', {
+      deviceId,
+      status,
+      timestamp: Date.now()
+    });
+  }
+  
+  // 广播WiFi信号强度更新
+  broadcastRssiUpdate(deviceId: string, rssi: number) {
+    console.log(`[WebSocket] 广播WiFi信号强度更新: 设备ID=${deviceId}, RSSI=${rssi} dBm`);
+    
+    this.io.emit('rssi_update', {
+      deviceId,
+      rssi,
+      timestamp: Date.now()
+    });
+  }
+  
+  // 广播WiFi信息更新
+  broadcastWifiInfoUpdate(deviceId: string, ssid: string) {
+    console.log(`[WebSocket] 广播WiFi信息更新: 设备ID=${deviceId}, SSID=${ssid}`);
+    
+    this.io.emit('wifi_info_update', {
+      deviceId,
+      ssid,
+      timestamp: Date.now()
+    });
+  }
+  
+  // 广播WiFi配置状态更新
+  broadcastWifiStatusUpdate(deviceId: string, status: string, configId?: number) {
+    console.log(`[WebSocket] 广播WiFi配置状态更新: 设备ID=${deviceId}, 状态=${status}`);
+    
+    this.io.emit('wifi_status_update', {
+      deviceId,
+      status,
+      configId,
+      timestamp: Date.now()
+    });
+  }
+  
+  // 获取连接的客户端数量
+  getConnectedClientsCount(): number {
+    return this.io.engine.clientsCount;
+  }
+}
+
+let wsServiceInstance: WebSocketService | null = null;
+
+export function setWebSocketService(service: WebSocketService) {
+  wsServiceInstance = service;
+}
+
+export function getWebSocketService(): WebSocketService | null {
+  return wsServiceInstance;
+}
+
+export default WebSocketService;

+ 39 - 0
mqtt-vue-dashboard/server/src/types/global.d.ts

@@ -0,0 +1,39 @@
+declare module 'jsonwebtoken' {
+  import { Secret, SignOptions, VerifyOptions } from 'jsonwebtoken';
+  
+  export function sign(payload: string | Buffer | object, secretOrPrivateKey: Secret, options?: SignOptions): string;
+  export function verify(token: string, secretOrPublicKey: Secret, options?: VerifyOptions): object | string;
+  export function decode(token: string, options?: { complete?: boolean; json?: boolean }): null | { [key: string]: any } | string;
+  
+  export class JsonWebTokenError extends Error {
+    name: 'JsonWebTokenError';
+    inner: Error;
+  }
+  
+  export class TokenExpiredError extends Error {
+    name: 'TokenExpiredError';
+    expiredAt: Date;
+  }
+  
+  export class NotBeforeError extends Error {
+    name: 'NotBeforeError';
+    date: Date;
+  }
+}
+
+declare module 'morgan' {
+  import { RequestHandler } from 'express';
+  
+  namespace morgan {
+    type FormatFn = (tokens: any, req: any, res: any) => string;
+    type TokenCallbackFn = (req: any, res: any, arg?: any) => any;
+    
+    function format(name: string, fmt: string | FormatFn): void;
+    function token(name: string, callback: TokenCallbackFn): void;
+    function compile(format: string): RequestHandler;
+  }
+  
+  function morgan(format?: string | FormatFn, options?: any): RequestHandler;
+  
+  export = morgan;
+}

+ 71 - 0
mqtt-vue-dashboard/server/src/utils/fileUtils.ts

@@ -0,0 +1,71 @@
+import fs from 'fs';
+import crypto from 'crypto';
+
+/**
+ * 生成文件的MD5哈希值
+ * @param filePath 文件路径
+ * @returns MD5哈希值
+ */
+export const generateMD5 = (filePath: string): Promise<string> => {
+  return new Promise((resolve, reject) => {
+    const hash = crypto.createHash('md5');
+    const stream = fs.createReadStream(filePath);
+
+    stream.on('error', reject);
+
+    stream.on('data', (chunk) => {
+      hash.update(chunk);
+    });
+
+    stream.on('end', () => {
+      const md5sum = hash.digest('hex');
+      resolve(md5sum);
+    });
+  });
+};
+
+/**
+ * 验证文件的MD5哈希值
+ * @param filePath 文件路径
+ * @param expectedMD5 预期的MD5哈希值
+ * @returns 是否匹配
+ */
+export const verifyMD5 = async (filePath: string, expectedMD5: string): Promise<boolean> => {
+  try {
+    const actualMD5 = await generateMD5(filePath);
+    return actualMD5 === expectedMD5;
+  } catch (error) {
+    console.error('验证MD5失败:', error);
+    return false;
+  }
+};
+
+/**
+ * 获取文件大小
+ * @param filePath 文件路径
+ * @returns 文件大小(字节)
+ */
+export const getFileSize = (filePath: string): number => {
+  try {
+    const stats = fs.statSync(filePath);
+    return stats.size;
+  } catch (error) {
+    console.error('获取文件大小失败:', error);
+    return 0;
+  }
+};
+
+/**
+ * 删除文件
+ * @param filePath 文件路径
+ * @returns 是否删除成功
+ */
+export const deleteFile = (filePath: string): boolean => {
+  try {
+    fs.unlinkSync(filePath);
+    return true;
+  } catch (error) {
+    console.error('删除文件失败:', error);
+    return false;
+  }
+};

+ 226 - 0
mqtt-vue-dashboard/server/src/utils/helpers.ts

@@ -0,0 +1,226 @@
+// 日期格式化工具
+export const formatDate = (date: Date | string): string => {
+  const d = new Date(date);
+  return d.toISOString().split('T')[0];
+};
+
+// 日期时间格式化工具
+export const formatDateTime = (date: Date | string): string => {
+  const d = new Date(date);
+  return d.toISOString();
+};
+
+// 时间戳格式化工具
+export const formatTimestamp = (timestamp: number): string => {
+  return new Date(timestamp).toISOString();
+};
+
+// 获取当前时间戳
+export const getCurrentTimestamp = (): number => {
+  return Date.now();
+};
+
+// 获取当前日期时间字符串
+export const getCurrentDateTime = (): string => {
+  return new Date().toISOString();
+};
+
+// 计算时间差(秒)
+export const getTimeDifferenceInSeconds = (date1: Date | string, date2: Date | string): number => {
+  const d1 = new Date(date1);
+  const d2 = new Date(date2);
+  return Math.abs((d1.getTime() - d2.getTime()) / 1000);
+};
+
+// 计算时间差(分钟)
+export const getTimeDifferenceInMinutes = (date1: Date | string, date2: Date | string): number => {
+  return getTimeDifferenceInSeconds(date1, date2) / 60;
+};
+
+// 计算时间差(小时)
+export const getTimeDifferenceInHours = (date1: Date | string, date2: Date | string): number => {
+  return getTimeDifferenceInMinutes(date1, date2) / 60;
+};
+
+// 计算时间差(天)
+export const getTimeDifferenceInDays = (date1: Date | string, date2: Date | string): number => {
+  return getTimeDifferenceInHours(date1, date2) / 24;
+};
+
+// 获取N天前的日期
+export const getDateDaysAgo = (days: number): Date => {
+  const date = new Date();
+  date.setDate(date.getDate() - days);
+  return date;
+};
+
+// 获取N小时前的时间
+export const getDateHoursAgo = (hours: number): Date => {
+  const date = new Date();
+  date.setHours(date.getHours() - hours);
+  return date;
+};
+
+// 获取N分钟前的时间
+export const getDateMinutesAgo = (minutes: number): Date => {
+  const date = new Date();
+  date.setMinutes(date.getMinutes() - minutes);
+  return date;
+};
+
+// 生成随机ID
+export const generateId = (): string => {
+  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
+};
+
+// 深拷贝对象
+export const deepClone = <T>(obj: T): T => {
+  if (obj === null || typeof obj !== 'object') {
+    return obj;
+  }
+  
+  if (obj instanceof Date) {
+    return new Date(obj.getTime()) as unknown as T;
+  }
+  
+  if (obj instanceof Array) {
+    return obj.map(item => deepClone(item)) as unknown as T;
+  }
+  
+  if (typeof obj === 'object') {
+    const clonedObj = {} as T;
+    for (const key in obj) {
+      if (obj.hasOwnProperty(key)) {
+        clonedObj[key] = deepClone(obj[key]);
+      }
+    }
+    return clonedObj;
+  }
+  
+  return obj;
+};
+
+// 睡眠函数
+export const sleep = (ms: number): Promise<void> => {
+  return new Promise(resolve => setTimeout(resolve, ms));
+};
+
+// 重试函数
+export const retry = async <T>(
+  fn: () => Promise<T>,
+  maxAttempts: number = 3,
+  delayMs: number = 1000
+): Promise<T> => {
+  let lastError: Error;
+  
+  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+    try {
+      return await fn();
+    } catch (error) {
+      lastError = error as Error;
+      console.error(`尝试 ${attempt}/${maxAttempts} 失败:`, error);
+      
+      if (attempt < maxAttempts) {
+        console.log(`${delayMs}ms后重试...`);
+        await sleep(delayMs);
+      }
+    }
+  }
+  
+  throw lastError!;
+};
+
+// 验证IP地址格式
+export const isValidIpAddress = (ip: string): boolean => {
+  const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
+  return ipv4Regex.test(ip);
+};
+
+// 验证MQTT主题格式
+export const isValidMqttTopic = (topic: string): boolean => {
+  if (!topic || topic.length > 65535) {
+    return false;
+  }
+  
+  // 检查是否包含通配符
+  const hasWildcards = topic.includes('#') || topic.includes('+');
+  
+  // 如果包含通配符,检查位置是否合法
+  if (hasWildcards) {
+    // # 只能作为最后一个字符
+    if (topic.includes('#') && topic.lastIndexOf('#') !== topic.length - 1) {
+      return false;
+    }
+    
+    // + 必须占据整个层级
+    const parts = topic.split('/');
+    for (const part of parts) {
+      if (part === '+') {
+        continue;
+      }
+      if (part.includes('+')) {
+        return false;
+      }
+    }
+  }
+  
+  return true;
+};
+
+// 格式化字节数
+export const formatBytes = (bytes: number, decimals: number = 2): string => {
+  if (bytes === 0) return '0 Bytes';
+  
+  const k = 1024;
+  const dm = decimals < 0 ? 0 : decimals;
+  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+  
+  const i = Math.floor(Math.log(bytes) / Math.log(k));
+  
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+};
+
+// 生成随机颜色
+export const generateRandomColor = (): string => {
+  return '#' + Math.floor(Math.random()*16777215).toString(16);
+};
+
+// 限制数组长度
+export const limitArrayLength = <T>(array: T[], maxLength: number): T[] => {
+  return array.length > maxLength ? array.slice(0, maxLength) : array;
+};
+
+// 分页工具
+export const paginate = <T>(
+  items: T[],
+  page: number = 1,
+  pageSize: number = 10
+): { items: T[], totalPages: number, currentPage: number, hasNext: boolean, hasPrev: boolean } => {
+  const totalPages = Math.ceil(items.length / pageSize);
+  const currentPage = Math.max(1, Math.min(page, totalPages));
+  const startIndex = (currentPage - 1) * pageSize;
+  const endIndex = startIndex + pageSize;
+  
+  return {
+    items: items.slice(startIndex, endIndex),
+    totalPages,
+    currentPage,
+    hasNext: currentPage < totalPages,
+    hasPrev: currentPage > 1
+  };
+};
+
+// 将 string | string[] | any | any[] | undefined 转换为 string
+export const toString = (value: string | string[] | any | any[] | undefined): string => {
+  if (Array.isArray(value)) {
+    const first = value[0];
+    if (typeof first === 'string') {
+      return first;
+    }
+    return String(first || '');
+  }
+  if (typeof value === 'string') {
+    return value;
+  }
+  return String(value || '');
+};

+ 39 - 0
mqtt-vue-dashboard/server/tsconfig.json

@@ -0,0 +1,39 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "module": "commonjs",
+    "lib": ["ES2020"],
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "removeComments": true,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "strictFunctionTypes": true,
+    "noImplicitThis": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "moduleResolution": "node",
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    },
+    "allowSyntheticDefaultImports": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true
+  },
+  "include": [
+    "src/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}

+ 146 - 0
mqtt-vue-dashboard/src/App.vue

@@ -0,0 +1,146 @@
+<template>
+  <a-config-provider :theme="themeConfig">
+    <router-view />
+  </a-config-provider>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted } from 'vue'
+import { theme } from 'ant-design-vue'
+import { useThemeStore } from '@/stores/theme'
+import { useAuthStore } from '@/stores/auth'
+import { useWebSocketStore } from '@/stores/websocket'
+
+const themeStore = useThemeStore()
+const authStore = useAuthStore()
+const wsStore = useWebSocketStore()
+
+const themeConfig = computed(() => ({
+  algorithm: themeStore.theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
+  token: {
+    colorPrimary: '#1890ff',
+    borderRadius: 6,
+    colorBgContainer: themeStore.themeColors.cardBackground,
+    colorBgElevated: themeStore.themeColors.cardBackground,
+    colorBgLayout: themeStore.themeColors.surface,
+    colorBorder: themeStore.themeColors.border,
+    colorBorderSecondary: themeStore.themeColors.border,
+    colorText: themeStore.themeColors.text,
+    colorTextSecondary: themeStore.themeColors.textSecondary,
+  }
+}))
+
+onMounted(async () => {
+  themeStore.applyThemeToDom()
+  await authStore.checkAuthStatus()
+  if (authStore.isAuthenticated) {
+    wsStore.initConnection()
+  }
+})
+</script>
+
+<style>
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  background: var(--theme-background);
+  color: var(--theme-text);
+  transition: background-color 0.3s ease, color 0.3s ease;
+}
+
+:root {
+  --theme-background: #ffffff;
+  --theme-surface: #f0f2f5;
+  --theme-primary: #1890ff;
+  --theme-secondary: #52c41a;
+  --theme-accent: #722ed1;
+  --theme-text: rgba(0, 0, 0, 0.88);
+  --theme-text-secondary: rgba(0, 0, 0, 0.45);
+  --theme-border: #f0f0f0;
+  --theme-shadow: rgba(0, 0, 0, 0.06);
+  --theme-card-background: #ffffff;
+  --theme-header-background: #ffffff;
+  --theme-sidebar-background: #ffffff;
+  --theme-success: #52c41a;
+  --theme-warning: #faad14;
+  --theme-error: #ff4d4f;
+  --theme-info: #1890ff;
+}
+
+.dark-theme {
+  --theme-background: #141414;
+  --theme-surface: #1a1a1a;
+  --theme-primary: #177ddc;
+  --theme-secondary: #49aa19;
+  --theme-accent: #531dab;
+  --theme-text: rgba(255, 255, 255, 0.88);
+  --theme-text-secondary: rgba(255, 255, 255, 0.45);
+  --theme-border: #303030;
+  --theme-shadow: rgba(0, 0, 0, 0.3);
+  --theme-card-background: #1f1f1f;
+  --theme-header-background: #1f1f1f;
+  --theme-sidebar-background: #141414;
+  --theme-success: #49aa19;
+  --theme-warning: #d89614;
+  --theme-error: #d32029;
+  --theme-info: #177ddc;
+}
+
+#app {
+  min-height: 100vh;
+}
+
+.ant-layout-sider {
+  transition: width 0.2s cubic-bezier(0.645, 0.045, 0.355, 1), min-width 0.2s cubic-bezier(0.645, 0.045, 0.355, 1), max-width 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
+}
+
+.ant-layout-sider .ant-layout-sider-children {
+  transition: none !important;
+}
+
+.ant-card {
+  border-radius: 8px !important;
+  box-shadow: 0 1px 2px 0 var(--theme-shadow) !important;
+}
+
+.ant-menu-inline {
+  border-inline-end: none !important;
+}
+
+.ant-drawer .ant-drawer-body {
+  padding: 0 !important;
+}
+
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+  background: rgba(0, 0, 0, 0.15);
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: rgba(0, 0, 0, 0.25);
+}
+
+.dark-theme ::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.15);
+}
+
+.dark-theme ::-webkit-scrollbar-thumb:hover {
+  background: rgba(255, 255, 255, 0.25);
+}
+</style>

File diff suppressed because it is too large
+ 0 - 0
mqtt-vue-dashboard/src/assets/vite.svg


+ 41 - 0
mqtt-vue-dashboard/src/composables/useRealtimeData.ts

@@ -0,0 +1,41 @@
+import { ref, onMounted, onUnmounted } from 'vue'
+import { useWebSocketStore } from '@/stores/websocket'
+
+export function useRealtimeData(type: string, callback: (data: any) => void) {
+  const wsStore = useWebSocketStore()
+  const callbackRef = ref(callback)
+
+  callbackRef.value = callback
+
+  let unsubscribe: (() => void) | null = null
+
+  onMounted(() => {
+    unsubscribe = wsStore.subscribe(type, (data: any) => {
+      callbackRef.value(data)
+    })
+  })
+
+  onUnmounted(() => {
+    if (unsubscribe) {
+      unsubscribe()
+    }
+  })
+}
+
+export function useIsMobile() {
+  const isMobile = ref(window.innerWidth < 768)
+
+  const handleResize = () => {
+    isMobile.value = window.innerWidth < 768
+  }
+
+  onMounted(() => {
+    window.addEventListener('resize', handleResize)
+  })
+
+  onUnmounted(() => {
+    window.removeEventListener('resize', handleResize)
+  })
+
+  return isMobile
+}

+ 5 - 0
mqtt-vue-dashboard/src/env.d.ts

@@ -0,0 +1,5 @@
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}

+ 588 - 0
mqtt-vue-dashboard/src/layouts/AppLayout.vue

@@ -0,0 +1,588 @@
+<template>
+  <a-layout class="app-layout">
+    <a-layout-sider
+      v-if="!isMobile"
+      v-model:collapsed="collapsed"
+      :trigger="null"
+      collapsible
+      :width="siderWidth"
+      :collapsed-width="collapsedWidth"
+      :theme="themeStore.theme"
+      :key="'sider-' + themeStore.theme"
+      class="app-sider"
+    >
+      <div class="sider-inner">
+        <div class="sider-logo" :class="{ 'sider-logo-collapsed': collapsed }">
+          <div class="logo-icon-wrapper">
+            <DashboardOutlined style="font-size: 20px;" />
+          </div>
+          <transition name="logo-text-fade">
+            <span v-show="!collapsed" class="logo-text">MQTT 仪表板</span>
+          </transition>
+        </div>
+
+        <div class="sider-menu-wrapper">
+          <a-menu
+            :theme="themeStore.theme"
+            mode="inline"
+            :selected-keys="selectedKeys"
+            :open-keys="openKeys"
+            :items="menuItems"
+            @click="handleMenuClick"
+            @openChange="onOpenChange"
+            class="sider-menu"
+          />
+        </div>
+
+        <div class="sider-footer" @click="toggleSider">
+          <div class="sider-footer-inner">
+            <component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" class="footer-icon" />
+            <transition name="logo-text-fade">
+              <span v-show="!collapsed" class="footer-text">收起菜单</span>
+            </transition>
+          </div>
+        </div>
+      </div>
+    </a-layout-sider>
+
+    <a-layout class="app-main" :class="{ 'app-main-mobile': isMobile }">
+      <a-layout-header class="app-header">
+        <div class="header-left">
+          <a-button
+            v-if="isMobile"
+            type="text"
+            class="trigger-btn"
+            @click="drawerVisible = true"
+          >
+            <template #icon>
+              <MenuOutlined />
+            </template>
+          </a-button>
+          <div class="breadcrumb-wrapper">
+            <span class="page-title">{{ currentPageTitle }}</span>
+          </div>
+        </div>
+
+        <a-space size="middle" align="center" class="header-right">
+          <a-tooltip :title="themeStore.theme === 'dark' ? '切换亮色模式' : '切换暗色模式'">
+            <a-button
+              type="text"
+              shape="circle"
+              @click="themeStore.toggleTheme()"
+              class="theme-toggle-btn"
+            >
+              <template #icon>
+                <BulbOutlined :style="{ color: themeStore.theme === 'dark' ? '#faad14' : undefined }" />
+              </template>
+            </a-button>
+          </a-tooltip>
+          <a-dropdown v-if="authStore.user">
+            <div class="user-info">
+              <a-avatar
+                :size="32"
+                :style="{
+                  backgroundColor: authStore.user.role === 'admin' ? '#f5222d' :
+                    authStore.user.role === 'user' ? '#52c41a' : '#1890ff'
+                }"
+              >
+                <template #icon><UserOutlined /></template>
+              </a-avatar>
+              <span v-if="!isMobile" class="user-name">{{ authStore.user.username }}</span>
+            </div>
+            <template #overlay>
+              <a-menu>
+                <a-menu-item key="role" disabled>
+                  <template #icon><SafetyOutlined /></template>
+                  {{ authStore.user.role === 'admin' ? '管理员' : '普通用户' }}
+                </a-menu-item>
+                <a-menu-divider />
+                <a-menu-item key="logout" @click="handleLogout" danger>
+                  <template #icon><LogoutOutlined /></template>
+                  退出登录
+                </a-menu-item>
+              </a-menu>
+            </template>
+          </a-dropdown>
+        </a-space>
+      </a-layout-header>
+
+      <a-layout-content class="app-content">
+        <slot />
+      </a-layout-content>
+    </a-layout>
+
+    <a-drawer
+      v-if="isMobile"
+      placement="left"
+      :open="drawerVisible"
+      @close="drawerVisible = false"
+      :width="280"
+      :closable="false"
+      :body-style="{ padding: 0 }"
+      :header-style="{ padding: 0, border: 'none' }"
+      class="mobile-drawer"
+    >
+      <template #title>
+        <div class="drawer-header">
+          <div class="drawer-logo">
+            <div class="logo-icon-wrapper">
+              <DashboardOutlined style="font-size: 20px; color: var(--theme-primary);" />
+            </div>
+            <span class="logo-text">MQTT 仪表板</span>
+          </div>
+          <a-button type="text" @click="drawerVisible = false" class="drawer-close">
+            <template #icon><CloseOutlined /></template>
+          </a-button>
+        </div>
+      </template>
+      <a-menu
+        :theme="themeStore.theme"
+        mode="inline"
+        :selected-keys="selectedKeys"
+        :open-keys="openKeys"
+        :items="menuItems"
+        @click="handleMobileMenuClick"
+        @openChange="onOpenChange"
+        class="drawer-menu"
+      />
+    </a-drawer>
+  </a-layout>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, h } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { message } from 'ant-design-vue'
+import {
+  DashboardOutlined,
+  MobileOutlined,
+  HomeOutlined,
+  LinkOutlined,
+  MessageOutlined,
+  CrownOutlined,
+  UserOutlined,
+  SafetyOutlined,
+  FileTextOutlined,
+  SettingOutlined,
+  LogoutOutlined,
+  MenuOutlined,
+  CloudUploadOutlined,
+  DatabaseOutlined,
+  MenuFoldOutlined,
+  MenuUnfoldOutlined,
+  BulbOutlined,
+  CloseOutlined
+} from '@ant-design/icons-vue'
+import { useAuthStore } from '@/stores/auth'
+import { useThemeStore } from '@/stores/theme'
+import { useIsMobile } from '@/composables/useRealtimeData'
+
+const router = useRouter()
+const route = useRoute()
+const authStore = useAuthStore()
+const themeStore = useThemeStore()
+const isMobile = useIsMobile()
+
+const siderWidth = 220
+const collapsedWidth = 64
+
+const collapsed = ref(false)
+const drawerVisible = ref(false)
+const selectedKeys = ref<string[]>([route.path])
+const openKeys = ref<string[]>([])
+
+const rootSubmenuKeys = ['/mqtt', '/system']
+
+const pageTitleMap: Record<string, string> = {
+  '/dashboard': '仪表板',
+  '/devices': '设备管理',
+  '/ota': 'OTA升级',
+  '/sensor-data': '传感器数据',
+  '/rooms': '房间管理',
+  '/connections': '连接管理',
+  '/messages': '消息管理',
+  '/mqtt/client-auth': '客户端认证',
+  '/mqtt/client-acl': '客户端授权',
+  '/settings': '系统设置',
+  '/mqtt/auth-logs': '认证日志',
+  '/system-logs': '系统日志'
+}
+
+const currentPageTitle = computed(() => {
+  return pageTitleMap[route.path] || 'MQTT 数据监控平台'
+})
+
+const toggleSider = () => {
+  if (isMobile.value) {
+    drawerVisible.value = true
+  } else {
+    collapsed.value = !collapsed.value
+  }
+}
+
+watch(() => route.path, (newPath) => {
+  selectedKeys.value = [newPath]
+  if (newPath.startsWith('/mqtt')) {
+    openKeys.value = [...openKeys.value.filter(k => !rootSubmenuKeys.includes(k)), '/mqtt']
+  } else if (newPath.startsWith('/system') || newPath.startsWith('/settings') || newPath.startsWith('/mqtt/auth-logs')) {
+    openKeys.value = [...openKeys.value.filter(k => !rootSubmenuKeys.includes(k)), '/system']
+  }
+}, { immediate: true })
+
+const onOpenChange = (keys: string[]) => {
+  const latestOpenKey = keys.find(key => !openKeys.value.includes(key))
+  if (latestOpenKey && rootSubmenuKeys.includes(latestOpenKey)) {
+    openKeys.value = keys.filter(key => !rootSubmenuKeys.includes(key) || key === latestOpenKey)
+  } else {
+    openKeys.value = keys
+  }
+}
+
+const menuItems = computed(() => {
+  const allItems: any[] = [
+    { key: '/dashboard', icon: h(DashboardOutlined), label: '仪表板' },
+    { key: '/devices', icon: h(MobileOutlined), label: '设备管理' },
+    { key: '/ota', icon: h(CloudUploadOutlined), label: 'OTA升级' },
+    { key: '/sensor-data', icon: h(DatabaseOutlined), label: '传感器数据' },
+    { key: '/rooms', icon: h(HomeOutlined), label: '房间管理' },
+    { key: '/connections', icon: h(LinkOutlined), label: '连接管理' },
+    { key: '/messages', icon: h(MessageOutlined), label: '消息管理' },
+    {
+      key: '/mqtt',
+      icon: h(CrownOutlined),
+      label: 'MQTT登录管理',
+      children: [
+        { key: '/mqtt/client-auth', icon: h(UserOutlined), label: '客户端认证' },
+        { key: '/mqtt/client-acl', icon: h(SafetyOutlined), label: '客户端授权' }
+      ]
+    },
+    {
+      key: '/system',
+      icon: h(SettingOutlined),
+      label: '系统管理',
+      children: [
+        { key: '/settings', icon: h(SettingOutlined), label: '系统设置' },
+        { key: '/mqtt/auth-logs', icon: h(FileTextOutlined), label: '认证日志' },
+        { key: '/system-logs', icon: h(FileTextOutlined), label: '系统日志' }
+      ]
+    }
+  ]
+
+  const filterByPermission = (items: any[]): any[] => {
+    return items
+      .filter(item => {
+        if (item.children) {
+          item.children = filterByPermission(item.children)
+          return item.children.length > 0
+        }
+        return authStore.hasPermission(item.key)
+      })
+  }
+
+  return filterByPermission(allItems)
+})
+
+const handleMenuClick = ({ key }: { key: string }) => {
+  selectedKeys.value = [key]
+  router.push(key)
+}
+
+const handleMobileMenuClick = ({ key }: { key: string }) => {
+  selectedKeys.value = [key]
+  router.push(key)
+  drawerVisible.value = false
+}
+
+const handleLogout = () => {
+  authStore.logout()
+  message.success('已成功登出')
+  router.replace('/login')
+}
+</script>
+
+<style scoped>
+.app-layout {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: row;
+}
+
+.app-sider {
+  flex-shrink: 0;
+  height: 100vh;
+  position: sticky;
+  top: 0;
+  overflow: hidden;
+  border-right: 1px solid var(--theme-border);
+  z-index: 100;
+}
+
+.app-sider :deep(.ant-layout-sider-children) {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+}
+
+.sider-inner {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+}
+
+.sider-logo {
+  height: 56px;
+  display: flex;
+  align-items: center;
+  padding: 0 16px;
+  gap: 10px;
+  border-bottom: 1px solid var(--theme-border);
+  overflow: hidden;
+  flex-shrink: 0;
+  transition: padding 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+
+.sider-logo-collapsed {
+  padding: 0;
+  justify-content: center;
+}
+
+.logo-icon-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  min-width: 32px;
+  border-radius: 8px;
+  background: linear-gradient(135deg, var(--theme-primary), var(--theme-accent));
+  color: #fff;
+  flex-shrink: 0;
+}
+
+.logo-text {
+  font-size: 15px;
+  font-weight: 700;
+  color: var(--theme-text);
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+.logo-text-fade-enter-active {
+  transition: opacity 0.15s ease 0.05s;
+}
+
+.logo-text-fade-leave-active {
+  transition: opacity 0.1s ease;
+}
+
+.logo-text-fade-enter-from,
+.logo-text-fade-leave-to {
+  opacity: 0;
+}
+
+.sider-menu-wrapper {
+  flex: 1;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding: 4px 0;
+}
+
+.sider-menu-wrapper::-webkit-scrollbar {
+  width: 4px;
+}
+
+.sider-menu-wrapper::-webkit-scrollbar-thumb {
+  background: rgba(0, 0, 0, 0.08);
+  border-radius: 2px;
+}
+
+.sider-menu {
+  border-right: none !important;
+}
+
+.sider-footer {
+  flex-shrink: 0;
+  border-top: 1px solid var(--theme-border);
+  cursor: pointer;
+  transition: background 0.2s;
+}
+
+.sider-footer:hover {
+  background: rgba(0, 0, 0, 0.04);
+}
+
+.sider-footer-inner {
+  display: flex;
+  align-items: center;
+  height: 48px;
+  padding: 0 20px;
+  gap: 10px;
+  overflow: hidden;
+}
+
+.sider-logo-collapsed .sider-footer-inner {
+  justify-content: center;
+  padding: 0;
+}
+
+.footer-icon {
+  font-size: 16px;
+  color: var(--theme-text-secondary);
+  min-width: 16px;
+}
+
+.footer-text {
+  font-size: 13px;
+  color: var(--theme-text-secondary);
+  white-space: nowrap;
+}
+
+.app-main {
+  flex: 1;
+  min-width: 0;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: var(--theme-surface);
+}
+
+.app-main-mobile {
+  margin-left: 0 !important;
+}
+
+.app-header {
+  padding: 0 20px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 56px;
+  background: var(--theme-header-background);
+  border-bottom: 1px solid var(--theme-border);
+  position: sticky;
+  top: 0;
+  z-index: 10;
+  flex-shrink: 0;
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+}
+
+.trigger-btn {
+  font-size: 18px;
+  width: 36px;
+  height: 36px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 6px;
+}
+
+.breadcrumb-wrapper {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+}
+
+.page-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--theme-text);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.header-right {
+  flex-shrink: 0;
+}
+
+.theme-toggle-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: transform 0.2s;
+}
+
+.theme-toggle-btn:hover {
+  transform: rotate(15deg);
+}
+
+.user-info {
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 4px 8px;
+  border-radius: 6px;
+  transition: background 0.2s;
+}
+
+.user-info:hover {
+  background: rgba(0, 0, 0, 0.04);
+}
+
+.user-name {
+  font-size: 14px;
+  color: var(--theme-text);
+  font-weight: 500;
+}
+
+.app-content {
+  flex: 1;
+  padding: 20px;
+  overflow: auto;
+  min-height: 0;
+}
+
+.drawer-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  border-bottom: 1px solid var(--theme-border);
+}
+
+.drawer-logo {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.drawer-close {
+  color: var(--theme-text-secondary);
+}
+
+.drawer-menu {
+  border-right: none !important;
+}
+
+.mobile-drawer :deep(.ant-drawer-body) {
+  padding: 0 !important;
+}
+
+.mobile-drawer :deep(.ant-drawer-header) {
+  padding: 0 !important;
+  border-bottom: none !important;
+}
+
+@media (max-width: 768px) {
+  .app-header {
+    padding: 0 12px;
+    height: 48px;
+  }
+
+  .page-title {
+    font-size: 14px;
+  }
+
+  .app-content {
+    padding: 12px;
+  }
+}
+</style>

+ 15 - 0
mqtt-vue-dashboard/src/main.ts

@@ -0,0 +1,15 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import Antd from 'ant-design-vue'
+import 'ant-design-vue/dist/reset.css'
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+const pinia = createPinia()
+
+app.use(pinia)
+app.use(router)
+app.use(Antd)
+
+app.mount('#app')

+ 134 - 0
mqtt-vue-dashboard/src/router/index.ts

@@ -0,0 +1,134 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import type { RouteRecordRaw } from 'vue-router'
+import { useAuthStore } from '@/stores/auth'
+import { UserRole } from '@/types/auth'
+
+const routes: RouteRecordRaw[] = [
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/Login.vue'),
+    meta: { public: true }
+  },
+  {
+    path: '/',
+    redirect: '/dashboard'
+  },
+  {
+    path: '/dashboard',
+    name: 'Dashboard',
+    component: () => import('@/views/Dashboard.vue'),
+    meta: { requiredRole: UserRole.VIEWER }
+  },
+  {
+    path: '/devices',
+    name: 'Devices',
+    component: () => import('@/views/Devices.vue'),
+    meta: { requiredRole: UserRole.USER }
+  },
+  {
+    path: '/device-logs',
+    name: 'DeviceLogs',
+    component: () => import('@/views/DeviceLogs.vue'),
+    meta: { requiredRole: UserRole.USER }
+  },
+  {
+    path: '/ota',
+    name: 'OTA',
+    component: () => import('@/views/OTA.vue'),
+    meta: { requiredRole: UserRole.USER }
+  },
+  {
+    path: '/sensor-data',
+    name: 'SensorData',
+    component: () => import('@/views/SensorData.vue'),
+    meta: { requiredRole: UserRole.USER }
+  },
+  {
+    path: '/rooms',
+    name: 'Rooms',
+    component: () => import('@/views/Rooms.vue'),
+    meta: { requiredRole: UserRole.USER }
+  },
+  {
+    path: '/room/:id',
+    name: 'RoomDetail',
+    component: () => import('@/views/RoomDetail.vue'),
+    meta: { requiredRole: UserRole.USER }
+  },
+  {
+    path: '/connections',
+    name: 'Connections',
+    component: () => import('@/views/Connections.vue'),
+    meta: { requiredRole: UserRole.ADMIN }
+  },
+  {
+    path: '/messages',
+    name: 'Messages',
+    component: () => import('@/views/Messages.vue'),
+    meta: { requiredRole: UserRole.USER }
+  },
+  {
+    path: '/mqtt/client-auth',
+    name: 'ClientAuth',
+    component: () => import('@/views/ClientAuthList.vue'),
+    meta: { requiredRole: UserRole.ADMIN }
+  },
+  {
+    path: '/mqtt/client-acl',
+    name: 'ClientAcl',
+    component: () => import('@/views/ClientAclList.vue'),
+    meta: { requiredRole: UserRole.ADMIN }
+  },
+  {
+    path: '/mqtt/auth-logs',
+    name: 'AuthLogs',
+    component: () => import('@/views/AuthLogList.vue'),
+    meta: { requiredRole: UserRole.ADMIN }
+  },
+  {
+    path: '/system-logs',
+    name: 'SystemLogs',
+    component: () => import('@/views/SystemLogList.vue'),
+    meta: { requiredRole: UserRole.ADMIN }
+  },
+  {
+    path: '/settings',
+    name: 'Settings',
+    component: () => import('@/views/Settings.vue'),
+    meta: { requiredRole: UserRole.ADMIN }
+  }
+]
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+
+router.beforeEach(async (to, _from, next) => {
+  const authStore = useAuthStore()
+
+  if (to.meta.public) {
+    if (authStore.isAuthenticated) {
+      next('/dashboard')
+    } else {
+      next()
+    }
+    return
+  }
+
+  if (!authStore.isAuthenticated) {
+    next('/login')
+    return
+  }
+
+  const requiredRole = to.meta.requiredRole as UserRole | undefined
+  if (requiredRole && !authStore.hasPermission(requiredRole)) {
+    next('/dashboard')
+    return
+  }
+
+  next()
+})
+
+export default router

+ 298 - 0
mqtt-vue-dashboard/src/services/api.ts

@@ -0,0 +1,298 @@
+import axios from 'axios'
+import type { ApiResponse } from '@/types'
+
+const API_BASE_URL = '/api'
+
+const STORAGE_KEYS = {
+  USER: 'mqtt_dashboard_user',
+  TOKEN: 'mqtt_dashboard_token'
+}
+
+const api = axios.create({
+  baseURL: API_BASE_URL,
+  timeout: 10000,
+  headers: {
+    'Content-Type': 'application/json'
+  }
+})
+
+api.interceptors.request.use(
+  (config) => {
+    const token = localStorage.getItem(STORAGE_KEYS.TOKEN)
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`
+    }
+    return config
+  },
+  (error) => Promise.reject(error)
+)
+
+let isRefreshing = false
+let failedQueue: any[] = []
+
+const processQueue = (error: any, token: string | null = null) => {
+  failedQueue.forEach((prom) => {
+    if (error) {
+      prom.reject(error)
+    } else {
+      prom.resolve(token)
+    }
+  })
+  failedQueue = []
+}
+
+api.interceptors.response.use(
+  (response) => response.data,
+  async (error) => {
+    const originalRequest = error.config
+
+    if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/refresh-token')) {
+      if (isRefreshing) {
+        return new Promise((resolve, reject) => {
+          failedQueue.push({ resolve, reject })
+        }).then((token) => {
+          originalRequest.headers.Authorization = `Bearer ${token}`
+          return api(originalRequest)
+        })
+      }
+
+      originalRequest._retry = true
+      isRefreshing = true
+
+      try {
+        const response = await authAPI.refreshToken()
+        if (!response.data?.token) {
+          throw new Error('刷新token失败')
+        }
+        const { token } = response.data
+        localStorage.setItem(STORAGE_KEYS.TOKEN, token)
+        processQueue(null, token)
+        originalRequest.headers.Authorization = `Bearer ${token}`
+        return api(originalRequest)
+      } catch (refreshError) {
+        processQueue(refreshError, null)
+        localStorage.removeItem(STORAGE_KEYS.USER)
+        localStorage.removeItem(STORAGE_KEYS.TOKEN)
+        window.location.href = '/login'
+        return Promise.reject(refreshError)
+      } finally {
+        isRefreshing = false
+      }
+    }
+
+    if (error.response?.status === 403) {
+      localStorage.removeItem(STORAGE_KEYS.USER)
+      localStorage.removeItem(STORAGE_KEYS.TOKEN)
+      window.location.href = '/login'
+    }
+
+    if (error.response?.data) {
+      return Promise.reject(error.response.data)
+    }
+
+    return Promise.reject(error)
+  }
+)
+
+export { STORAGE_KEYS }
+
+export const authAPI = {
+  login: (credentials: { username: string; password: string }): Promise<ApiResponse<{ user: any; token: string }>> =>
+    api.post('/auth/login', credentials),
+
+  register: (userData: { username: string; email: string; password: string; role?: string }): Promise<ApiResponse> =>
+    api.post('/auth/register', userData),
+
+  getCurrentUser: (): Promise<ApiResponse> => api.get('/auth/me'),
+
+  changePassword: (passwordData: { oldPassword: string; newPassword: string }): Promise<ApiResponse> =>
+    api.post('/auth/change-password', passwordData),
+
+  refreshToken: (): Promise<ApiResponse<{ token: string }>> => api.post('/auth/refresh-token'),
+
+  getUsers: (): Promise<ApiResponse> => api.get('/auth/users'),
+
+  createUser: (userData: { username: string; password: string; role: string; email?: string }): Promise<ApiResponse> =>
+    api.post('/auth/users', userData),
+
+  updateUser: (id: string, userData: { username?: string; role?: string; email?: string }): Promise<ApiResponse> =>
+    api.put(`/auth/users/${id}`, userData),
+
+  deleteUser: (id: string): Promise<ApiResponse> => api.delete(`/auth/users/${id}`)
+}
+
+export const permissionAPI = {
+  getAllPages: (): Promise<ApiResponse> => api.get('/permissions/pages'),
+  getUserPermissions: (userId: string): Promise<ApiResponse> => api.get(`/permissions/users/${userId}/permissions`),
+  assignPermissions: (userId: string, pageIds: number[]): Promise<ApiResponse> =>
+    api.post(`/permissions/users/${userId}/permissions/batch`, { pageIds })
+}
+
+export const deviceAPI = {
+  getAllDevices: (): Promise<ApiResponse> => api.get('/devices'),
+  getDeviceById: (id: string): Promise<ApiResponse> => api.get(`/devices/${id}`),
+  getDeviceByClientId: (clientid: string): Promise<ApiResponse> => api.get(`/devices/client/${clientid}`),
+  getDevicesByRoom: (roomId: string): Promise<ApiResponse> => api.get(`/room-devices/room/${roomId}`),
+  getDevicesByType: (type: string): Promise<ApiResponse> => api.get(`/room-devices/type/${type}`),
+  getUnboundDevices: (): Promise<ApiResponse> => api.get('/devices/unbound'),
+  createDevice: (data: any): Promise<ApiResponse> => api.post('/room-devices', data),
+  updateDevice: (id: string, data: any): Promise<ApiResponse> => api.put(`/room-devices/${id}`, data),
+  updateMqttDevice: (clientid: string, data: any): Promise<ApiResponse> => api.put(`/devices/${clientid}`, data),
+  updateDeviceStatus: (id: string, status: string): Promise<ApiResponse> =>
+    api.patch(`/room-devices/${id}/status`, { status }),
+  deleteDevice: (id: string): Promise<ApiResponse> => api.delete(`/devices/${id}`),
+  getDeviceLogs: (deviceId: string, params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/device-logs${queryString}`)
+  },
+  getDeviceStats: (): Promise<ApiResponse> => api.get('/devices/stats'),
+  controlRelay: (deviceId: string, status: 'ON' | 'OFF'): Promise<ApiResponse> =>
+    api.post('/devices/control-relay', { deviceId, status }),
+  configureWiFi: (deviceId: string, ssid: string, password: string): Promise<ApiResponse> =>
+    api.post('/devices/configure-wifi', { deviceId, ssid, password }),
+  restartDevice: (deviceId: string): Promise<ApiResponse> => api.post('/devices/restart', { deviceId })
+}
+
+export const roomAPI = {
+  getAllRooms: (): Promise<ApiResponse> => api.get('/rooms'),
+  getRoomById: (id: string): Promise<ApiResponse> => api.get(`/rooms/${id}`),
+  getRoomsByFloor: (floorId: number): Promise<ApiResponse> => api.get(`/rooms/floor/${floorId}`),
+  createRoom: (data: any): Promise<ApiResponse> => api.post('/rooms', data),
+  updateRoom: (id: string, data: any): Promise<ApiResponse> => api.put(`/rooms/${id}`, data),
+  deleteRoom: (id: string): Promise<ApiResponse> => api.delete(`/rooms/${id}`),
+  getRoomStats: (): Promise<ApiResponse> => api.get('/rooms/stats')
+}
+
+export const deviceBindingAPI = {
+  getAllBindings: (): Promise<ApiResponse> => api.get('/device-bindings'),
+  getAllDevicesWithBindingStatus: (params: { page: number; pageSize: number; status?: string; room_id?: number; search?: string }): Promise<ApiResponse> =>
+    api.get('/device-bindings/all-devices-status', { params }),
+  getAvailableDevices: (): Promise<ApiResponse> => api.get('/device-bindings/available-devices'),
+  getDevicesByRoom: (roomId: string): Promise<ApiResponse> => api.get(`/device-bindings/room/${roomId}`),
+  getRoomDevicesWithDetails: (roomId: string): Promise<ApiResponse> => api.get(`/device-bindings/room/${roomId}/details`),
+  bindDevice: (data: { device_clientid: string; room_id: number; device_name?: string; device_type?: string; properties?: any }): Promise<ApiResponse> =>
+    api.post('/device-bindings/bind', data),
+  unbindDevice: (deviceClientId: string): Promise<ApiResponse> => api.delete(`/device-bindings/unbind/${deviceClientId}`),
+  updateBinding: (id: number, data: { room_id?: number; device_name?: string; device_type?: string; properties?: any }): Promise<ApiResponse> =>
+    api.put(`/device-bindings/${id}`, data)
+}
+
+export const otaAPI = {
+  getFirmwareFiles: (): Promise<ApiResponse> => api.get('/ota/firmware'),
+  uploadFirmware: (formData: FormData): Promise<ApiResponse> =>
+    api.post('/ota/firmware', formData, { headers: { 'Content-Type': 'multipart/form-data' } }),
+  deleteFirmware: (id: number): Promise<ApiResponse> => api.delete(`/ota/firmware/${id}`),
+  startOTA: (data: { firmwareId: number; deviceIds: string[]; retryCount?: number; retryInterval?: number; timeout?: number }): Promise<ApiResponse> =>
+    api.post('/ota/upgrade', data),
+  getOTATasks: (): Promise<ApiResponse> => api.get('/ota/tasks'),
+  cancelOTATask: (taskId: number): Promise<ApiResponse> => api.put(`/ota/tasks/${taskId}/cancel`),
+  getOTAResults: (taskId: number): Promise<ApiResponse> => api.get(`/ota/results/${taskId}`),
+  retryOTATask: (taskId: number): Promise<ApiResponse> => api.put(`/ota/tasks/${taskId}/retry`),
+  deleteOTATask: (taskId: number): Promise<ApiResponse> => api.delete(`/ota/tasks/${taskId}`)
+}
+
+export const connectionAPI = {
+  getConnections: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/connections${queryString}`)
+  },
+  getConnection: (id: string): Promise<ApiResponse> => api.get(`/connections/${id}`),
+  disconnectConnection: (id: string): Promise<ApiResponse> => api.post(`/connections/${id}/disconnect`)
+}
+
+export const messageAPI = {
+  getMessages: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/messages${queryString}`)
+  },
+  getAllMessages: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/messages${queryString}`)
+  },
+  getMessage: (id: string): Promise<ApiResponse> => api.get(`/messages/${id}`),
+  getMessageStats: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/messages/stats${queryString}`)
+  },
+  getMessageHeatmapData: (days?: number): Promise<ApiResponse> => {
+    const queryString = days ? `?days=${days}` : ''
+    return api.get(`/messages/stats/heatmap${queryString}`)
+  }
+}
+
+export const clientAuthAPI = {
+  getClientAuths: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/client-auth${queryString}`)
+  },
+  getClientAuthById: (id: string): Promise<ApiResponse> => api.get(`/client-auth/${id}`),
+  getClientAuthByUsername: (username: string): Promise<ApiResponse> => api.get(`/client-auth/username/${username}`),
+  getClientAuthByClientId: (clientid: string): Promise<ApiResponse> => api.get(`/client-auth/clientid/${clientid}`),
+  createClientAuth: (authData: any): Promise<ApiResponse> => api.post('/client-auth', authData),
+  updateClientAuth: (id: string, authData: any): Promise<ApiResponse> => api.put(`/client-auth/${id}`, authData),
+  deleteClientAuth: (id: string): Promise<ApiResponse> => api.delete(`/client-auth/${id}`),
+  verifyClientAuth: (authData: any): Promise<ApiResponse> => api.post('/client-auth/verify', authData),
+  getClientAuthStats: (): Promise<ApiResponse> => api.get('/client-auth/stats')
+}
+
+export const clientAclAPI = {
+  getClientAcls: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/client-acl${queryString}`)
+  },
+  getClientAclDetail: (id: string): Promise<ApiResponse> => api.get(`/client-acl/${id}`),
+  createClientAcl: (acl: any): Promise<ApiResponse> => api.post('/client-acl', acl),
+  updateClientAcl: (id: string, acl: any): Promise<ApiResponse> => api.put(`/client-acl/${id}`, acl),
+  deleteClientAcl: (id: string): Promise<ApiResponse> => api.delete(`/client-acl/${id}`)
+}
+
+export const authLogAPI = {
+  getAuthLogs: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/auth-logs${queryString}`)
+  },
+  getAuthLogDetail: (id: string): Promise<ApiResponse> => api.get(`/auth-logs/${id}`),
+  getAuthLogStats: (): Promise<ApiResponse> => api.get('/auth-logs/stats'),
+  cleanupOldAuthLogs: (days: number): Promise<ApiResponse> => api.delete(`/auth-logs/cleanup?days=${days}`)
+}
+
+export const systemLogAPI = {
+  getSystemLogs: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/system-logs${queryString}`)
+  }
+}
+
+export const dashboardAPI = {
+  getOverview: (): Promise<ApiResponse> => api.get('/dashboard/overview'),
+  getConnectionStats: (): Promise<ApiResponse> => api.get('/dashboard/connection-stats'),
+  getSystemInfo: (): Promise<ApiResponse> => api.get('/dashboard/system-info')
+}
+
+export const sensorDataAPI = {
+  getSensorData: (params?: any): Promise<ApiResponse> => {
+    const queryString = params ? `?${new URLSearchParams(params).toString()}` : ''
+    return api.get(`/sensor-data${queryString}`)
+  }
+}
+
+export const wifiConfigAPI = {
+  getWifiConfig: (): Promise<ApiResponse> => api.get('/wifi-configs/latest'),
+  saveWifiConfig: (data: any): Promise<ApiResponse> => api.post('/wifi-configs', data),
+  getDeviceWifiConfigs: (clientid: string, limit?: number): Promise<ApiResponse> =>
+    api.get(`/wifi-configs/device/${clientid}`, { params: { limit } }),
+  getLatestWifiConfig: (clientid: string): Promise<ApiResponse> => api.get(`/wifi-configs/device/${clientid}/latest`),
+  getAllWifiConfigs: (limit?: number, offset?: number): Promise<ApiResponse> =>
+    api.get('/wifi-configs/all', { params: { limit, offset } }),
+  deleteWifiConfig: (id: number): Promise<ApiResponse> => api.delete(`/wifi-configs/${id}`)
+}
+
+export const brokerAPI = {
+  getStatus: (): Promise<ApiResponse> => api.get('/broker/status'),
+  getConnectedClients: (): Promise<ApiResponse> => api.get('/broker/clients'),
+  disconnectClient: (clientId: string): Promise<ApiResponse> => api.post(`/broker/clients/${clientId}/disconnect`),
+  publishMessage: (data: { topic: string; payload: string | object; qos?: number; retain?: boolean }): Promise<ApiResponse> =>
+    api.post('/broker/publish', data)
+}
+
+export default api

+ 214 - 0
mqtt-vue-dashboard/src/services/websocket.ts

@@ -0,0 +1,214 @@
+import { io, type Socket } from 'socket.io-client'
+
+class WebSocketService {
+  private socket: Socket | null = null
+  private reconnectAttempts = 0
+  private maxReconnectAttempts = 5
+  private reconnectInterval = 3000
+  private listeners: Map<string, Array<(data: any) => void>> = new Map()
+  private isConnecting = false
+  private url: string
+
+  constructor(url?: string) {
+    this.url = url || this.generateWebSocketUrl()
+  }
+
+  private generateWebSocketUrl(): string {
+    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
+    const hostname = window.location.hostname
+    const port = window.location.port
+
+    let wsPort: string
+    if (port) {
+      const currentPort = parseInt(port)
+      if (currentPort === 3000 || currentPort === 5173 || currentPort === 5174 ||
+          currentPort === 5175 || currentPort === 5176 || currentPort === 5177 || currentPort === 5178) {
+        wsPort = ':3002'
+      } else if (currentPort === 80 || currentPort === 443) {
+        wsPort = ':3002'
+      } else {
+        wsPort = ':3002'
+      }
+    } else {
+      wsPort = ':3002'
+    }
+
+    return `${protocol}//${hostname}${wsPort}`
+  }
+
+  connect(): Promise<void> {
+    return new Promise((resolve) => {
+      if (this.socket && this.socket.connected) {
+        resolve()
+        return
+      }
+
+      this.url = this.generateWebSocketUrl()
+
+      if (this.isConnecting) {
+        const checkConnection = () => {
+          if (this.socket && this.socket.connected) {
+            resolve()
+          } else if (!this.isConnecting) {
+            resolve()
+          } else {
+            setTimeout(checkConnection, 100)
+          }
+        }
+        checkConnection()
+        return
+      }
+
+      this.isConnecting = true
+
+      try {
+        this.socket = io(this.url, {
+          transports: ['websocket', 'polling'],
+          timeout: 2000,
+          forceNew: true
+        })
+
+        const connectionTimeout = setTimeout(() => {
+          if (this.isConnecting) {
+            this.isConnecting = false
+            this.handleMessage({
+              type: 'connection_status',
+              payload: { connected: false, mode: 'none' }
+            })
+            resolve()
+          }
+        }, 3000)
+
+        this.socket.on('connect', () => {
+          clearTimeout(connectionTimeout)
+          this.isConnecting = false
+          this.reconnectAttempts = 0
+          this.requestInitialData()
+          resolve()
+        })
+
+        this.socket.onAny((eventName, ...args) => {
+          this.handleMessage({
+            type: eventName,
+            payload: args[0]
+          })
+        })
+
+        this.socket.on('disconnect', () => {
+          this.isConnecting = false
+          this.handleReconnect()
+        })
+
+        this.socket.on('connect_error', (error) => {
+          console.warn('WebSocket连接失败:', this.url, error.message)
+          this.isConnecting = false
+          resolve()
+        })
+      } catch {
+        this.isConnecting = false
+        resolve()
+      }
+    })
+  }
+
+  private requestInitialData(): void {
+    if (!this.socket || !this.socket.connected) return
+
+    this.socket.emit('request_device_data')
+    this.socket.emit('request_connection_stats')
+    this.socket.emit('request_message_stats')
+    this.socket.emit('request_recent_connections')
+    this.socket.emit('request_recent_messages')
+    this.socket.emit('request_device_status_distribution')
+  }
+
+  private handleMessage(data: any) {
+    const { type, payload } = data
+
+    if ((type === 'device_status' || type === 'device_connection') && (!payload || !payload.clientid)) {
+      return
+    }
+
+    if (type === 'device_status_distribution' && payload) {
+      const transformedPayload = {
+        online: payload.online || 0,
+        offline: payload.offline || 0,
+        unknown: payload.unknown || 0,
+        total: payload.total || (payload.online + payload.offline + payload.unknown),
+        timestamp: payload.timestamp || Date.now()
+      }
+      if (this.listeners.has(type)) {
+        this.listeners.get(type)!.forEach(callback => {
+          try { callback(transformedPayload) } catch (e) { console.error(e) }
+        })
+      }
+      return
+    }
+
+    if (this.listeners.has(type)) {
+      this.listeners.get(type)!.forEach(callback => {
+        try { callback(payload) } catch (e) { console.error(e) }
+      })
+    }
+
+    if (this.listeners.has('*')) {
+      this.listeners.get('*')!.forEach(callback => {
+        try { callback(data) } catch (e) { console.error(e) }
+      })
+    }
+  }
+
+  subscribe(type: string, callback: (data: any) => void): () => void {
+    if (!this.listeners.has(type)) {
+      this.listeners.set(type, [])
+    }
+    this.listeners.get(type)!.push(callback)
+
+    if (this.socket && this.socket.connected) {
+      this.socket.on(type, callback)
+    }
+
+    return () => {
+      const callbacks = this.listeners.get(type)
+      if (callbacks) {
+        const index = callbacks.indexOf(callback)
+        if (index !== -1) callbacks.splice(index, 1)
+        if (callbacks.length === 0) this.listeners.delete(type)
+      }
+      if (this.socket && this.socket.connected) {
+        this.socket.off(type, callback)
+      }
+    }
+  }
+
+  send(type: string, data: any) {
+    if (this.socket && this.socket.connected) {
+      this.socket.emit(type, data)
+    }
+  }
+
+  disconnect() {
+    if (this.socket) {
+      this.socket.disconnect()
+      this.socket = null
+    }
+    this.listeners.clear()
+  }
+
+  get isConnected(): boolean {
+    return this.socket ? this.socket.connected : false
+  }
+
+  private handleReconnect() {
+    if (this.reconnectAttempts >= this.maxReconnectAttempts) return
+    this.reconnectAttempts++
+    setTimeout(() => {
+      if (!this.socket || !this.socket.connected) {
+        this.connect().catch(() => {})
+      }
+    }, this.reconnectInterval)
+  }
+}
+
+const webSocketService = new WebSocketService()
+export default webSocketService

+ 147 - 0
mqtt-vue-dashboard/src/stores/auth.ts

@@ -0,0 +1,147 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import type { User } from '@/types/auth'
+import { UserRole } from '@/types/auth'
+import { authAPI, permissionAPI, STORAGE_KEYS } from '@/services/api'
+import type { PagePermission } from '@/types/auth'
+
+export const useAuthStore = defineStore('auth', () => {
+  const user = ref<User | null>(null)
+  const isLoading = ref(true)
+  const userPermissions = ref<PagePermission[]>([])
+
+  const isAuthenticated = computed(() => !!user.value)
+
+  const loadUserPermissions = async (userId?: string) => {
+    const targetUserId = userId || user.value?.id
+    if (!targetUserId) return
+    try {
+      const response = await permissionAPI.getUserPermissions(targetUserId)
+      if (response.success) {
+        userPermissions.value = response.data || []
+      }
+    } catch {
+      userPermissions.value = []
+    }
+  }
+
+  const checkAuthStatus = async () => {
+    try {
+      const storedUser = localStorage.getItem(STORAGE_KEYS.USER)
+      const token = localStorage.getItem(STORAGE_KEYS.TOKEN)
+
+      if (storedUser && token) {
+        const userData = JSON.parse(storedUser)
+        user.value = userData
+
+        try {
+          const response = await authAPI.getCurrentUser()
+          if (response.success) {
+            user.value = response.data
+            await loadUserPermissions()
+          } else {
+            try {
+              const refreshResponse = await authAPI.refreshToken()
+              if (refreshResponse.success && refreshResponse.data?.token) {
+                localStorage.setItem(STORAGE_KEYS.TOKEN, refreshResponse.data.token)
+                const userResponse = await authAPI.getCurrentUser()
+                if (userResponse.success) {
+                  user.value = userResponse.data
+                  await loadUserPermissions()
+                }
+              }
+            } catch {
+              localStorage.removeItem(STORAGE_KEYS.USER)
+              localStorage.removeItem(STORAGE_KEYS.TOKEN)
+              user.value = null
+              userPermissions.value = []
+            }
+          }
+        } catch {
+          localStorage.removeItem(STORAGE_KEYS.USER)
+          localStorage.removeItem(STORAGE_KEYS.TOKEN)
+          user.value = null
+          userPermissions.value = []
+        }
+      } else {
+        user.value = null
+        userPermissions.value = []
+      }
+    } catch {
+      localStorage.removeItem(STORAGE_KEYS.USER)
+      localStorage.removeItem(STORAGE_KEYS.TOKEN)
+      user.value = null
+      userPermissions.value = []
+    } finally {
+      isLoading.value = false
+    }
+  }
+
+  const login = async (credentials: { username: string; password: string }) => {
+    isLoading.value = true
+    try {
+      const response = await authAPI.login(credentials)
+      if (response.success && response.data) {
+        const { user: userData, token } = response.data
+        localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(userData))
+        localStorage.setItem(STORAGE_KEYS.TOKEN, token)
+        user.value = userData
+        await loadUserPermissions(userData.id)
+      } else {
+        throw new Error(response.message || '登录失败')
+      }
+    } finally {
+      isLoading.value = false
+    }
+  }
+
+  const logout = () => {
+    localStorage.removeItem(STORAGE_KEYS.USER)
+    localStorage.removeItem(STORAGE_KEYS.TOKEN)
+    user.value = null
+    userPermissions.value = []
+  }
+
+  const hasPermission = (requiredRole: UserRole | string): boolean => {
+    if (!user.value) return false
+
+    if (typeof requiredRole === 'string') {
+      if (Object.values(UserRole).includes(requiredRole as UserRole)) {
+        const roleHierarchy: Record<string, number> = {
+          [UserRole.ADMIN]: 3,
+          [UserRole.USER]: 2,
+          [UserRole.VIEWER]: 1
+        }
+        return (roleHierarchy[user.value.role] || 0) >= (roleHierarchy[requiredRole as UserRole] || 0)
+      } else {
+        return checkPagePermission(requiredRole)
+      }
+    }
+
+    const roleHierarchy: Record<string, number> = {
+      [UserRole.ADMIN]: 3,
+      [UserRole.USER]: 2,
+      [UserRole.VIEWER]: 1
+    }
+    return (roleHierarchy[user.value.role] || 0) >= (roleHierarchy[requiredRole] || 0)
+  }
+
+  const checkPagePermission = (pagePath: string): boolean => {
+    if (!user.value) return false
+    if (user.value.role === UserRole.ADMIN) return true
+    return userPermissions.value.some(permission => pagePath.startsWith(permission.path))
+  }
+
+  return {
+    user,
+    isLoading,
+    isAuthenticated,
+    userPermissions,
+    checkAuthStatus,
+    login,
+    logout,
+    hasPermission,
+    checkPagePermission,
+    loadUserPermissions
+  }
+})

+ 101 - 0
mqtt-vue-dashboard/src/stores/theme.ts

@@ -0,0 +1,101 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export type ThemeType = 'light' | 'dark'
+
+const lightTheme = {
+  name: 'light',
+  background: '#ffffff',
+  surface: '#f0f2f5',
+  primary: '#1890ff',
+  secondary: '#52c41a',
+  accent: '#722ed1',
+  text: 'rgba(0, 0, 0, 0.88)',
+  textSecondary: 'rgba(0, 0, 0, 0.45)',
+  border: '#f0f0f0',
+  shadow: 'rgba(0, 0, 0, 0.06)',
+  cardBackground: '#ffffff',
+  headerBackground: '#ffffff',
+  sidebarBackground: '#ffffff',
+  success: '#52c41a',
+  warning: '#faad14',
+  error: '#ff4d4f',
+  info: '#1890ff'
+}
+
+const darkTheme = {
+  name: 'dark',
+  background: '#141414',
+  surface: '#1a1a1a',
+  primary: '#177ddc',
+  secondary: '#49aa19',
+  accent: '#531dab',
+  text: 'rgba(255, 255, 255, 0.88)',
+  textSecondary: 'rgba(255, 255, 255, 0.45)',
+  border: '#303030',
+  shadow: 'rgba(0, 0, 0, 0.3)',
+  cardBackground: '#1f1f1f',
+  headerBackground: '#1f1f1f',
+  sidebarBackground: '#141414',
+  success: '#49aa19',
+  warning: '#d89614',
+  error: '#d32029',
+  info: '#177ddc'
+}
+
+export type ThemeColors = typeof lightTheme
+
+export const useThemeStore = defineStore('theme', () => {
+  const theme = ref<ThemeType>((localStorage.getItem('theme') as ThemeType) || 'light')
+  const themeColors = ref<ThemeColors>(theme.value === 'dark' ? darkTheme : lightTheme)
+
+  const toggleTheme = () => {
+    setTheme(theme.value === 'light' ? 'dark' : 'light')
+  }
+
+  const setTheme = (newTheme: ThemeType) => {
+    theme.value = newTheme
+    themeColors.value = newTheme === 'dark' ? darkTheme : lightTheme
+    localStorage.setItem('theme', newTheme)
+    applyThemeToDom()
+  }
+
+  const applyThemeToDom = () => {
+    const root = document.documentElement
+    const colors = themeColors.value
+
+    root.style.setProperty('--theme-background', colors.background)
+    root.style.setProperty('--theme-surface', colors.surface)
+    root.style.setProperty('--theme-primary', colors.primary)
+    root.style.setProperty('--theme-secondary', colors.secondary)
+    root.style.setProperty('--theme-accent', colors.accent)
+    root.style.setProperty('--theme-text', colors.text)
+    root.style.setProperty('--theme-text-secondary', colors.textSecondary)
+    root.style.setProperty('--theme-border', colors.border)
+    root.style.setProperty('--theme-shadow', colors.shadow)
+    root.style.setProperty('--theme-card-background', colors.cardBackground)
+    root.style.setProperty('--theme-header-background', colors.headerBackground)
+    root.style.setProperty('--theme-sidebar-background', colors.sidebarBackground)
+    root.style.setProperty('--theme-success', colors.success)
+    root.style.setProperty('--theme-warning', colors.warning)
+    root.style.setProperty('--theme-error', colors.error)
+    root.style.setProperty('--theme-info', colors.info)
+
+    document.body.setAttribute('data-theme', theme.value)
+    if (theme.value === 'dark') {
+      root.classList.add('dark-theme')
+      root.classList.remove('light-theme')
+    } else {
+      root.classList.add('light-theme')
+      root.classList.remove('dark-theme')
+    }
+  }
+
+  return {
+    theme,
+    themeColors,
+    toggleTheme,
+    setTheme,
+    applyThemeToDom
+  }
+})

+ 55 - 0
mqtt-vue-dashboard/src/stores/websocket.ts

@@ -0,0 +1,55 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import webSocketService from '@/services/websocket'
+
+export const useWebSocketStore = defineStore('websocket', () => {
+  const isConnected = ref(false)
+  const lastMessage = ref<{ type: string; payload: any; timestamp: number } | null>(null)
+
+  const connect = async () => {
+    try {
+      await webSocketService.connect()
+    } catch (error) {
+      console.error('WebSocket连接失败:', error)
+    }
+  }
+
+  const disconnect = () => {
+    webSocketService.disconnect()
+    isConnected.value = false
+  }
+
+  const subscribe = (type: string, callback: (data: any) => void) => {
+    return webSocketService.subscribe(type, callback)
+  }
+
+  const send = (type: string, data: any) => {
+    webSocketService.send(type, data)
+  }
+
+  const initConnection = () => {
+    webSocketService.subscribe('connection_status', (data: any) => {
+      isConnected.value = data.connected
+    })
+
+    webSocketService.subscribe('*', (data: any) => {
+      lastMessage.value = {
+        type: data.type || 'unknown',
+        payload: data,
+        timestamp: Date.now()
+      }
+    })
+
+    connect().catch(() => {})
+  }
+
+  return {
+    isConnected,
+    lastMessage,
+    connect,
+    disconnect,
+    subscribe,
+    send,
+    initConnection
+  }
+})

+ 60 - 0
mqtt-vue-dashboard/src/types/auth.ts

@@ -0,0 +1,60 @@
+export enum UserRole {
+  ADMIN = 'admin',
+  USER = 'user',
+  VIEWER = 'viewer'
+}
+
+export interface User {
+  id: string
+  username: string
+  email: string
+  role: UserRole
+  createdAt: string
+  lastLoginAt?: string
+}
+
+export interface LoginForm {
+  username: string
+  password: string
+}
+
+export interface PagePermission {
+  id: number
+  name: string
+  path: string
+  description?: string
+}
+
+export interface ClientAuth {
+  id?: number
+  username: string
+  clientid: string
+  password?: string
+  password_hash?: string
+  salt?: string
+  use_salt?: boolean
+  status: 'enabled' | 'disabled'
+  device_type?: string
+  description?: string
+  is_superuser?: boolean
+  created_at?: string
+  updated_at?: string
+  last_login_at?: string
+  auth_method?: 'password' | 'token' | 'certificate' | 'external'
+  auth_expiry?: string | null
+  allowed_ip_ranges?: string | null
+  allowed_time_ranges?: string | null
+  auth_policy_id?: number | null
+}
+
+export interface ClientAcl {
+  id?: number
+  username: string
+  topic: string
+  action: 'publish' | 'subscribe' | 'pubsub'
+  permission: 'allow' | 'deny'
+  priority?: number
+  description?: string
+  created_at?: string
+  updated_at?: string
+}

Some files were not shown because too many files changed in this diff