Browse Source

恢复的代码

yangfei 1 year ago
parent
commit
0f39ac3c29

+ 2 - 1
.gitignore

@@ -27,4 +27,5 @@ build/Release
 # Dependency directory
 # Dependency directory
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
 node_modules
-
+packeg-lock.json
+ecosystem.config.js

+ 3 - 0
README.md

@@ -1,2 +1,5 @@
 # vueiot-api
 # vueiot-api
 
 
+```
+后端api,测试11111
+```

+ 174 - 0
app.js

@@ -0,0 +1,174 @@
+const express = require('express');
+const cors = require('cors');
+const routes = require('./routes');
+const webhookRouter = require('./webhook/webhook');
+const logger = require('./logger'); // 引入日志记录器
+const fs = require('fs');
+const path = require('path');
+
+// 主应用服务器
+const app = express();
+
+// 记录应用启动信息
+logger.info('正在初始化应用服务器...');
+
+// 启用 CORS
+app.use(cors());
+logger.info('CORS 中间件已启用');
+
+// 解析 JSON 请求体
+app.use(express.json());
+logger.info('JSON 解析中间件已启用');
+
+// 解析 URL 编码的请求体
+app.use(express.urlencoded({ extended: true }));
+logger.info('URL 编码解析中间件已启用');
+
+// 请求日志中间件
+app.use((req, res, next) => {
+    const start = Date.now();
+    res.on('finish', () => {
+        const duration = Date.now() - start;
+        logger.info(`${req.method} ${req.originalUrl} - ${res.statusCode} - ${duration}ms`);
+    });
+    next();
+});
+
+// 加载路由
+app.use('/', routes); // 将主路由挂载到 /api 路径
+app.use('/', webhookRouter); // 将 Webhook 路由挂载到 /webhook 路径
+logger.info('路由模块已加载');
+
+// 404 处理
+app.use((req, res, next) => {
+    logger.warn(`404 Not Found: ${req.method} ${req.originalUrl}`);
+    res.status(404).json({
+        success: false,
+        message: '请求的资源不存在'
+    });
+});
+
+// 错误处理中间件
+app.use((err, req, res, next) => {
+    logger.error('应用错误:', err);
+    logger.error('错误堆栈:', err.stack);
+    res.status(500).json({
+        success: false,
+        message: '服务器内部错误'
+    });
+});
+
+// 主服务器配置
+const MAIN_PORT = 3000;
+const MAIN_HOST = '0.0.0.0';
+
+// 启动主服务器
+app.listen(MAIN_PORT, MAIN_HOST, () => {
+    logger.info(`主服务器已启动: http://${MAIN_HOST}:${MAIN_PORT}`);
+    logger.info('主服务器配置信息:');
+    logger.info(`- 监听端口: ${MAIN_PORT}`);
+    logger.info(`- 监听地址: ${MAIN_HOST}`);
+    logger.info(`- 环境: ${process.env.NODE_ENV || 'development'}`);
+});
+
+// OTA 服务器
+const otaApp = express();
+
+// 添加 JSON 解析中间件
+otaApp.use(express.json());
+otaApp.use(express.urlencoded({ extended: true }));
+
+// 固件文件路径
+const FIRMWARE_PATH = path.join(__dirname, '../sketch_feb24a.ino.bin'); // 确保固件文件与服务器代码在同一目录下
+
+// 最新固件版本
+const LATEST_FIRMWARE_VERSION = "1.0.4"; // 服务器上的最新固件版本
+
+// 需要更新的设备ID列表
+const TARGET_DEVICES = ["54:32:04:0C:ED:C8", ""]; // 替换为实际的设备ID
+
+// 修改检查更新路由
+otaApp.post('/check-update', (req, res) => {
+    logger.debug('收到请求体:', req.body);
+    
+    if (!req.body || !req.body.device_id || !req.body.version) {
+      logger.error('无效的请求体:', req.body);
+      return res.status(400).json({
+        success: false,
+        message: '请求体必须包含 device_id 和 version 字段'
+      });
+    }
+    // 获取设备ID和当前固件版本
+    const { device_id, version } = req.body;
+    logger.info(`收到设备ID: ${device_id}, 当前固件版本: ${version}`);
+
+    // 检查设备是否需要更新
+    if (TARGET_DEVICES.includes(device_id) && version !== LATEST_FIRMWARE_VERSION) {
+        logger.info(`设备 ${device_id} 需要更新。`);
+        res.send("update"); // 返回"update"表示需要更新
+    } else {
+        logger.info(`设备 ${device_id} 无需更新。`);
+        res.send("no_update"); // 返回"no_update"表示无需更新
+    }
+});
+
+// 固件下载路由
+otaApp.get('/firmware', (req, res) => {
+    // 返回固件文件
+    logger.info("正在下发固件...");
+    if (fs.existsSync(FIRMWARE_PATH)) {
+        res.download(FIRMWARE_PATH, (err) => {
+            if (err) {
+                logger.error("固件下发失败:", err);
+                res.status(500).send("固件下发失败");
+            }
+        });
+    } else {
+        logger.error("固件文件未找到");
+        res.status(404).send("固件文件未找到");
+    }
+});
+
+// OTA 服务器配置
+const OTA_PORT = 2999;
+const OTA_HOST = '0.0.0.0';
+
+// 启动 OTA 服务器
+otaApp.listen(OTA_PORT, OTA_HOST, () => {
+    logger.info(`OTA 服务器已启动: http://${OTA_HOST}:${OTA_PORT}`);
+    logger.info('OTA 服务器配置信息:');
+    logger.info(`- 监听端口: ${OTA_PORT}`);
+    logger.info(`- 监听地址: ${OTA_HOST}`);
+});
+
+// 捕获未处理的异常
+process.on('uncaughtException', (err) => {
+    logger.error('未捕获的异常:', err);
+    logger.error('错误堆栈:', err.stack);
+    // 给进程一点时间来记录日志
+    setTimeout(() => {
+        process.exit(1);
+    }, 1000);
+});
+
+// 捕获未处理的 Promise 拒绝
+process.on('unhandledRejection', (reason, promise) => {
+    logger.error('未处理的 Promise 拒绝:', reason);
+    logger.error('Promise:', promise);
+});
+
+// 优雅关闭
+process.on('SIGTERM', () => {
+    logger.info('收到 SIGTERM 信号,准备关闭服务器...');
+    // 在这里可以添加清理代码
+    process.exit(0);
+});
+
+process.on('SIGINT', () => {
+    logger.info('收到 SIGINT 信号,准备关闭服务器...');
+    // 在这里可以添加清理代码
+    process.exit(0);
+});
+// app.use('/', otaRouter);  // 修改
+// app.use('/', otaRouter);
+module.exports = app;

+ 47 - 0
config/db.js

@@ -0,0 +1,47 @@
+const mysql = require('mysql2');
+const logger = require('../logger');
+
+// 从环境变量中获取数据库配置
+const dbConfig = {
+    host: process.env.DB_HOST || '192.168.3.31',
+    user: process.env.DB_USER || 'yangfei',
+    password: process.env.DB_PASSWORD || 'yangfei',
+    database: process.env.DB_NAME || 'myiot',
+    waitForConnections: true,
+    connectionLimit: 10, // 连接池大小
+    queueLimit: 0, // 无限制排队
+    connectTimeout: 10000, // 连接超时时间
+};
+
+// 创建连接池
+const pool = mysql.createPool(dbConfig);
+
+// 测试连接
+pool.getConnection((err, connection) => {
+    if (err) {
+        logger.error('MySQL2 连接失败:', err);
+        return;
+    }
+    logger.info('MySQL2 数据库连接成功');
+    connection.release(); // 释放连接回池中
+});
+
+// 处理连接错误
+pool.on('error', (err) => {
+    logger.error('MySQL2 连接池错误:', err);
+    if (err.code === 'PROTOCOL_CONNECTION_LOST') {
+        // 重新连接
+        pool.getConnection((err, connection) => {
+            if (err) {
+                logger.error('MySQL2 重新连接失败:', err);
+            } else {
+                logger.info('MySQL2 重新连接成功');
+                connection.release();
+            }
+        });
+    } else {
+        throw err;
+    }
+});
+
+module.exports = pool;

+ 109 - 0
controllers/authController.js

@@ -0,0 +1,109 @@
+const bcrypt = require('bcrypt');
+const jwt = require('jsonwebtoken');
+const pool = require('../config/db');
+const logger = require('../logger'); // 引入日志记录器
+
+exports.register = async (req, res) => {
+  const { username, password } = req.body;
+  logger.info(`开始处理用户注册请求: ${username}`);
+
+  try {
+    const [users] = await pool.promise().query('SELECT * FROM users WHERE username = ?', [username]);
+    if (users.length > 0) {
+      logger.warn(`用户注册失败:用户名 ${username} 已存在`); // 记录警告日志
+      return res.status(400).json({ message: '用户名已存在' });
+    }
+
+    const saltRounds = 10;
+    const hashedPassword = await bcrypt.hash(password, saltRounds);
+    logger.debug('密码加密完成');
+
+    const [result] = await pool.promise().query(
+      'INSERT INTO users (username, password) VALUES (?, ?)',
+      [username, hashedPassword]
+    );
+
+    logger.info(`用户 ${username} 注册成功,用户ID:${result.insertId}`); // 记录成功日志
+    res.status(201).json({ 
+      success: true,
+      message: '用户注册成功', 
+      userId: result.insertId 
+    });
+  } catch (error) {
+    logger.error('注册过程发生错误:', error); // 记录错误日志
+    res.status(500).json({ 
+      success: false,
+      message: '注册失败,请稍后重试' 
+    });
+  }
+};
+
+exports.login = async (req, res) => {
+  const { username, password } = req.body;
+  logger.info(`用户登录尝试: ${username}`);
+
+  try {
+    const [users] = await pool.promise().query('SELECT * FROM users WHERE username = ?', [username]);
+    if (users.length === 0) {
+      logger.warn(`登录失败:用户名 ${username} 不存在`); // 记录警告日志
+      return res.status(401).json({ 
+        success: false,
+        message: '用户名或密码错误' 
+      });
+    }
+
+    const user = users[0];
+    const isPasswordValid = await bcrypt.compare(password, user.password);
+    
+    if (!isPasswordValid) {
+      logger.warn(`用户 ${username} 登录失败:密码错误`); // 记录警告日志
+      return res.status(401).json({ 
+        success: false,
+        message: '用户名或密码错误' 
+      });
+    }
+
+    const token = jwt.sign({ userId: user.id }, 'your-secret-key', { expiresIn: '1h' });
+    logger.info(`用户 ${username} 登录成功,生成新的token`);
+    logger.debug(`用户 ${username} 的token将在1小时后过期`);
+    
+    res.json({ 
+      success: true,
+      token,
+      message: '登录成功'
+    });
+  } catch (error) {
+    logger.error('登录过程发生错误:', error); // 记录错误日志
+    res.status(500).json({ 
+      success: false,
+      message: '登录失败,请稍后重试' 
+    });
+  }
+};
+
+// 可以添加其他身份验证相关的方法
+exports.verifyToken = (req, res, next) => {
+  const token = req.headers.authorization?.split(' ')[1];
+  logger.debug('开始验证token');
+
+  if (!token) {
+    logger.warn('请求未携带token');
+    return res.status(401).json({ 
+      success: false,
+      message: '未提供认证token' 
+    });
+  }
+
+  try {
+    const decoded = jwt.verify(token, 'your-secret-key');
+    req.userId = decoded.userId;
+    logger.debug(`token验证成功,用户ID: ${decoded.userId}`);
+    next();
+  } catch (error) {
+    logger.error('token验证失败:', error);
+    res.status(401).json({ 
+      success: false,
+      message: 'token无效或已过期' 
+    });
+  }
+};

+ 448 - 0
controllers/deviceController.js

@@ -0,0 +1,448 @@
+const pool = require('../config/db');
+const logger = require('../logger'); // 引入日志记录器
+const mqtt = require('../mqttClient'); // 从根目录引入
+const client = mqtt.client; // 根据你的导出方式调整
+// 新增设备查询方法
+getDevices: async (req, res) => {
+  try {
+    const fields = req.query.fields || 'device_id,name,model,system_version,status'
+    const [results] = await pool.promise().query(`
+      SELECT ${fields} 
+      FROM devices
+      WHERE system_version IS NOT NULL
+    `)
+    res.json(results)
+  } catch (error) {
+    logger.error('获取设备列表失败:', error)
+    res.status(500).json({ code: 500, message: '服务器内部错误' })
+  }
+}
+
+const getDevices = (req, res) => {
+  logger.info('开始获取所有设备信息');
+  const query = `
+    SELECT 
+      d.id, d.device_id, d.room_id, r.room_name, d.status, d.last_seen, d.name, 
+      d.temperature, d.upload_time, d.switch_status, d.switch_status_time, 
+      d.level_status, d.level_status_time, d.bound_device_id, d.bound_time, 
+      d.first_online_time, d.last_online_time, d.last_offline_time, d.ip_address
+    FROM devices d
+    LEFT JOIN rooms r ON d.room_id = r.id
+  `;
+
+  pool.query(query, (err, results) => {
+    if (err) {
+      logger.error('查询所有设备失败:', err);
+      return res.status(500).send('Error querying devices');
+    }
+    logger.info(`成功获取 ${results.length} 个设备信息`);
+    res.status(200).json(results);
+  });
+};
+
+// 绑定设备到房间
+const bindDeviceToRoom = (req, res) => {
+  const { deviceId, roomId } = req.body;
+  logger.info(`尝试绑定设备 ${deviceId} 到房间 ${roomId}`);
+
+  if (!deviceId || !roomId) {
+    logger.warn('绑定设备失败:设备 ID 和房间 ID 不能为空');
+    return res.status(400).json({ success: false, message: '设备 ID 和房间 ID 不能为空' });
+  }
+
+  const isRelayDevice = deviceId.includes('ESP32');
+
+  if (isRelayDevice) {
+    // 检查房间是否已经绑定了继电器
+    const checkRelayQuery = `
+      SELECT device_id 
+      FROM devices 
+      WHERE room_id = ? AND device_id LIKE '%ESP32%'
+    `;
+
+    pool.query(checkRelayQuery, [roomId], (err, results) => {
+      if (err) {
+        logger.error('检查继电器设备失败:', err);
+        return res.status(500).json({ success: false, message: '检查继电器设备时出错' });
+      }
+
+      if (results.length > 0) {
+        logger.warn(`绑定设备失败:房间 ${roomId} 已经绑定了继电器`);
+        return res.status(400).json({ success: false, message: '该房间已经绑定了继电器,无法再次绑定' });
+      }
+
+      // 执行绑定操作
+      executeBinding(deviceId, roomId, res);
+    });
+  } else {
+    // 非继电器设备直接执行绑定
+    executeBinding(deviceId, roomId, res);
+  }
+};
+
+// 辅助函数:执行设备绑定
+const executeBinding = (deviceId, roomId, res) => {
+  // 首先获取房间名称
+  const getRoomNameQuery = 'SELECT room_name FROM rooms WHERE id = ?';
+  
+  pool.query(getRoomNameQuery, [roomId], (err, roomResults) => {
+    if (err) {
+      logger.error('获取房间名称失败:', err);
+      return res.status(500).json({ success: false, message: '获取房间名称时出错' });
+    }
+
+    if (roomResults.length === 0) {
+      logger.warn(`房间 ${roomId} 不存在`);
+      return res.status(404).json({ success: false, message: '房间不存在' });
+    }
+
+    const roomName = roomResults[0].room_name;
+
+    // 更新设备表中的 room_id 和 room_name
+    const bindQuery = 'UPDATE devices SET room_id = ?, room_name = ? WHERE device_id = ?';
+    
+    pool.query(bindQuery, [roomId, roomName, deviceId], (err, results) => {
+      if (err) {
+        logger.error('绑定设备失败:', err);
+        return res.status(500).json({ success: false, message: '绑定设备时出错' });
+      }
+
+      if (results.affectedRows === 0) {
+        logger.warn(`设备 ${deviceId} 不存在或已绑定到相同房间`);
+        return res.status(404).json({ success: false, message: '设备不存在或已绑定到相同房间' });
+      }
+
+      logger.info(`设备 ${deviceId} 成功绑定到房间 ${roomId}`);
+      res.status(200).json({ success: true, message: '设备绑定成功' });
+    });
+  });
+};
+
+// 解绑设备
+const unbindDevice = (req, res) => {
+  const { deviceId } = req.params;
+  logger.info(`尝试解绑设备: ${deviceId}`);
+
+  const query = 'UPDATE devices SET room_id = NULL, room_name = NULL WHERE device_id = ?';
+  pool.query(query, [deviceId], (err, results) => {
+    if (err) {
+      logger.error('解绑设备失败:', err);
+      return res.status(500).send('Error unbinding device');
+    }
+    logger.info(`设备 ${deviceId} 解绑成功`);
+    res.status(200).send('设备已从房间中移除');
+  });
+};
+
+// 删除设备
+const deleteDevice = (req, res) => {
+  const { deviceId } = req.params;
+  logger.info(`尝试删除设备: ${deviceId}`);
+
+  const query = 'DELETE FROM devices WHERE device_id = ?';
+  pool.query(query, [deviceId], (err, results) => {
+    if (err) {
+      logger.error('删除设备失败:', err);
+      return res.status(500).send('Error deleting device');
+    }
+    logger.info(`设备 ${deviceId} 删除成功`);
+    res.status(200).send('设备删除成功');
+  });
+};
+
+// 更新设备信息
+const updateDevice = async (req, res) => {
+  const { deviceId, name, roomId } = req.body;
+  logger.info(`尝试更新设备信息: deviceId=${deviceId}, name=${name}, roomId=${roomId}`);
+
+  try {
+    // 检查设备类型
+    const deviceQuery = 'SELECT device_id FROM devices WHERE device_id = ?';
+    const [deviceResults] = await pool.promise().query(deviceQuery, [deviceId]);
+
+    if (deviceResults.length === 0) {
+      logger.warn(`设备 ${deviceId} 不存在`);
+      return res.status(404).json({ success: false, message: '设备不存在' });
+    }
+
+    // 如果是继电器设备,检查目标房间是否已有其他继电器
+    if (deviceId.includes('ESP32')) {
+      const relayQuery = `
+        SELECT device_id 
+        FROM devices 
+        WHERE room_id = ? AND device_id LIKE "%ESP32%" AND device_id != ?
+      `;
+      const [relayResults] = await pool.promise().query(relayQuery, [roomId, deviceId]);
+
+      if (relayResults.length > 0) {
+        logger.warn(`房间 ${roomId} 已绑定其他继电器`);
+        return res.status(400).json({ success: false, message: '该房间已绑定其他继电器' });
+      }
+    }
+
+    // 获取房间名称
+    let roomName = null;
+    if (roomId) {
+      const roomQuery = 'SELECT room_name FROM rooms WHERE id = ?';
+      const [roomResults] = await pool.promise().query(roomQuery, [roomId]);
+
+      if (roomResults.length > 0) {
+        roomName = roomResults[0].room_name;
+      } else {
+        logger.warn(`房间 ${roomId} 不存在`);
+        return res.status(404).json({ success: false, message: '房间不存在' });
+      }
+    }
+
+    // 更新设备信息,包括房间名称
+    const updateQuery = 'UPDATE devices SET name = ?, room_id = ?, room_name = ? WHERE device_id = ?';
+    await pool.promise().query(updateQuery, [name, roomId, roomName, deviceId]);
+
+    logger.info(`成功更新设备信息: deviceId=${deviceId}, roomName=${roomName}`);
+    res.status(200).json({ success: true, message: '设备信息更新成功' });
+  } catch (err) {
+    logger.error('更新设备信息失败:', err);
+    res.status(500).json({ success: false, message: '更新设备信息失败' });
+  }
+};
+
+// 获取设备状态
+const getDeviceStatus = (req, res) => {
+  logger.info('开始获取设备状态信息');
+  const query = `
+    SELECT 
+      d.device_id, 
+      d.status AS device_status, 
+      rs.state AS relay_state, 
+      rs.temperature, 
+      rs.timestamp
+    FROM devices d
+    LEFT JOIN relay_state rs ON d.device_id = rs.device_id
+    ORDER BY rs.timestamp DESC
+  `;
+
+  pool.query(query, (err, results) => {
+    if (err) {
+      logger.error('查询设备状态失败:', err);
+      return res.status(500).send('Error querying device status');
+    }
+    logger.info(`成功获取 ${results.length} 个设备的状态信息`);
+    res.status(200).json(results);
+  });
+};
+
+// 设备管理界面查询绑定状态
+const getDevicesByRoom = (req, res) => {
+  const { roomId } = req.query;
+  logger.info(`开始获取房间 ${roomId} 的设备信息`);
+
+  if (!roomId) {
+    logger.warn('获取设备失败:房间ID为空');
+    return res.status(400).send('房间 ID 不能为空');
+  }
+
+  const query = `
+    SELECT 
+      device_id, 
+      name, 
+      status, 
+      temperature, 
+      switch_status, 
+      level_status
+    FROM devices
+    WHERE room_id = ?
+  `;
+
+  pool.query(query, [roomId], (err, results) => {
+    if (err) {
+      logger.error(`获取房间 ${roomId} 的设备失败:`, err);
+      return res.status(500).send('Error querying devices by room');
+    }
+    logger.info(`成功获取房间 ${roomId} 的 ${results.length} 个设备信息`);
+    res.status(200).json(results);
+  });
+};
+
+// 设备管理在线状态
+const getDeviceStatusByRoom = (req, res) => {
+  const { roomId } = req.query;
+  logger.info(`开始获取房间 ${roomId} 的设备状态`);
+
+  if (!roomId) {
+    logger.warn('获取设备状态失败:房间ID为空');
+    return res.status(400).send('房间 ID 不能为空');
+  }
+
+  const query = `
+    SELECT 
+      d.device_id, 
+      d.status AS device_status, 
+      rs.state AS relay_state, 
+      rs.temperature, 
+      rs.timestamp
+    FROM devices d
+    LEFT JOIN relay_state rs ON d.device_id = rs.device_id
+    WHERE d.room_id = ?
+    ORDER BY rs.timestamp DESC
+  `;
+
+  pool.query(query, [roomId], (err, results) => {
+    if (err) {
+      logger.error(`获取房间 ${roomId} 的设备状态失败:`, err);
+      return res.status(500).send('Error querying device status');
+    }
+    logger.info(`成功获取房间 ${roomId} 的 ${results.length} 个设备状态`);
+    res.status(200).json(results);
+  });
+};
+
+// 获取 GPIO 状态
+const getGpioState = (req, res) => {
+  const { deviceId } = req.query;
+  logger.info(`开始获取设备 ${deviceId} 的GPIO状态`);
+
+  if (!deviceId) {
+    logger.warn('获取GPIO状态失败:设备ID为空');
+    return res.status(400).send('设备 ID 不能为空');
+  }
+
+  const query = `
+    SELECT state 
+    FROM gpio_state 
+    WHERE device_id = ? 
+    ORDER BY timestamp DESC 
+    LIMIT 1
+  `;
+
+  pool.query(query, [deviceId], (err, results) => {
+    if (err) {
+      logger.error(`获取设备 ${deviceId} 的GPIO状态失败:`, err);
+      return res.status(500).send('Error querying GPIO state');
+    }
+    logger.info(`成功获取设备 ${deviceId} 的GPIO状态`);
+    res.status(200).json(results[0] || { state: 'low' });
+  });
+};
+
+// 绑定继电器到人体传感器
+const bindRelayToSensor = async (req, res) => {
+  const { sensorId, roomId } = req.body;
+  logger.info(`尝试绑定房间 ${roomId} 的继电器到传感器 ${sensorId}`);
+
+  if (!sensorId || !roomId) {
+    logger.warn('绑定失败:传感器ID或房间ID为空');
+    return res.status(400).json({ success: false, message: '传感器ID和房间ID不能为空' });
+  }
+
+  try {
+    // 查询房间中的继电器设备
+    const relayQuery = `
+      SELECT device_id 
+      FROM devices 
+      WHERE room_id = ? AND device_id LIKE '%ESP32%'
+    `;
+    const [relayResults] = await pool.promise().query(relayQuery, [roomId]);
+
+    if (relayResults.length === 0) {
+      logger.warn(`房间 ${roomId} 没有继电器设备`);
+      return res.status(404).json({ success: false, message: '该房间没有继电器设备' });
+    }
+
+    const relayId = relayResults[0].device_id;
+    logger.info(`找到继电器设备: ${relayId}`);
+
+    // 更新人体传感器模块的 bound_device_id 和 bound_time
+    const updateQuery = `
+      UPDATE devices 
+      SET bound_device_id = ?, bound_time = NOW() 
+      WHERE device_id = ?
+    `;
+    await pool.promise().query(updateQuery, [relayId, sensorId]);
+
+    // 通过 MQTT 发送继电器 ID
+    const controlTopic = `device/${sensorId}/control`;
+    client.publish(controlTopic, relayId, { qos: 1 }, (err) => {  // 添加了 qos: 1 确保消息可靠传递
+      if (err) {
+        logger.error('MQTT消息发送失败:', err);
+        return res.status(500).json({ success: false, message: 'MQTT 消息发布失败' });
+      }
+      
+      logger.info(`成功绑定并配置继电器: 传感器=${sensorId}, 继电器=${relayId}`);
+      res.status(200).json({ 
+        success: true,
+        message: '继电器与传感器绑定成功,并已发送配置信息'
+      });
+    });
+  } catch (error) {
+    logger.error('绑定继电器到传感器失败:', error);
+    res.status(500).json({ success: false, message: '处理绑定请求时出错' });
+  }
+};
+
+// 新增:获取所有房间及其设备信息
+const getAllRoomsWithDevices = async (req, res) => {
+  logger.info('开始获取所有房间及其设备信息');
+  
+  const query = `
+    SELECT 
+      r.id AS room_id,
+      r.room_name,
+      r.orientation,
+      d.id AS device_id,
+      d.device_id AS device_uid,
+      d.status,
+      d.temperature,
+      d.switch_status,
+      d.level_status
+    FROM rooms r
+    LEFT JOIN devices d ON r.id = d.room_id
+    ORDER BY r.id
+  `;
+
+  pool.query(query, (err, results) => {
+    if (err) {
+      logger.error('获取房间和设备信息失败:', err);
+      return res.status(500).send('Error querying rooms and devices');
+    }
+    
+    // 将结果按房间分组
+    const roomsMap = new Map();
+    results.forEach(row => {
+      if (!roomsMap.has(row.room_id)) {
+        roomsMap.set(row.room_id, {
+          id: row.room_id,
+          room_name: row.room_name,
+          orientation: row.orientation,
+          devices: []
+        });
+      }
+      if (row.device_id) {
+        roomsMap.get(row.room_id).devices.push({
+          device_id: row.device_uid,
+          status: row.status,
+          temperature: row.temperature,
+          switch_status: row.switch_status,
+          level_status: row.level_status
+        });
+      }
+    });
+
+    const rooms = Array.from(roomsMap.values());
+    logger.info(`成功获取 ${rooms.length} 个房间及其设备信息`);
+    res.status(200).json(rooms);
+  });
+};
+
+module.exports = {
+  getDevices,
+  bindDeviceToRoom,
+  unbindDevice,
+  deleteDevice,
+  updateDevice,
+  getDeviceStatus,
+  getDeviceStatusByRoom,
+  getDevicesByRoom,
+  getGpioState,
+  bindRelayToSensor,
+  getAllRoomsWithDevices
+};

+ 35 - 0
controllers/heaterUsageController.js

@@ -0,0 +1,35 @@
+const pool = require('../config/db');
+const logger = require('../logger');
+
+module.exports = {
+    getUsageData: async (req, res) => {
+        const { range } = req.query;
+        logger.info(`获取电暖器使用数据请求: 时间范围=${range}`);
+        
+        try {
+            let query = `
+                SELECT r.room_name, SUM(h.duration) AS duration, h.date
+                FROM heater_usage h
+                JOIN rooms r ON h.room_id = r.id
+            `;
+
+            if (range === 'week') {
+                logger.debug('按周统计电暖器使用数据');
+                query += ` GROUP BY YEARWEEK(h.date, 1), h.room_id`;
+            } else if (range === 'month') {
+                logger.debug('按月统计电暖器使用数据');
+                query += ` GROUP BY YEAR(h.date), MONTH(h.date), h.room_id`;
+            } else {
+                logger.debug('按天统计电暖器使用数据');
+                query += ` GROUP BY h.date, h.room_id`;
+            }
+
+            const [results] = await pool.promise().query(query);
+            logger.info(`成功获取电暖器使用数据,记录数: ${results.length}`);
+            res.json(results);
+        } catch (error) {
+            logger.error(`获取电暖器使用数据失败: ${error}`);
+            res.status(500).send('获取数据失败');
+        }
+    }
+};

+ 155 - 0
controllers/otaController.js

@@ -0,0 +1,155 @@
+const pool = require('../config/db');
+const logger = require('../logger');
+const fs = require('fs');
+const path = require('path');
+const multer = require('multer');
+const { v4: uuidv4 } = require('uuid');
+const crypto = require('crypto');
+
+const FIRMWARE_PATH = path.join(__dirname, '../sketch_feb24a.ino.bin');
+const LATEST_FIRMWARE_VERSION = "1.0.3";
+const upload = multer({ dest: 'uploads/' });
+
+// 固件签名验证函数
+const validateFirmwareSignature = (filePath) => {
+    // 这里添加实际的签名验证逻辑
+    return true; // 暂时返回true用于测试
+};
+// 新增固件版本查询方法
+module.exports = {
+    // 原有检查更新方法
+    checkUpdates: async (req, res) => {
+        const { device_id, current_version } = req.body;
+        logger.info(`OTA检查请求: 设备=${device_id}, 版本=${current_version}`);
+        
+        try {
+            const [device] = await pool.promise().query(
+                'SELECT * FROM devices WHERE device_id = ?', 
+                [device_id]
+            );
+            
+            if (!device.length) {
+                return res.status(404).json({ 
+                    code: 404, 
+                    message: '设备未注册' 
+                });
+            }
+            
+            const requiresUpdate = current_version !== LATEST_FIRMWARE_VERSION;
+            const updateInfo = {
+                latest_version: LATEST_FIRMWARE_VERSION,
+                release_notes: "1. 优化温控逻辑\n2. 修复安全漏洞",
+                file_size: fs.statSync(FIRMWARE_PATH).size,
+                mandatory: requiresUpdate
+            };
+            
+            res.json({
+                code: 200,
+                requires_update: requiresUpdate,
+                ...updateInfo
+            });
+        } catch (error) {
+            logger.error('检查更新失败:', error);
+            res.status(500).json({ 
+                code: 500, 
+                message: '服务器内部错误' 
+            });
+        }
+    },
+
+    // 原有下载方法
+    downloadFirmware: (req, res) => {
+        if (fs.existsSync(FIRMWARE_PATH)) {
+            res.download(FIRMWARE_PATH);
+        } else {
+            res.status(404).send("固件文件未找到");
+        }
+    },
+
+    // 新增上传处理方法
+    uploadFirmware: upload,
+    handleFirmwareUpload: async (req, res) => {
+        if (!req.file) {
+            return res.status(400).json({ 
+                code: 400, 
+                message: '未选择固件文件' 
+            });
+        }
+        
+        try {
+            if (!validateFirmwareSignature(req.file.path)) {
+                fs.unlinkSync(req.file.path);
+                return res.status(403).json({ 
+                    code: 403, 
+                    message: '固件签名验证失败' 
+                });
+            }
+            
+            fs.renameSync(req.file.path, FIRMWARE_PATH);
+            res.json({ 
+                code: 200, 
+                message: '固件上传成功',
+                version: LATEST_FIRMWARE_VERSION
+            });
+        } catch (error) {
+            res.status(500).json({ 
+                code: 500, 
+                message: '固件上传失败' 
+            });
+        }
+    },
+
+    // 新增升级方法
+    upgradeStatus: new Map(),
+    startUpgrade: (req, res) => {
+        const { device_id } = req.body;
+        const upgradeId = uuidv4();
+        
+        // 修复this指向问题
+        module.exports.upgradeStatus.set(upgradeId, {
+            device_id,
+            progress: 0,
+            status: 'pending',
+            start_time: Date.now()
+        });
+        
+        res.json({ 
+            code: 200, 
+            message: '升级已启动',
+            upgrade_id: upgradeId
+        });
+    },  // ← 保持逗号分隔
+    checkDeviceUpdate: async (req, res) => {
+        const { deviceId, currentVersion } = req.body;
+        try {
+            const [versions] = await pool.promise().query(
+                'SELECT * FROM firmware_versions WHERE device_type = ? ORDER BY release_date DESC LIMIT 1',
+                [deviceId]
+            );
+            
+            // 添加缺失的闭合括号和错误处理
+            const latest = versions[0] || { version_number: currentVersion };
+            const hasUpdate = latest.version_number > currentVersion;
+            
+            res.json({
+                code: 200,
+                hasUpdate,
+                latestVersion: latest.version_number
+            });
+        } catch (error) {
+            logger.error('检查更新失败:', error);
+            res.status(500).json({ code: 500, message: '服务器内部错误' });
+        }
+    },  // ← 添加逗号分隔
+    getFirmwareVersions: async (req, res) => {
+        try {
+            const [results] = await pool.promise().query(
+                'SELECT * FROM firmware_versions ORDER BY release_date DESC'
+            );
+            res.json({ code: 200, data: results });
+        } catch (error) {
+            logger.error('获取固件版本失败:', error);
+            res.status(500).json({ code: 500, message: '服务器内部错误' });
+        }
+    }
+};  // ← 确保对象正确闭合

+ 109 - 0
controllers/relayController.js

@@ -0,0 +1,109 @@
+const pool = require('../config/db');
+const { client } = require('../mqttClient');
+const logger = require('../logger');
+
+// 控制继电器状态
+const toggleRelay = async (req, res) => {
+  const { state } = req.params; // 获取状态参数(on 或 off)
+  const { roomId } = req.query; // 获取房间 ID
+  logger.info(`尝试设置房间 ${roomId} 的继电器状态为 ${state}`);
+
+  // 校验 state 参数
+  if (state.toLowerCase() !== 'on' && state.toLowerCase() !== 'off') {
+    logger.warn(`无效的继电器状态参数: ${state}`);
+    return res.status(400).send('无效的状态参数,请使用 "on" 或 "off"');
+  }
+
+  // 校验 roomId 参数
+  if (!roomId) {
+    logger.warn('缺少房间ID参数');
+    return res.status(400).send('房间 ID 不能为空');
+  }
+
+  try {
+    // 查询房间的继电器设备
+    const query = `
+      SELECT device_id 
+      FROM devices 
+      WHERE room_id = ? AND status = 'online' AND device_id LIKE '%ESP32%'
+      LIMIT 1
+    `;
+    const [results] = await pool.promise().query(query, [roomId]);
+
+    if (results.length === 0) {
+      logger.warn(`房间 ${roomId} 没有在线的继电器设备`);
+      return res.status(404).send('该房间没有在线的继电器设备');
+    }
+
+    const deviceId = results[0].device_id;
+    const relayTopic = `device/${deviceId}/relay/control`;
+
+    if (!client.connected) {
+      logger.error('MQTT客户端未连接');
+      return res.status(500).send('MQTT 客户端未连接');
+    }
+
+    // 发布 MQTT 消息
+    client.publish(relayTopic, state.toUpperCase(), (err) => {
+      if (err) {
+        logger.error('MQTT消息发送失败:', err);
+        return res.status(500).send('发布消息失败: ' + err.message);
+      }
+      logger.info(`成功发送继电器控制命令: 房间=${roomId}, 状态=${state}`);
+      res.send(`房间 ${roomId} 的继电器状态已设置为 ${state}`);
+    });
+  } catch (err) {
+    logger.error('控制继电器失败:', err);
+    res.status(500).send('控制继电器失败');
+  }
+};
+
+// 下发继电器ID给人体存在传感器
+const sendRelayIdToSensor = async (req, res) => {
+  const { roomId } = req.params;
+  logger.info(`尝试为房间 ${roomId} 的人体传感器配置继电器ID`);
+
+  try {
+    // 查询房间的人体传感器和继电器设备
+    const query = `
+      SELECT 
+        (SELECT device_id FROM devices WHERE room_id = ? AND device_id LIKE '%24G-%' LIMIT 1) AS humanSensorDeviceId,
+        (SELECT device_id FROM devices WHERE room_id = ? AND device_id LIKE '%ESP32%' LIMIT 1) AS relayDeviceId
+    `;
+    const [results] = await pool.promise().query(query, [roomId, roomId]);
+
+    if (!results[0].humanSensorDeviceId) {
+      logger.warn(`房间 ${roomId} 未绑定人体传感器模块`);
+      return res.status(404).json({ success: false, message: '该房间未绑定人体传感器模块' });
+    }
+
+    if (!results[0].relayDeviceId) {
+      logger.warn(`房间 ${roomId} 未绑定继电器模块`);
+      return res.status(404).json({ success: false, message: '该房间未绑定继电器模块' });
+    }
+
+    const { humanSensorDeviceId, relayDeviceId } = results[0];
+    const controlTopic = `device/${humanSensorDeviceId}/control`;
+
+    // 发布 MQTT 消息
+    client.publish(controlTopic, relayDeviceId, (err) => {
+      if (err) {
+        logger.error('MQTT消息发送失败:', err);
+        return res.status(500).json({ success: false, message: 'MQTT 消息发布失败' });
+      }
+      logger.info(`成功配置继电器ID: 传感器=${humanSensorDeviceId}, 继电器=${relayDeviceId}`);
+      res.status(200).json({ 
+        success: true, 
+        message: `已将继电器 ${relayDeviceId} 配置给人体传感器模块` 
+      });
+    });
+  } catch (error) {
+    logger.error('配置继电器ID失败:', error);
+    res.status(500).json({ success: false, message: '处理人体传感器模块时出错' });
+  }
+};
+
+module.exports = {
+  toggleRelay,
+  sendRelayIdToSensor,
+};

+ 101 - 0
controllers/roomController.js

@@ -0,0 +1,101 @@
+const pool = require('../config/db');
+const logger = require('../logger');
+
+// 内存缓存
+const roomCache = new Map();
+
+// 获取所有房间(支持分页)
+const getRooms = async (req, res) => {
+  const { page = 1, pageSize = 20 } = req.query;
+  const offset = (page - 1) * pageSize;
+  logger.info(`开始获取房间信息,页码: ${page}, 每页大小: ${pageSize}`);
+
+  try {
+    // 查询数据库
+    const query = `
+      SELECT * FROM rooms
+      LIMIT ? OFFSET ?
+    `;
+    const [results] = await pool.promise().query(query, [parseInt(pageSize), parseInt(offset)]);
+
+    logger.info(`成功获取 ${results.length} 个房间信息`);
+    res.status(200).json(results);
+  } catch (err) {
+    logger.error('查询房间失败:', err);
+    res.status(500).send('Error querying rooms');
+  }
+};
+
+// 新增房间
+const addRoom = async (req, res) => {
+  const { room_name, description, floor, orientation } = req.body;
+  logger.info(`尝试添加新房间: ${room_name}`);
+
+  if (!room_name) {
+    logger.warn('添加房间失败: 房间名称为空');
+    return res.status(400).send('房间名称不能为空');
+  }
+
+  try {
+    const query = 'INSERT INTO rooms (room_name, description, floor, orientation) VALUES (?, ?, ?, ?)';
+    const [results] = await pool.promise().query(query, [room_name, description, floor, orientation]);
+
+    logger.info(`成功添加房间: ${room_name}, ID: ${results.insertId}`);
+    res.status(201).json({ id: results.insertId, room_name, description, floor, orientation });
+  } catch (err) {
+    logger.error('添加房间失败:', err);
+    res.status(500).send('Error adding room');
+  }
+};
+
+// 删除房间
+const deleteRoom = async (req, res) => {
+  const roomId = req.params.id;
+  logger.info(`尝试删除房间, ID: ${roomId}`);
+
+  try {
+    const query = 'DELETE FROM rooms WHERE id = ?';
+    const [results] = await pool.promise().query(query, [roomId]);
+
+    logger.info(`成功删除房间, ID: ${roomId}`);
+    res.status(200).json({ message: '房间删除成功' });
+  } catch (err) {
+    logger.error(`删除房间失败, ID: ${roomId}:`, err);
+    res.status(500).send('Error deleting room');
+  }
+};
+
+// 更新房间信息
+const updateRoom = async (req, res) => {
+  const roomId = req.params.id;
+  const { room_name, description, floor, orientation } = req.body;
+  logger.info(`尝试更新房间信息, ID: ${roomId}`);
+
+  if (!room_name) {
+    logger.warn('更新房间失败: 房间名称为空');
+    return res.status(400).send('房间名称不能为空');
+  }
+
+  try {
+    const query = 'UPDATE rooms SET room_name = ?, description = ?, floor = ?, orientation = ? WHERE id = ?';
+    const [results] = await pool.promise().query(query, [room_name, description, floor, orientation, roomId]);
+
+    if (results.affectedRows === 0) {
+      logger.warn(`房间 ${roomId} 不存在`);
+      return res.status(404).send('房间不存在');
+    }
+
+    logger.info(`成功更新房间信息, ID: ${roomId}`);
+    res.status(200).json({ message: '房间信息更新成功' });
+  } catch (err) {
+    logger.error(`更新房间信息失败, ID: ${roomId}:`, err);
+    res.status(500).send('Error updating room');
+  }
+};
+
+module.exports = {
+  getRooms,
+  addRoom,
+  deleteRoom,
+  updateRoom,
+};

+ 41 - 0
logger.js

@@ -0,0 +1,41 @@
+const winston = require('winston');
+const path = require('path');
+
+// 定义日志格式
+const logFormat = winston.format.combine(
+  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
+  winston.format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
+);
+
+// 创建日志记录器
+const logger = winston.createLogger({
+  level: 'debug', // 日志级别
+  format: logFormat,
+  transports: [
+    // 输出到控制台
+    new winston.transports.Console({
+      format: winston.format.combine(
+        winston.format.colorize(), // 添加颜色
+        logFormat
+      )
+    }),
+    // 输出到文件
+    new winston.transports.File({
+      filename: path.join(__dirname, '../logs', 'app.log'), // 日志文件路径
+      maxsize: 10485760, // 10MB
+      maxFiles: 5,
+      tailable: true
+    }),
+    // 添加业务日志分类
+    new winston.transports.File({
+      filename: path.join(__dirname, '../logs', 'business.log'), // 业务日志文件路径
+      format: winston.format.combine(
+        winston.format.timestamp(),
+        winston.format.json()
+      ),
+      level: 'info' // 业务日志级别
+    })
+  ]
+});
+
+module.exports = logger;

+ 85 - 0
migrations/202406020000_heater_usage_procedure.sql

@@ -0,0 +1,85 @@
+DELIMITER $$
+
+CREATE PROCEDURE ProcessHeaterUsage(
+    IN p_device_id VARCHAR(255),
+    IN p_event_time DATETIME,
+    IN p_raw_id INT
+)
+BEGIN
+    DECLARE v_room_id INT;
+    DECLARE v_room_name VARCHAR(255);
+    DECLARE v_device_status ENUM('online','offline');
+    DECLARE v_switch_status ENUM('on','off');
+    DECLARE v_temperature DECIMAL(5,2);
+    
+    -- 唯一的异常处理器声明
+    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
+    BEGIN
+        INSERT INTO error_logs (error_message, error_time)
+        VALUES (CONCAT('Heater usage processing failed for ', p_device_id), NOW());
+    END;
+
+    -- 获取设备最新状态
+    SELECT 
+        d.room_id,
+        d.room_name,
+        d.status,
+        d.switch_status,
+        d.temperature
+    INTO 
+        v_room_id,
+        v_room_name,
+        v_device_status,
+        v_switch_status,
+        v_temperature
+    FROM devices d
+    WHERE d.device_id = p_device_id
+    LIMIT 1;
+    
+    -- 开启条件:在线、开关开启、温度>14
+    IF v_device_status = 'online' 
+       AND v_switch_status = 'on' 
+       AND v_temperature > 14 THEN
+        
+        -- 检查是否存在未关闭记录
+        IF NOT EXISTS (
+            SELECT 1 FROM heater_usage 
+            WHERE device_id = p_device_id 
+            AND end_time IS NULL
+        ) THEN
+            INSERT INTO heater_usage (
+                device_id,
+                room_id,
+                room_name,
+                start_time,
+                end_time,
+                duration,
+                date
+            ) VALUES (
+                p_device_id,
+                v_room_id,
+                v_room_name,
+                p_event_time,
+                NULL,
+                0,
+                DATE(p_event_time - INTERVAL IF(HOUR(p_event_time) < 12, 1, 0) DAY)
+            );
+        END IF;
+
+    -- 关闭条件:任意条件不满足时更新结束时间
+    ELSE
+        UPDATE heater_usage
+        SET end_time = p_event_time,
+            duration = TIMESTAMPDIFF(SECOND, start_time, p_event_time),
+            -- 同步更新房间信息(设备可能更换房间)
+            room_id = v_room_id,
+            room_name = v_room_name
+        WHERE device_id = p_device_id
+          AND end_time IS NULL
+        ORDER BY start_time DESC
+        LIMIT 1;
+    END IF;
+
+END$$
+
+DELIMITER ;

+ 49 - 0
mqttClient.js

@@ -0,0 +1,49 @@
+const mqtt = require('mqtt');
+const logger = require('./logger');
+
+const mqttBroker = 'mqtt://192.168.3.31';
+const mqttUser = 'yangfei';
+const mqttPassword = 'yangfei';
+
+const client = mqtt.connect(mqttBroker, {
+  username: mqttUser,
+  password: mqttPassword,
+  clientId: '后台服务员', // 设置客户端 ID
+  // clientId: `backend_${Date.now()}`, // 使用时间戳生成唯一客户端ID
+  clean: true, // 清除会话
+  reconnectPeriod: 5000, // 5秒重连间隔
+  connectTimeout: 30000, // 30秒连接超时
+  keepalive: 60 // 60秒心跳
+});
+
+client.on('connect', () => {
+  if (!client.connected) {
+    logger.info('后台管理员上线');
+  }
+  logger.info('MQTT 连接成功');
+});
+
+client.on('reconnect', () => {
+  logger.info('正在尝试重新连接 MQTT 代理...');
+});
+
+client.on('close', () => {
+  logger.warn('MQTT 连接已断开');
+});
+
+client.on('error', (err) => {
+  logger.error('MQTT 连接失败:', err.message);
+  // 尝试重新连接
+  setTimeout(() => {
+    client.reconnect();
+  }, 5000);
+});
+
+client.on('offline', () => {
+  logger.warn('MQTT 客户端已离线');
+});
+
+// 动态生成 relayTopic
+const getRelayTopic = (deviceId) => `device/${deviceId}/relay/control`;
+
+module.exports = { client, getRelayTopic };

+ 273 - 0
myiot.sql

@@ -0,0 +1,273 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server         : 192.168.3.22
+ Source Server Type    : MySQL
+ Source Server Version : 101106 (10.11.6-MariaDB-0+deb12u1)
+ Source Host           : localhost:3306
+ Source Schema         : myiot
+
+ Target Server Type    : MySQL
+ Target Server Version : 101106 (10.11.6-MariaDB-0+deb12u1)
+ File Encoding         : 65001
+
+ Date: 08/03/2025 03:15:55
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for authz_checks
+-- ----------------------------
+DROP TABLE IF EXISTS `authz_checks`;
+CREATE TABLE `authz_checks`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `topic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `action` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `result` enum('allow','deny') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `timestamp` datetime NOT NULL,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 55909 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for devices
+-- ----------------------------
+DROP TABLE IF EXISTS `devices`;
+CREATE TABLE `devices`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `device_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `room_id` int NULL DEFAULT NULL,
+  `room_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '房间名称',
+  `status` enum('online','offline') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'offline',
+  `last_seen` datetime NULL DEFAULT NULL,
+  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `temperature` decimal(5, 2) NULL DEFAULT NULL COMMENT '设备温度',
+  `upload_time` datetime NULL DEFAULT NULL COMMENT '数据上传时间',
+  `switch_status` enum('on','off') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开关状态',
+  `switch_status_time` datetime NULL DEFAULT NULL COMMENT '开关状态变化时间',
+  `level_status` enum('high','low') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '高低电平状态',
+  `level_status_time` datetime NULL DEFAULT NULL COMMENT '高低电平状态变化时间',
+  `bound_device_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定设备的ID',
+  `bound_time` datetime NULL DEFAULT NULL COMMENT '绑定时间',
+  `first_online_time` datetime NULL DEFAULT NULL COMMENT '第一次上线时间',
+  `last_online_time` datetime NULL DEFAULT NULL COMMENT '最近一次上线时间',
+  `last_offline_time` datetime NULL DEFAULT NULL COMMENT '最近一次离线时间',
+  `ip_address` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `system_version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'v1.0.0' COMMENT '系统版本',
+  `firmware_version` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1.0.0',
+  `last_upgrade_time` datetime NULL DEFAULT NULL,
+  `upgrade_status` enum('pending','upgrading','completed','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `device_id`(`device_id` ASC) USING BTREE,
+  INDEX `room_id`(`room_id` ASC) USING BTREE,
+  CONSTRAINT `devices_ibfk_1` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE = InnoDB AUTO_INCREMENT = 2749 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for error_logs
+-- ----------------------------
+DROP TABLE IF EXISTS `error_logs`;
+CREATE TABLE `error_logs`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `error_message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `error_time` datetime NOT NULL,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for firmware_versions
+-- ----------------------------
+DROP TABLE IF EXISTS `firmware_versions`;
+CREATE TABLE `firmware_versions`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `version_number` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `device_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `release_date` datetime NOT NULL,
+  `file_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `file_size` bigint NOT NULL,
+  `is_signed` tinyint(1) NULL DEFAULT 0,
+  `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `release_notes` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
+  `created_at` timestamp NULL DEFAULT current_timestamp,
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `version_number`(`version_number` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for gpio_state
+-- ----------------------------
+DROP TABLE IF EXISTS `gpio_state`;
+CREATE TABLE `gpio_state`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `device_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `state` enum('high','low') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `timestamp` timestamp NULL DEFAULT current_timestamp,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 238 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for heater_usage
+-- ----------------------------
+DROP TABLE IF EXISTS `heater_usage`;
+CREATE TABLE `heater_usage`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `device_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `room_id` int NOT NULL COMMENT '关联房间ID',
+  `room_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `start_time` datetime NOT NULL,
+  `end_time` datetime NULL DEFAULT NULL,
+  `duration` int NULL DEFAULT 0 COMMENT '使用时长(秒)',
+  `date` date NOT NULL COMMENT '统计日期',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `device_id`(`device_id` ASC) USING BTREE,
+  INDEX `idx_room_date`(`room_id` ASC, `date` ASC) USING BTREE,
+  CONSTRAINT `heater_usage_ibfk_1` FOREIGN KEY (`device_id`) REFERENCES `devices` (`device_id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+  CONSTRAINT `heater_usage_ibfk_2` FOREIGN KEY (`room_id`) REFERENCES `rooms` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for relay_state
+-- ----------------------------
+DROP TABLE IF EXISTS `relay_state`;
+CREATE TABLE `relay_state`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `device_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `state` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `temperature` float NULL DEFAULT NULL,
+  `room_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `room_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `timestamp` datetime NOT NULL,
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1002 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for rooms
+-- ----------------------------
+DROP TABLE IF EXISTS `rooms`;
+CREATE TABLE `rooms`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `room_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `floor` int NULL DEFAULT NULL,
+  `orientation` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
+  `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `room_name`(`room_name` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for switch_status
+-- ----------------------------
+DROP TABLE IF EXISTS `switch_status`;
+CREATE TABLE `switch_status`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `device_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `timestamp` datetime NOT NULL,
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `switch_status_ibfk_1`(`device_id` ASC) USING BTREE,
+  CONSTRAINT `switch_status_ibfk_1` FOREIGN KEY (`device_id`) REFERENCES `devices` (`device_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE = InnoDB AUTO_INCREMENT = 598 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for users
+-- ----------------------------
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
+  `created_at` timestamp NULL DEFAULT current_timestamp,
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE INDEX `username`(`username` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Procedure structure for ProcessHeaterUsage
+-- ----------------------------
+DROP PROCEDURE IF EXISTS `ProcessHeaterUsage`;
+delimiter ;;
+CREATE PROCEDURE `ProcessHeaterUsage`(IN p_device_id VARCHAR(255),
+    IN p_event_time DATETIME,
+    IN p_raw_id INT)
+BEGIN
+    DECLARE v_room_id INT;
+    DECLARE v_room_name VARCHAR(255);
+    DECLARE v_device_status ENUM('online','offline');
+    DECLARE v_switch_status ENUM('on','off');
+    DECLARE v_temperature DECIMAL(5,2);
+    
+    -- 唯一的异常处理器声明
+    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
+    BEGIN
+        INSERT INTO error_logs (error_message, error_time)
+        VALUES (CONCAT('Heater usage processing failed for ', p_device_id), NOW());
+    END;
+
+    -- 获取设备最新状态
+    SELECT 
+        d.room_id,
+        d.room_name,
+        d.status,
+        d.switch_status,
+        d.temperature
+    INTO 
+        v_room_id,
+        v_room_name,
+        v_device_status,
+        v_switch_status,
+        v_temperature
+    FROM devices d
+    WHERE d.device_id = p_device_id
+    LIMIT 1;
+    
+    -- 开启条件:在线、开关开启、温度>14
+    IF v_device_status = 'online' 
+       AND v_switch_status = 'on' 
+       AND v_temperature > 14 THEN
+        
+        -- 检查是否存在未关闭记录
+        IF NOT EXISTS (
+            SELECT 1 FROM heater_usage 
+            WHERE device_id = p_device_id 
+            AND end_time IS NULL
+        ) THEN
+            INSERT INTO heater_usage (
+                device_id,
+                room_id,
+                room_name,
+                start_time,
+                end_time,
+                duration,
+                date
+            ) VALUES (
+                p_device_id,
+                v_room_id,
+                v_room_name,
+                p_event_time,
+                NULL,
+                0,
+                DATE(p_event_time - INTERVAL IF(HOUR(p_event_time) < 12, 1, 0) DAY)
+            );
+        END IF;
+
+    -- 关闭条件:任意条件不满足时更新结束时间
+    ELSE
+        UPDATE heater_usage
+        SET end_time = p_event_time,
+            duration = TIMESTAMPDIFF(SECOND, start_time, p_event_time),
+            -- 同步更新房间信息(设备可能更换房间)
+            room_id = v_room_id,
+            room_name = v_room_name
+        WHERE device_id = p_device_id
+          AND end_time IS NULL
+        ORDER BY start_time DESC
+        LIMIT 1;
+    END IF;
+
+END
+;;
+delimiter ;
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 2928 - 0
package-lock.json

@@ -0,0 +1,2928 @@
+{
+    "name": "vueiot-api",
+    "version": "1.0.0",
+    "lockfileVersion": 3,
+    "requires": true,
+    "packages": {
+        "": {
+            "name": "vueiot-api",
+            "version": "1.0.0",
+            "license": "MIT",
+            "dependencies": {
+                "bcrypt": "^5.1.1",
+                "body-parser": "^1.20.2",
+                "cors": "^2.8.5",
+                "crypto-js": "^4.2.0",
+                "dayjs": "^1.11.13",
+                "express": "^4.18.2",
+                "jsonwebtoken": "^9.0.2",
+                "mqtt": "^5.10.3",
+                "multer": "^1.4.5-lts.1",
+                "mysql2": "^3.6.0",
+                "uuid": "^11.1.0",
+                "winston": "^3.11.0"
+            },
+            "devDependencies": {
+                "nodemon": "^3.0.2"
+            },
+            "engines": {
+                "node": ">=16.0.0"
+            }
+        },
+        "node_modules/@babel/runtime": {
+            "version": "7.26.9",
+            "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
+            "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
+            "license": "MIT",
+            "dependencies": {
+                "regenerator-runtime": "^0.14.0"
+            },
+            "engines": {
+                "node": ">=6.9.0"
+            }
+        },
+        "node_modules/@colors/colors": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
+            "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.1.90"
+            }
+        },
+        "node_modules/@dabh/diagnostics": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
+            "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
+            "license": "MIT",
+            "dependencies": {
+                "colorspace": "1.1.x",
+                "enabled": "2.0.x",
+                "kuler": "^2.0.0"
+            }
+        },
+        "node_modules/@mapbox/node-pre-gyp": {
+            "version": "1.0.11",
+            "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+            "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+            "license": "BSD-3-Clause",
+            "dependencies": {
+                "detect-libc": "^2.0.0",
+                "https-proxy-agent": "^5.0.0",
+                "make-dir": "^3.1.0",
+                "node-fetch": "^2.6.7",
+                "nopt": "^5.0.0",
+                "npmlog": "^5.0.1",
+                "rimraf": "^3.0.2",
+                "semver": "^7.3.5",
+                "tar": "^6.1.11"
+            },
+            "bin": {
+                "node-pre-gyp": "bin/node-pre-gyp"
+            }
+        },
+        "node_modules/@types/node": {
+            "version": "22.13.9",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
+            "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
+            "license": "MIT",
+            "dependencies": {
+                "undici-types": "~6.20.0"
+            }
+        },
+        "node_modules/@types/readable-stream": {
+            "version": "4.0.18",
+            "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz",
+            "integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "*",
+                "safe-buffer": "~5.1.1"
+            }
+        },
+        "node_modules/@types/readable-stream/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/@types/triple-beam": {
+            "version": "1.3.5",
+            "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
+            "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
+            "license": "MIT"
+        },
+        "node_modules/@types/ws": {
+            "version": "8.18.0",
+            "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
+            "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/node": "*"
+            }
+        },
+        "node_modules/abbrev": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+            "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+            "license": "ISC"
+        },
+        "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": "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/agent-base": {
+            "version": "6.0.2",
+            "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+            "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+            "license": "MIT",
+            "dependencies": {
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6.0.0"
+            }
+        },
+        "node_modules/agent-base/node_modules/debug": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+            "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+            "license": "MIT",
+            "dependencies": {
+                "ms": "^2.1.3"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/agent-base/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/ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "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/aproba": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+            "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
+            "license": "ISC"
+        },
+        "node_modules/are-we-there-yet": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+            "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+            "deprecated": "This package is no longer supported.",
+            "license": "ISC",
+            "dependencies": {
+                "delegates": "^1.0.0",
+                "readable-stream": "^3.6.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/are-we-there-yet/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/array-flatten": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+            "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+            "license": "MIT"
+        },
+        "node_modules/async": {
+            "version": "3.2.6",
+            "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+            "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+            "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/balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+            "license": "MIT"
+        },
+        "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/bcrypt": {
+            "version": "5.1.1",
+            "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
+            "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
+            "hasInstallScript": true,
+            "license": "MIT",
+            "dependencies": {
+                "@mapbox/node-pre-gyp": "^1.0.11",
+                "node-addon-api": "^5.0.0"
+            },
+            "engines": {
+                "node": ">= 10.0.0"
+            }
+        },
+        "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.0.20",
+            "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.20.tgz",
+            "integrity": "sha512-JMP0loH6ApbpT4Aa9oU5NqAkdDvcyc8koeuK8i5mYoBCVj3XCXG0uweGNN2m6DqaCO2yRHdm+MjCeTsR5VsmcA==",
+            "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": "1.20.3",
+            "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+            "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+            "license": "MIT",
+            "dependencies": {
+                "bytes": "3.1.2",
+                "content-type": "~1.0.5",
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "destroy": "1.2.0",
+                "http-errors": "2.0.0",
+                "iconv-lite": "0.4.24",
+                "on-finished": "2.4.1",
+                "qs": "6.13.0",
+                "raw-body": "2.5.2",
+                "type-is": "~1.6.18",
+                "unpipe": "1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8",
+                "npm": "1.2.8000 || >= 1.4.16"
+            }
+        },
+        "node_modules/brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "license": "MIT",
+            "dependencies": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "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/chownr": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+            "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+            "license": "ISC",
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/color": {
+            "version": "3.2.1",
+            "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
+            "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
+            "license": "MIT",
+            "dependencies": {
+                "color-convert": "^1.9.3",
+                "color-string": "^1.6.0"
+            }
+        },
+        "node_modules/color-convert": {
+            "version": "1.9.3",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+            "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+            "license": "MIT",
+            "dependencies": {
+                "color-name": "1.1.3"
+            }
+        },
+        "node_modules/color-name": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+            "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+            "license": "MIT"
+        },
+        "node_modules/color-string": {
+            "version": "1.9.1",
+            "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+            "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+            "license": "MIT",
+            "dependencies": {
+                "color-name": "^1.0.0",
+                "simple-swizzle": "^0.2.2"
+            }
+        },
+        "node_modules/color-support": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+            "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+            "license": "ISC",
+            "bin": {
+                "color-support": "bin.js"
+            }
+        },
+        "node_modules/colorspace": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
+            "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
+            "license": "MIT",
+            "dependencies": {
+                "color": "^3.1.3",
+                "text-hex": "1.0.x"
+            }
+        },
+        "node_modules/commist": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
+            "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
+            "license": "MIT"
+        },
+        "node_modules/concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+            "license": "MIT"
+        },
+        "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/console-control-strings": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+            "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+            "license": "ISC"
+        },
+        "node_modules/content-disposition": {
+            "version": "0.5.4",
+            "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+            "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+            "license": "MIT",
+            "dependencies": {
+                "safe-buffer": "5.2.1"
+            },
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "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.1",
+            "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+            "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/cookie-signature": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+            "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+            "license": "MIT"
+        },
+        "node_modules/core-util-is": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+            "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+            "license": "MIT"
+        },
+        "node_modules/cors": {
+            "version": "2.8.5",
+            "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+            "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+            "license": "MIT",
+            "dependencies": {
+                "object-assign": "^4",
+                "vary": "^1"
+            },
+            "engines": {
+                "node": ">= 0.10"
+            }
+        },
+        "node_modules/crypto-js": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+            "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+            "license": "MIT"
+        },
+        "node_modules/dayjs": {
+            "version": "1.11.13",
+            "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+            "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+            "license": "MIT"
+        },
+        "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/delegates": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+            "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+            "license": "MIT"
+        },
+        "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/destroy": {
+            "version": "1.2.0",
+            "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+            "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8",
+                "npm": "1.2.8000 || >= 1.4.16"
+            }
+        },
+        "node_modules/detect-libc": {
+            "version": "2.0.3",
+            "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+            "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+            "license": "Apache-2.0",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "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/emoji-regex": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+            "license": "MIT"
+        },
+        "node_modules/enabled": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
+            "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
+            "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/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/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": "4.21.2",
+            "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+            "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+            "license": "MIT",
+            "dependencies": {
+                "accepts": "~1.3.8",
+                "array-flatten": "1.1.1",
+                "body-parser": "1.20.3",
+                "content-disposition": "0.5.4",
+                "content-type": "~1.0.4",
+                "cookie": "0.7.1",
+                "cookie-signature": "1.0.6",
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "encodeurl": "~2.0.0",
+                "escape-html": "~1.0.3",
+                "etag": "~1.8.1",
+                "finalhandler": "1.3.1",
+                "fresh": "0.5.2",
+                "http-errors": "2.0.0",
+                "merge-descriptors": "1.0.3",
+                "methods": "~1.1.2",
+                "on-finished": "2.4.1",
+                "parseurl": "~1.3.3",
+                "path-to-regexp": "0.1.12",
+                "proxy-addr": "~2.0.7",
+                "qs": "6.13.0",
+                "range-parser": "~1.2.1",
+                "safe-buffer": "5.2.1",
+                "send": "0.19.0",
+                "serve-static": "1.16.2",
+                "setprototypeof": "1.2.0",
+                "statuses": "2.0.1",
+                "type-is": "~1.6.18",
+                "utils-merge": "1.0.1",
+                "vary": "~1.1.2"
+            },
+            "engines": {
+                "node": ">= 0.10.0"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/express"
+            }
+        },
+        "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/fecha": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
+            "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
+            "license": "MIT"
+        },
+        "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": "1.3.1",
+            "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+            "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+            "license": "MIT",
+            "dependencies": {
+                "debug": "2.6.9",
+                "encodeurl": "~2.0.0",
+                "escape-html": "~1.0.3",
+                "on-finished": "2.4.1",
+                "parseurl": "~1.3.3",
+                "statuses": "2.0.1",
+                "unpipe": "~1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/fn.name": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
+            "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
+            "license": "MIT"
+        },
+        "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": "0.5.2",
+            "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+            "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/fs-minipass": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+            "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+            "license": "ISC",
+            "dependencies": {
+                "minipass": "^3.0.0"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/fs-minipass/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+            "license": "ISC",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/fs.realpath": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+            "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+            "license": "ISC"
+        },
+        "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/gauge": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+            "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+            "deprecated": "This package is no longer supported.",
+            "license": "ISC",
+            "dependencies": {
+                "aproba": "^1.0.3 || ^2.0.0",
+                "color-support": "^1.1.2",
+                "console-control-strings": "^1.0.0",
+                "has-unicode": "^2.0.1",
+                "object-assign": "^4.1.1",
+                "signal-exit": "^3.0.0",
+                "string-width": "^4.2.3",
+                "strip-ansi": "^6.0.1",
+                "wide-align": "^1.1.2"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "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": {
+            "version": "7.2.3",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+            "deprecated": "Glob versions prior to v9 are no longer supported",
+            "license": "ISC",
+            "dependencies": {
+                "fs.realpath": "^1.0.0",
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "^3.1.1",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            },
+            "engines": {
+                "node": "*"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "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-unicode": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+            "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+            "license": "ISC"
+        },
+        "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/help-me": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
+            "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
+            "license": "MIT"
+        },
+        "node_modules/http-errors": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+            "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+            "license": "MIT",
+            "dependencies": {
+                "depd": "2.0.0",
+                "inherits": "2.0.4",
+                "setprototypeof": "1.2.0",
+                "statuses": "2.0.1",
+                "toidentifier": "1.0.1"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "node_modules/https-proxy-agent": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+            "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+            "license": "MIT",
+            "dependencies": {
+                "agent-base": "6",
+                "debug": "4"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/https-proxy-agent/node_modules/debug": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+            "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+            "license": "MIT",
+            "dependencies": {
+                "ms": "^2.1.3"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/https-proxy-agent/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/iconv-lite": {
+            "version": "0.4.24",
+            "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+            "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+            "license": "MIT",
+            "dependencies": {
+                "safer-buffer": ">= 2.1.2 < 3"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "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/inflight": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+            "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+            "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+            "license": "ISC",
+            "dependencies": {
+                "once": "^1.3.0",
+                "wrappy": "1"
+            }
+        },
+        "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/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-arrayish": {
+            "version": "0.3.2",
+            "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+            "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+            "license": "MIT"
+        },
+        "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-fullwidth-code-point": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "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-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/is-stream": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+            "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/isarray": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+            "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+            "license": "MIT"
+        },
+        "node_modules/js-sdsl": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
+            "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
+            "license": "MIT",
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/js-sdsl"
+            }
+        },
+        "node_modules/jsonwebtoken": {
+            "version": "9.0.2",
+            "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+            "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+            "license": "MIT",
+            "dependencies": {
+                "jws": "^3.2.2",
+                "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": "1.4.1",
+            "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+            "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+            "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": "3.2.2",
+            "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+            "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+            "license": "MIT",
+            "dependencies": {
+                "jwa": "^1.4.1",
+                "safe-buffer": "^5.0.1"
+            }
+        },
+        "node_modules/kuler": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
+            "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
+            "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/logform": {
+            "version": "2.7.0",
+            "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
+            "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@colors/colors": "1.6.0",
+                "@types/triple-beam": "^1.3.2",
+                "fecha": "^4.2.0",
+                "ms": "^2.1.1",
+                "safe-stable-stringify": "^2.3.1",
+                "triple-beam": "^1.3.0"
+            },
+            "engines": {
+                "node": ">= 12.0.0"
+            }
+        },
+        "node_modules/logform/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/long": {
+            "version": "5.3.1",
+            "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz",
+            "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==",
+            "license": "Apache-2.0"
+        },
+        "node_modules/lru-cache": {
+            "version": "10.4.3",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+            "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+            "license": "ISC"
+        },
+        "node_modules/lru.min": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
+            "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==",
+            "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-dir": {
+            "version": "3.1.0",
+            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+            "license": "MIT",
+            "dependencies": {
+                "semver": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/make-dir/node_modules/semver": {
+            "version": "6.3.1",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+            "license": "ISC",
+            "bin": {
+                "semver": "bin/semver.js"
+            }
+        },
+        "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": "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/merge-descriptors": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+            "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/methods": {
+            "version": "1.1.2",
+            "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+            "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/mime": {
+            "version": "1.6.0",
+            "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+            "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+            "license": "MIT",
+            "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/minimatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+            "license": "ISC",
+            "dependencies": {
+                "brace-expansion": "^1.1.7"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/minimist": {
+            "version": "1.2.8",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+            "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+            "license": "MIT",
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
+        "node_modules/minipass": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+            "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+            "license": "ISC",
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/minizlib": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+            "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+            "license": "MIT",
+            "dependencies": {
+                "minipass": "^3.0.0",
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/minizlib/node_modules/minipass": {
+            "version": "3.3.6",
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+            "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+            "license": "ISC",
+            "dependencies": {
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/mkdirp": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+            "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+            "license": "MIT",
+            "bin": {
+                "mkdirp": "bin/cmd.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/mqtt": {
+            "version": "5.10.4",
+            "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.4.tgz",
+            "integrity": "sha512-wN+SuhT2/ZaG6NPxca0N6YtRivnMxk6VflxQUEeqDH4erKdj+wPAGhHmcTLzvqfE4sJRxrEJ+XJxUc0No0E7eQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@types/readable-stream": "^4.0.18",
+                "@types/ws": "^8.5.14",
+                "commist": "^3.2.0",
+                "concat-stream": "^2.0.0",
+                "debug": "^4.4.0",
+                "help-me": "^5.0.0",
+                "lru-cache": "^10.4.3",
+                "minimist": "^1.2.8",
+                "mqtt-packet": "^9.0.1",
+                "number-allocator": "^1.0.14",
+                "readable-stream": "^4.7.0",
+                "reinterval": "^1.1.0",
+                "rfdc": "^1.4.1",
+                "split2": "^4.2.0",
+                "worker-timers": "^7.1.8",
+                "ws": "^8.18.0"
+            },
+            "bin": {
+                "mqtt": "build/bin/mqtt.js",
+                "mqtt_pub": "build/bin/pub.js",
+                "mqtt_sub": "build/bin/sub.js"
+            },
+            "engines": {
+                "node": ">=16.0.0"
+            }
+        },
+        "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.0",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+            "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+            "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/mqtt/node_modules/debug": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+            "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+            "license": "MIT",
+            "dependencies": {
+                "ms": "^2.1.3"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/mqtt/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": "1.4.5-lts.1",
+            "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
+            "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
+            "license": "MIT",
+            "dependencies": {
+                "append-field": "^1.0.0",
+                "busboy": "^1.0.0",
+                "concat-stream": "^1.5.2",
+                "mkdirp": "^0.5.4",
+                "object-assign": "^4.1.1",
+                "type-is": "^1.6.4",
+                "xtend": "^4.0.0"
+            },
+            "engines": {
+                "node": ">= 6.0.0"
+            }
+        },
+        "node_modules/multer/node_modules/concat-stream": {
+            "version": "1.6.2",
+            "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+            "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+            "engines": [
+                "node >= 0.8"
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "buffer-from": "^1.0.0",
+                "inherits": "^2.0.3",
+                "readable-stream": "^2.2.2",
+                "typedarray": "^0.0.6"
+            }
+        },
+        "node_modules/multer/node_modules/mkdirp": {
+            "version": "0.5.6",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+            "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+            "license": "MIT",
+            "dependencies": {
+                "minimist": "^1.2.6"
+            },
+            "bin": {
+                "mkdirp": "bin/cmd.js"
+            }
+        },
+        "node_modules/multer/node_modules/readable-stream": {
+            "version": "2.3.8",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+            "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+            "license": "MIT",
+            "dependencies": {
+                "core-util-is": "~1.0.0",
+                "inherits": "~2.0.3",
+                "isarray": "~1.0.0",
+                "process-nextick-args": "~2.0.0",
+                "safe-buffer": "~5.1.1",
+                "string_decoder": "~1.1.1",
+                "util-deprecate": "~1.0.1"
+            }
+        },
+        "node_modules/multer/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/multer/node_modules/string_decoder": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+            "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+            "license": "MIT",
+            "dependencies": {
+                "safe-buffer": "~5.1.0"
+            }
+        },
+        "node_modules/mysql2": {
+            "version": "3.13.0",
+            "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz",
+            "integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==",
+            "license": "MIT",
+            "dependencies": {
+                "aws-ssl-profiles": "^1.1.1",
+                "denque": "^2.1.0",
+                "generate-function": "^2.3.1",
+                "iconv-lite": "^0.6.3",
+                "long": "^5.2.1",
+                "lru.min": "^1.0.0",
+                "named-placeholders": "^1.1.3",
+                "seq-queue": "^0.0.5",
+                "sqlstring": "^2.3.2"
+            },
+            "engines": {
+                "node": ">= 8.0"
+            }
+        },
+        "node_modules/mysql2/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==",
+            "license": "MIT",
+            "dependencies": {
+                "safer-buffer": ">= 2.1.2 < 3.0.0"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/named-placeholders": {
+            "version": "1.1.3",
+            "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
+            "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
+            "license": "MIT",
+            "dependencies": {
+                "lru-cache": "^7.14.1"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            }
+        },
+        "node_modules/named-placeholders/node_modules/lru-cache": {
+            "version": "7.18.3",
+            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+            "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+            "license": "ISC",
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "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/node-addon-api": {
+            "version": "5.1.0",
+            "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
+            "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
+            "license": "MIT"
+        },
+        "node_modules/node-fetch": {
+            "version": "2.7.0",
+            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+            "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+            "license": "MIT",
+            "dependencies": {
+                "whatwg-url": "^5.0.0"
+            },
+            "engines": {
+                "node": "4.x || >=6.0.0"
+            },
+            "peerDependencies": {
+                "encoding": "^0.1.0"
+            },
+            "peerDependenciesMeta": {
+                "encoding": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/nodemon": {
+            "version": "3.1.9",
+            "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
+            "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "chokidar": "^3.5.2",
+                "debug": "^4",
+                "ignore-by-default": "^1.0.1",
+                "minimatch": "^3.1.2",
+                "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.0",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+            "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+            "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/nopt": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+            "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+            "license": "ISC",
+            "dependencies": {
+                "abbrev": "1"
+            },
+            "bin": {
+                "nopt": "bin/nopt.js"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "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/npmlog": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+            "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+            "deprecated": "This package is no longer supported.",
+            "license": "ISC",
+            "dependencies": {
+                "are-we-there-yet": "^2.0.0",
+                "console-control-strings": "^1.1.0",
+                "gauge": "^3.0.0",
+                "set-blocking": "^2.0.0"
+            }
+        },
+        "node_modules/number-allocator": {
+            "version": "1.0.14",
+            "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
+            "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
+            "license": "MIT",
+            "dependencies": {
+                "debug": "^4.3.1",
+                "js-sdsl": "4.3.0"
+            }
+        },
+        "node_modules/number-allocator/node_modules/debug": {
+            "version": "4.4.0",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+            "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+            "license": "MIT",
+            "dependencies": {
+                "ms": "^2.1.3"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/number-allocator/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/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/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/one-time": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
+            "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
+            "license": "MIT",
+            "dependencies": {
+                "fn.name": "1.x.x"
+            }
+        },
+        "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-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/path-to-regexp": {
+            "version": "0.1.12",
+            "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+            "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+            "license": "MIT"
+        },
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "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/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/qs": {
+            "version": "6.13.0",
+            "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+            "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+            "license": "BSD-3-Clause",
+            "dependencies": {
+                "side-channel": "^1.0.6"
+            },
+            "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": "2.5.2",
+            "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+            "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+            "license": "MIT",
+            "dependencies": {
+                "bytes": "3.1.2",
+                "http-errors": "2.0.0",
+                "iconv-lite": "0.4.24",
+                "unpipe": "1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "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/regenerator-runtime": {
+            "version": "0.14.1",
+            "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+            "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+            "license": "MIT"
+        },
+        "node_modules/reinterval": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz",
+            "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==",
+            "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/rimraf": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+            "deprecated": "Rimraf versions prior to v4 are no longer supported",
+            "license": "ISC",
+            "dependencies": {
+                "glob": "^7.1.3"
+            },
+            "bin": {
+                "rimraf": "bin.js"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "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/safe-stable-stringify": {
+            "version": "2.5.0",
+            "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+            "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "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.1",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+            "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+            "license": "ISC",
+            "bin": {
+                "semver": "bin/semver.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/send": {
+            "version": "0.19.0",
+            "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+            "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+            "license": "MIT",
+            "dependencies": {
+                "debug": "2.6.9",
+                "depd": "2.0.0",
+                "destroy": "1.2.0",
+                "encodeurl": "~1.0.2",
+                "escape-html": "~1.0.3",
+                "etag": "~1.8.1",
+                "fresh": "0.5.2",
+                "http-errors": "2.0.0",
+                "mime": "1.6.0",
+                "ms": "2.1.3",
+                "on-finished": "2.4.1",
+                "range-parser": "~1.2.1",
+                "statuses": "2.0.1"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/send/node_modules/encodeurl": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+            "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.8"
+            }
+        },
+        "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/seq-queue": {
+            "version": "0.0.5",
+            "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+            "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+        },
+        "node_modules/serve-static": {
+            "version": "1.16.2",
+            "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+            "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+            "license": "MIT",
+            "dependencies": {
+                "encodeurl": "~2.0.0",
+                "escape-html": "~1.0.3",
+                "parseurl": "~1.3.3",
+                "send": "0.19.0"
+            },
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
+        "node_modules/set-blocking": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+            "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+            "license": "ISC"
+        },
+        "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/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.0",
+            "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+            "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+            "license": "MIT",
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "object-inspect": "^1.13.3"
+            },
+            "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/signal-exit": {
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+            "license": "ISC"
+        },
+        "node_modules/simple-swizzle": {
+            "version": "0.2.2",
+            "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+            "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+            "license": "MIT",
+            "dependencies": {
+                "is-arrayish": "^0.3.1"
+            }
+        },
+        "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/split2": {
+            "version": "4.2.0",
+            "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+            "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+            "license": "ISC",
+            "engines": {
+                "node": ">= 10.x"
+            }
+        },
+        "node_modules/sqlstring": {
+            "version": "2.3.3",
+            "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+            "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.6"
+            }
+        },
+        "node_modules/stack-trace": {
+            "version": "0.0.10",
+            "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+            "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
+            "license": "MIT",
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/statuses": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+            "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+            "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/string-width": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+            "license": "MIT",
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "license": "MIT",
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "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/tar": {
+            "version": "6.2.1",
+            "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+            "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+            "license": "ISC",
+            "dependencies": {
+                "chownr": "^2.0.0",
+                "fs-minipass": "^2.0.0",
+                "minipass": "^5.0.0",
+                "minizlib": "^2.1.1",
+                "mkdirp": "^1.0.3",
+                "yallist": "^4.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/text-hex": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+            "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
+            "license": "MIT"
+        },
+        "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/tr46": {
+            "version": "0.0.3",
+            "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+            "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+            "license": "MIT"
+        },
+        "node_modules/triple-beam": {
+            "version": "1.4.1",
+            "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
+            "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 14.0.0"
+            }
+        },
+        "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": "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/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/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": "6.20.0",
+            "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+            "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+            "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/utils-merge": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+            "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 0.4.0"
+            }
+        },
+        "node_modules/uuid": {
+            "version": "11.1.0",
+            "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+            "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+            "funding": [
+                "https://github.com/sponsors/broofa",
+                "https://github.com/sponsors/ctavan"
+            ],
+            "license": "MIT",
+            "bin": {
+                "uuid": "dist/esm/bin/uuid"
+            }
+        },
+        "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/webidl-conversions": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+            "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+            "license": "BSD-2-Clause"
+        },
+        "node_modules/whatwg-url": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+            "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+            "license": "MIT",
+            "dependencies": {
+                "tr46": "~0.0.3",
+                "webidl-conversions": "^3.0.0"
+            }
+        },
+        "node_modules/wide-align": {
+            "version": "1.1.5",
+            "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+            "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+            "license": "ISC",
+            "dependencies": {
+                "string-width": "^1.0.2 || 2 || 3 || 4"
+            }
+        },
+        "node_modules/winston": {
+            "version": "3.17.0",
+            "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
+            "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
+            "license": "MIT",
+            "dependencies": {
+                "@colors/colors": "^1.6.0",
+                "@dabh/diagnostics": "^2.0.2",
+                "async": "^3.2.3",
+                "is-stream": "^2.0.0",
+                "logform": "^2.7.0",
+                "one-time": "^1.0.0",
+                "readable-stream": "^3.4.0",
+                "safe-stable-stringify": "^2.3.1",
+                "stack-trace": "0.0.x",
+                "triple-beam": "^1.3.0",
+                "winston-transport": "^4.9.0"
+            },
+            "engines": {
+                "node": ">= 12.0.0"
+            }
+        },
+        "node_modules/winston-transport": {
+            "version": "4.9.0",
+            "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
+            "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
+            "license": "MIT",
+            "dependencies": {
+                "logform": "^2.7.0",
+                "readable-stream": "^3.6.2",
+                "triple-beam": "^1.3.0"
+            },
+            "engines": {
+                "node": ">= 12.0.0"
+            }
+        },
+        "node_modules/winston-transport/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/winston/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/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/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/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/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/ws": {
+            "version": "8.18.1",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
+            "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+            "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/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/yallist": {
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+            "license": "ISC"
+        }
+    }
+}

+ 37 - 0
package.json

@@ -0,0 +1,37 @@
+{
+    "name": "vueiot-api",
+    "version": "1.0.0",
+    "description": "A Vue IoT API project",
+    "main": "app.js",
+    "scripts": {
+        "start": "node app.js",
+        "dev": "nodemon app.js",
+        "test": "echo \"Error: no test specified\" && exit 1"
+    },
+    "dependencies": {
+        "bcrypt": "^5.1.1",
+        "body-parser": "^1.20.2",
+        "cors": "^2.8.5",
+        "crypto-js": "^4.2.0",
+        "dayjs": "^1.11.13",
+        "express": "^4.18.2",
+        "jsonwebtoken": "^9.0.2",
+        "mqtt": "^5.10.3",
+        "multer": "^1.4.5-lts.1",
+        "mysql2": "^3.6.0",
+        "uuid": "^11.1.0",
+        "winston": "^3.11.0"
+    },
+    "devDependencies": {
+        "nodemon": "^3.0.2"
+    },
+    "engines": {
+        "node": ">=16.0.0"
+    },
+    "repository": {
+        "type": "git",
+        "url": "https://git.caner.top/yangfei/vueiot-api.git"
+    },
+    "author": "Your Name",
+    "license": "MIT"
+}

+ 158 - 0
routes.js

@@ -0,0 +1,158 @@
+const express = require('express');
+const router = express.Router(); // 修复多余的分号
+const roomController = require('./controllers/roomController');
+const deviceController = require('./controllers/deviceController');
+const relayController = require('./controllers/relayController');
+const authController = require('./controllers/authController');
+const otaController = require('./controllers/otaController');
+const logger = require('./logger');
+const heaterUsageController = require('./controllers/heaterUsageController');
+
+// 中间件:记录所有API请求
+router.use((req, res, next) => {
+    logger.info(`收到 ${req.method} 请求: ${req.originalUrl}`);
+    logger.debug('请求体:', req.body);
+    next();
+});
+
+// 注册接口
+router.post('/api/register', (req, res, next) => {
+    logger.info(`新用户注册请求: ${req.body.username}`);
+    next();
+}, authController.register);
+
+// 登录接口
+router.post('/api/login', (req, res, next) => {
+    logger.info(`用户登录请求: ${req.body.username}`);
+    next();
+}, authController.login);
+
+// 房间相关路由
+router.get('/api/rooms', (req, res, next) => {
+    logger.info('获取所有房间列表请求');
+    next();
+}, roomController.getRooms);
+
+router.post('/api/rooms', (req, res, next) => {
+    logger.info(`创建新房间请求: ${req.body.room_name}`);
+    next();
+}, roomController.addRoom);
+
+router.put('/api/rooms/:id', (req, res, next) => {
+    logger.info(`更新房间信息请求, ID: ${req.params.id}`);
+    next();
+}, roomController.updateRoom);
+
+router.delete('/api/rooms/:id', (req, res, next) => {
+    logger.info(`删除房间请求, ID: ${req.params.id}`);
+    next();
+}, roomController.deleteRoom);
+
+// 设备相关路由
+router.get('/api/devices', (req, res, next) => {
+    logger.info('获取所有设备列表请求');
+    next();
+}, deviceController.getDevices);
+
+// 删除重复的路由定义,只保留这个 ▼▼▼
+router.get('/firmware-versions', otaController.getFirmwareVersions);
+
+// 修改为正确的控制器方法 ▼▼▼
+router.get('/firmware-versions', otaController.getFirmwareVersions || ((req, res) => {
+    res.status(501).json({ code: 501, message: '功能暂未实现' });
+}));
+
+// 新增:获取所有房间及其设备信息
+router.get('/api/all-rooms-with-devices', (req, res, next) => {
+    logger.info('获取所有房间及其设备信息请求');
+    next();
+}, deviceController.getAllRoomsWithDevices);
+
+router.post('/api/devices/bind', (req, res, next) => {
+    logger.info(`绑定设备请求: 设备ID=${req.body.deviceId}, 房间ID=${req.body.roomId}`);
+    next();
+}, deviceController.bindDeviceToRoom);
+
+router.post('/api/devices/unbind/:deviceId', (req, res, next) => {
+    logger.info(`解绑设备请求: ${req.params.deviceId}`);
+    next();
+}, deviceController.unbindDevice);
+
+router.delete('/api/devices/:deviceId', (req, res, next) => {
+    logger.info(`删除设备请求: ${req.params.deviceId}`);
+    next();
+}, deviceController.deleteDevice);
+
+router.post('/api/devices/update', (req, res, next) => {
+    logger.info(`更新设备信息请求: ${req.body.deviceId}`);
+    next();
+}, deviceController.updateDevice);
+
+router.get('/api/device-status', (req, res, next) => {
+    logger.info('获取设备状态请求');
+    next();
+}, deviceController.getDeviceStatus);
+
+router.get('/api/device-status-by-room', (req, res, next) => {
+    logger.info(`获取房间设备状态请求, 房间ID: ${req.query.roomId}`);
+    next();
+}, deviceController.getDeviceStatusByRoom);
+
+router.get('/api/devices-by-room', (req, res, next) => {
+    logger.info(`获取房间设备列表请求, 房间ID: ${req.query.roomId}`);
+    next();
+}, deviceController.getDevicesByRoom);
+
+router.get('/api/gpio-state', (req, res, next) => {
+    logger.info(`获取GPIO状态请求, 设备ID: ${req.query.deviceId}`);
+    next();
+}, deviceController.getGpioState);
+
+// 继电器控制路由
+router.get('/relay/:state', (req, res, next) => {
+    logger.info(`继电器控制请求: 状态=${req.params.state}, 房间ID=${req.query.roomId}`);
+    next();
+}, relayController.toggleRelay);
+
+router.post('/send-relay-id-to-sensor/:roomId', (req, res, next) => {
+    logger.info(`发送继电器ID到传感器请求, 房间ID: ${req.params.roomId}`);
+    next();
+}, relayController.sendRelayIdToSensor);
+
+// 绑定继电器到人体传感器
+router.post('/api/devices/bind-relay-to-sensor', (req, res, next) => {
+    logger.info(`绑定继电器到传感器请求: 传感器ID=${req.body.sensorId}, 房间ID=${req.body.roomId}`);
+    next();
+}, deviceController.bindRelayToSensor);
+
+// 检查更新
+// 定义OTA路由
+// 取消注释并更新OTA路由
+router.post('/check-updates', otaController.checkUpdates);
+router.get('/download-firmware', otaController.downloadFirmware);
+router.post('/start-upgrade', otaController.startUpgrade);
+
+// 新增固件上传接口
+router.post('/upload-firmware', 
+    otaController.uploadFirmware.single('firmware'),
+    otaController.handleFirmwareUpload
+);
+
+// 错误处理中间件
+router.use((err, req, res, next) => {
+    logger.error('路由错误:', err);
+    res.status(500).json({ 
+        success: false, 
+        message: '服务器内部错误' 
+    });
+});
+
+router.get('/api/heater-usage', (req, res, next) => {
+    logger.info('获取电暖器使用数据请求');
+    next();
+}, heaterUsageController.getUsageData);
+
+// 在路由文件中添加(约130行附近)
+router.post('/check-device-update', otaController.checkDeviceUpdate);
+
+module.exports = router;

+ 116 - 0
services/HeaterUsageService.js

@@ -0,0 +1,116 @@
+const db = require('../config/db');
+const logger = require('../logger');
+// 移除这一行重复的引用声明
+// -const HeaterUsageService = require('../services/HeaterUsageService');
+
+// 保留正确的类定义
+class HeaterUsageService {
+    // 在handleHeaterUsage方法中添加数据验证
+    static async handleHeaterUsage(device_id, status, temperature, timestamp) {
+        try {
+            if (!device_id || !timestamp) {
+                throw new Error('设备ID和时间戳不能为空');
+            }
+            // TODO 0: 总体逻辑:1. 当当前设备状态为开并且温度大于12,开始插入数据库并记录开始时间 2. 当当前状态关闭时并且查询到有大于12的记录,插入结束时间并计算时长
+            // TODO 举例:
+            // if (status === 'on' && temperature > 12) {
+            //     // 开始插入开始时间
+            //     const sql = `INSERT INTO heater_usage (device_id, start_time, end_time, duration) VALUES (?, ?, NULL, 0)`;
+            //     db.query(sql, [device_id, startTime]);
+            // }else{
+            //     // 查询是否有当前设置大于12的数据:其实就是最近开始时间的一条
+            //     const sql1 = `SELECT * FROM heater_usage WHERE device_id = ? ORDER BY start_time DESC LIMIT 1`;
+            //     const [results] =await db.query(sql1, [device_id]);
+            //     // 如果有就计算
+            //     if(results.length){
+            //         // 在当前结果中,插入结束时间并计算时长,结束时间就取当前结束的时间
+            //         // TODO 5 这里应该是修改当前查询到的信息,奈何sql不精,哈哈哈,下面的仅做参考
+            //         const end_time = Date.now();
+            //         const sql = `UPDATE heater_usage 
+            //                     SET end_time = ?, 
+            //                         duration = TIMESTAMPDIFF(SECOND, start_time, ?) 
+            //                     WHERE device_id = ? AND end_time IS NULL`;
+            //         db.query(sql, [end_time, end_time, device_id]);
+            //     }
+            // }
+
+
+            // old
+            logger.info(`处理电暖器使用事件: 设备ID=${device_id}, 状态=${status}, 温度=${temperature}`);
+            //TODO 1: temperature 类型要确定是数字类型,这个判断 记录 和更新只需要执行一个?下面又对
+            if (status === 'on' && temperature > 12) {
+                const startTime = new Date(timestamp);
+                logger.debug(`开始记录使用时间: ${startTime}`);
+                
+                const lastRecord = await this.getLastHeaterUsage(device_id);
+                // TODO 2: 在 3 会返回ture,你这个判断永远不会执行,只有表为空的时候才会插入一条
+                if (!lastRecord || lastRecord.end_time) {
+                    logger.info(`插入新的电暖器使用记录: 设备ID=${device_id}`);
+                    await this.insertHeaterUsage(device_id, startTime);
+                }
+            } else {
+                logger.info(`更新电暖器使用结束时间: 设备ID=${device_id}`);
+                await this.updateHeaterUsageEndTime(device_id, timestamp);
+            }
+        } catch (error) {
+            logger.error(`处理电暖器使用事件失败: ${error.message}`);
+            throw error;
+        }
+    }
+
+    // 在getLastHeaterUsage方法中添加更多调试信息
+    // TODO 3:排序查询从大到小 取第一个?如果有记录会永远返回
+    static async getLastHeaterUsage(device_id) {
+        logger.debug(`查询设备最后使用记录: 设备ID=${device_id}`);
+        try {
+            const sql = `SELECT * FROM heater_usage WHERE device_id = ? ORDER BY end_time DESC LIMIT 1`;
+            const [results] = await db.query(sql, [device_id]);
+            logger.debug(`查询结果: ${JSON.stringify(results)}`);
+            return results[0] || null;
+        } catch (error) {
+            logger.error(`查询失败: ${error.message}`);
+            throw error;
+        }
+    }
+
+    // TODO 4:插入数据没更新end_time,,一直为null 上面 3 查询就会一直是第一条,没排序
+    static async insertHeaterUsage(device_id, startTime) {
+        // 插入新记录
+        const sql = `INSERT INTO heater_usage (device_id, start_time, end_time, duration, date) VALUES (?, ?, NULL, 0, ?)`;
+        const date = this.getUsageDate(startTime);
+        try {
+            const [results] = await db.query(sql, [device_id, startTime, date]);
+            return results;
+        } catch (error) {
+            logger.error(`插入记录失败: ${error.message}`);
+            throw error;
+        }
+    }
+
+    static async updateHeaterUsageEndTime(device_id, endTime) {
+        // 更新结束时间并计算时长
+        const sql = `UPDATE heater_usage 
+                     SET end_time = ?, 
+                         duration = TIMESTAMPDIFF(SECOND, start_time, ?) 
+                     WHERE device_id = ? AND end_time IS NULL`;
+        try {
+            const [results] = await db.query(sql, [endTime, endTime, device_id]);
+            return results;
+        } catch (error) {
+            logger.error(`更新结束时间失败: ${error.message}`);
+            throw error;
+        }
+    }
+
+    // 修复点2:移除类定义外的多余代码
+    static getUsageDate(timestamp) {
+        // 获取统计日期(以中午12点为分界)
+        const date = new Date(timestamp);
+        if (date.getHours() < 12) {
+            date.setDate(date.getDate() - 1); //TODO 啥意思?减1毫秒?
+        }
+        return date.toISOString().split('T')[0]; //TODO 只返回年月日 ?估计这一步是更新end_time?检查 4 sql参数
+    }
+}
+
+module.exports = HeaterUsageService;

+ 27 - 0
webhook/AuthzCheck.js

@@ -0,0 +1,27 @@
+const db = require('../config/db');
+const logger = require('../logger');
+
+class AuthzCheck {
+    // 插入授权检查结果
+    static insert(authzData, callback) {
+        const { client_id, topic, action, result, timestamp } = authzData;
+        const sql = `
+            INSERT INTO authz_checks (client_id, topic, action, result, timestamp)
+            VALUES (?, ?, ?, ?, ?)
+        `;
+        
+        logger.info(`记录授权检查: 客户端=${client_id}, 主题=${topic}, 操作=${action}`);
+        logger.debug('执行 SQL:', sql, [client_id, topic, action, result, timestamp]);
+        
+        db.query(sql, [client_id, topic, action, result, timestamp], (err, result) => {
+            if (err) {
+                logger.error('记录授权检查失败:', err);
+                return callback(err);
+            }
+            logger.info('授权检查记录成功');
+            callback(null, result);
+        });
+    }
+}
+
+module.exports = AuthzCheck;

+ 209 - 0
webhook/Device.js

@@ -0,0 +1,209 @@
+const db = require('../config/db');
+const logger = require('../logger');
+
+class Device {
+    // 插入新设备
+    static insert(device_id, status, last_seen, first_online_time, last_online_time, ip_address, callback) {
+        const sql = `
+            INSERT INTO devices (device_id, status, last_seen, first_online_time, last_online_time, ip_address)
+            VALUES (?, ?, ?, ?, ?, ?)
+        `;
+        logger.info(`插入新设备: ${device_id}`);
+        const devicesSql = `/* 原有SQL语句保持不变 */`;
+        
+        db.query(devicesSql, [device_id, status, last_seen, first_online_time, last_online_time, ip_address], (err, result) => {
+            if (err) {
+                logger.error('插入新设备失败:', err);
+                return callback(err);
+            }
+            
+            // 新增relay_state表插入(初始化默认房间信息)
+            const relaySql = `
+                INSERT INTO relay_state 
+                (device_id, state, temperature, timestamp, room_number, room_name)
+                VALUES (?, 'off', NULL, ?, '默认房间号', '默认房间')
+            `;
+            db.query(relaySql, [device_id, last_seen], (relayErr) => {
+                if (relayErr) logger.error('初始化继电器状态失败:', relayErr);
+            });
+
+            callback(null, result);
+        });
+    }
+
+    // 更新设备上线状态
+    static updateOnlineStatus(device_id, status, last_seen, first_online_time, last_online_time, ip_address, callback) {
+        logger.info(`更新设备 ${device_id} 在线状态`);
+        const sql = `
+            UPDATE devices
+            SET 
+                status = ?,
+                last_seen = ?,
+                first_online_time = ?,
+                last_online_time = ?,
+                ip_address = ?
+            WHERE device_id = ?
+        `;
+
+        db.query(sql, [status, last_seen, first_online_time, last_online_time, ip_address, device_id], callback);
+    }
+
+    // 更新设备下线状态
+    static updateOfflineStatus(device_id, status, last_seen, last_offline_time, callback) {
+        const sql = `
+            UPDATE devices
+            SET 
+                status = ?,
+                last_seen = ?,
+                last_offline_time = ?
+            WHERE device_id = ?
+        `;
+
+        db.query(sql, [status, last_seen, last_offline_time, device_id], callback);
+    }
+    // 修改updateTemperature方法参数处理
+    static updateTemperature(device_id, temperature, upload_time, room_number, room_name, callback = () => {}) {
+        logger.info(`更新设备温度: 设备ID=${device_id}, 温度=${temperature}`);
+        const sql = `
+            UPDATE devices
+            SET temperature = ?, upload_time = ?
+            WHERE device_id = ?
+        `;
+        db.query(sql, [temperature, upload_time, device_id], (err, results) => {
+            if (err) {
+                logger.error(`更新设备温度失败: 设备ID=${device_id}, 错误: ${err}`);
+                return callback(err);
+            }
+            
+            // 新增relay_state插入(添加默认回调)
+            const relaySql = `
+                INSERT INTO relay_state 
+                (device_id, temperature, timestamp, room_number, room_name)
+                VALUES (?, ?, ?, ?, ?)
+            `;
+            db.query(relaySql, [device_id, temperature, upload_time, room_number, room_name], (relayErr) => {
+                if (relayErr) {
+                    logger.error(`温度记录失败: ${device_id}`);
+                }
+                callback(null, results); // 确保在此处调用回调
+            });
+        });
+    }
+    // 只更新开关状态和开关状态时间
+    static updateSwitchStatus(device_id, switch_status, switch_status_time, callback) {
+        logger.info(`更新设备开关状态: 设备ID=${device_id}, 状态=${switch_status}`);
+        const sql = `
+            UPDATE devices
+            SET switch_status = ?, switch_status_time = ?
+            WHERE device_id = ?
+        `;
+        db.query(sql, [switch_status, switch_status_time, device_id], (err, results) => {
+            if (err) {
+                logger.error(`更新设备开关状态失败: 设备ID=${device_id}, 错误: ${err}`);
+                return callback(err);
+            }
+            
+            // 新增relay_state更新
+            const relaySql = `
+                UPDATE relay_state 
+                SET switch_status = ?, update_time = ?
+                WHERE device_id = ?
+            `;
+            db.query(relaySql, [switch_status, switch_status_time, device_id], (relayErr) => {
+                if (relayErr) {
+                    logger.error(`更新继电器状态失败: ${device_id}`);
+                }
+            });
+
+            logger.debug(`设备开关状态更新成功: 设备ID=${device_id}`);
+            callback(null, results);
+        });
+    }
+    static updateTemperature(device_id, temperature, upload_time, callback) {
+        logger.info(`更新设备温度: 设备ID=${device_id}, 温度=${temperature}`);
+        const sql = `
+            UPDATE devices
+            SET temperature = ?, upload_time = ?
+            WHERE device_id = ?
+        `;
+        db.query(sql, [temperature, upload_time, device_id], (err, results) => {
+            if (err) {
+                logger.error(`更新设备温度失败: 设备ID=${device_id}, 错误: ${err}`);
+                return callback(err);
+            }
+            
+            // 新增relay_state更新
+            const relaySql = `
+                UPDATE relay_state 
+                SET temperature = ?, update_time = ?
+                WHERE device_id = ?
+            `;
+            db.query(relaySql, [temperature, upload_time, device_id], (relayErr) => {
+                if (relayErr) {
+                    logger.error(`更新温度记录失败: ${device_id}`);
+                }
+            });
+
+            logger.debug(`设备温度更新成功: 设备ID=${device_id}`);
+            callback(null, results);
+        });
+    }
+    // 只更新高低电平状态和高低电平状态时间
+    static updateLevelStatus(device_id, level_status, level_status_time, callback) {
+        const sql = `
+            UPDATE devices
+            SET level_status = ?, level_status_time = ?
+            WHERE device_id = ?
+        `;
+        db.query(sql, [level_status, level_status_time, device_id], callback);
+    }
+    // 例如 getDeviceById 方法
+    static getDeviceById(device_id, callback) {
+      const sql = `
+        SELECT * FROM devices
+        WHERE device_id = ?
+      `;
+      db.query(sql, [device_id], (err, results) => {
+        if (err) {
+          console.error('Error fetching device:', err);
+          return callback(err);
+        }
+        const device = results.length > 0? results[0] : null;
+        callback(null, device);
+      });
+    }
+    // 建议添加数据库连接检查
+    static getDeviceById(device_id, callback) {
+      if (!db) {
+        console.error('Database connection is not initialized');
+        return callback(new Error('Database connection is not initialized'));
+      }
+      const sql = `
+        SELECT * FROM devices
+        WHERE device_id = ?
+      `;
+      db.query(sql, [device_id], (err, results) => {
+        if (err) {
+          console.error('Error fetching device:', err);
+          return callback(err);
+        }
+        const device = results.length > 0? results[0] : null;
+        callback(null, device);
+      });
+    }
+    static async getDeviceStatus(device_id) {
+        return new Promise((resolve, reject) => {
+            this.getDeviceById(device_id, (err, device) => {
+                if (err) return reject(err);
+                resolve({
+                    status: device?.status || 'offline',
+                    temperature: device?.temperature || 0,
+                    switch_status: device?.switch_status || 'off'
+                });
+            });
+        });
+    }
+
+}
+
+module.exports = Device;

+ 19 - 0
webhook/RelayState.js

@@ -0,0 +1,19 @@
+const db = require('../config/db');
+const logger = require('../logger');
+
+class RelayState {
+    static updateRoomInfo(device_id, room_number, room_name) {
+        const sql = `
+            UPDATE relay_state 
+            SET room_number = ?, room_name = ?
+            WHERE device_id = ?
+        `;
+        db.query(sql, [room_number, room_name, device_id], (err) => {
+            if (err) {
+                logger.error(`更新房间信息失败: ${device_id}`);
+            }
+        });
+    }
+}
+
+module.exports = RelayState;

+ 96 - 0
webhook/webhook.js

@@ -0,0 +1,96 @@
+const express = require('express');
+const WebhookService = require('../webhook/webhookService');
+const logger = require('../logger');
+
+const router = express.Router();
+
+// Webhook 路由
+router.post('/webhook', (req, res) => {
+    const eventData = req.body;
+    const eventType = eventData.event;
+
+    logger.info(`收到 Webhook 事件: ${eventType}`);
+    logger.debug('事件数据:', JSON.stringify(eventData, null, 2));
+
+    // 根据事件类型处理不同的事件
+    switch (eventType) {
+        case 'client.connected':
+        case 'client.disconnected':
+            logger.info(`处理设备${eventType}事件`);
+            WebhookService.handleDeviceEvent(eventData);
+            break;
+
+        case 'switch_status_update':
+            logger.info(`处理开关状态更新: 设备=${eventData.clientid}`);
+            WebhookService.handleSwitchEvent(eventData);
+            break;
+
+        case 'client.check_authn_complete':
+            logger.info(`处理认证检查完成事件: 客户端=${eventData.clientid}`);
+            logger.debug('认证结果:', eventData.result);
+            break;
+
+        case 'session.subscribed':
+            logger.info(`处理会话订阅事件: 客户端=${eventData.clientid}, 主题=${eventData.topic}`);
+            break;
+
+        case 'message.delivered':
+            logger.info(`处理消息投递事件: 客户端=${eventData.clientid}, 主题=${eventData.topic}`);
+            WebhookService.handleMessageDeliveredEvent(eventData);
+            break;
+
+        case 'message.publish':
+            logger.info(`处理消息发布事件: 主题=${eventData.topic}`);
+            // 处理不同类型的消息主题
+            if (eventData.topic.includes('/temperature')) {
+                logger.info('处理温度数据事件');
+                WebhookService.handleTemperatureEvent(eventData);
+            }
+            // 如果主题以 device/ 开头并以 /relay/state 结尾,处理继电器状态事件
+            else if (eventData.topic.startsWith('device/') && eventData.topic.endsWith('/relay/state')) {
+                logger.info('处理继电器状态事件');
+                WebhookService.handleRelayStateEvent(eventData);
+            }
+            // 如果主题包含 GPIO,处理 GPIO 状态事件
+            else if (eventData.topic.startsWith('device/') && eventData.topic.includes('/gpio')) {
+                logger.info('处理GPIO状态事件');
+                WebhookService.handleGpioStateEvent(eventData);
+            } else {
+                logger.warn(`未处理的消息主题: ${eventData.topic}`);
+            }
+            break;
+
+        case 'client.check_authz_complete':
+            logger.info(`处理授权检查完成事件: 客户端=${eventData.clientid}`);
+            WebhookService.handleAuthzCheckEvent(eventData);
+            break;
+
+        case 'client.connack':
+            logger.info(`处理连接确认事件: 客户端=${eventData.clientid}`);
+            logger.debug('连接详情:', {
+                clean_start: eventData.clean_start,
+                keepalive: eventData.keepalive,
+                proto_ver: eventData.proto_ver
+            });
+            break;
+
+        case 'message.dropped':
+            logger.warn('消息丢弃事件:', {
+                client_id: eventData.clientid || eventData.client_id || 'unknown',
+                topic: eventData.topic,
+                payload: eventData.payload,
+                reason: eventData.reason,
+                timestamp: WebhookService.convertToBeijingTime(eventData.timestamp)
+            });
+            break;
+
+        default:
+            logger.warn(`未处理的事件类型: ${eventType}`);
+    }
+
+    // 返回成功响应
+    logger.debug('Webhook 处理完成');
+    res.status(200).json({ status: 'success' });
+});
+
+module.exports = router;

+ 275 - 0
webhook/webhookService.js

@@ -0,0 +1,275 @@
+const AuthzCheck = require('./AuthzCheck');
+const Device = require('./Device');
+const logger = require('../logger'); // 引入日志记录器
+const HeaterUsageService = require('../services/HeaterUsageService');
+const RelayState = require('./RelayState'); // 需要新建RelayState模型
+
+class WebhookService {
+    // 将 UTC 时间转换为北京时间(UTC+8)
+    static convertToBeijingTime(utcTimestamp) {
+        const date = new Date(utcTimestamp);
+        date.setHours(date.getHours() + 8); // 转换为北京时间
+        return date.toISOString().slice(0, 19).replace('T', ' ');
+    }
+
+    // 处理设备上下线事件
+    static handleDeviceEvent(eventData) {
+        const client_id = eventData.clientid || undefined;
+        const status = eventData.event === 'client.connected' ? 'online' : 'offline';
+        const timestamp = WebhookService.convertToBeijingTime(eventData.timestamp);
+        const ip_address = eventData.peername || 'unknown:0';
+
+        logger.info(`处理设备${status}事件: ${client_id}`);
+        logger.debug('事件数据:', { client_id, status, timestamp, ip_address });
+
+        // 查询设备的当前状态
+        Device.getDeviceById(client_id, (err, device) => {
+            if (err) {
+                logger.error('查询设备失败:', err);
+                return;
+            }
+
+            // 如果设备不存在,插入新设备
+            if (!device) {
+                const first_online_time = status === 'online' ? timestamp : null;
+                logger.info(`新设备首次上线: ${client_id}`);
+
+                Device.insert(
+                    client_id,
+                    status,
+                    timestamp,
+                    first_online_time,
+                    status === 'online' ? timestamp : null,
+                    ip_address,
+                    (err, result) => {
+                        if (err) {
+                            logger.error('插入新设备失败:', err);
+                            return;
+                        }
+                        logger.info(`新设备插入成功,设备ID:${result.insertId}`);
+                    }
+                );
+            } else {
+                // 设备存在,更新状态
+                if (status === 'online') {
+                    const first_online_time = (!device || !device.first_online_time) ? timestamp : device.first_online_time;
+                    logger.info(`设备 ${client_id} 上线`);
+
+                    Device.updateOnlineStatus(
+                        client_id,
+                        status,
+                        timestamp,
+                        first_online_time,
+                        timestamp,
+                        ip_address,
+                        (err) => {
+                            if (err) {
+                                logger.error('更新设备在线状态失败:', err);
+                                return;
+                            }
+                            logger.info(`设备 ${client_id} 在线状态更新成功`);
+                        }
+                    );
+                } else {
+                    logger.info(`设备 ${client_id} 下线`);
+                    Device.updateOfflineStatus(
+                        client_id,
+                        status,
+                        timestamp,
+                        timestamp,
+                        (err) => {
+                            if (err) {
+                                logger.error('更新设备离线状态失败:', err);
+                                return;
+                            }
+                            logger.info(`设备 ${client_id} 离线状态更新成功`);
+                        }
+                    );
+                }
+            }
+        });
+    }
+    // 处理温度回传事件
+    // 在handleTemperatureEvent方法中:
+    // 修改handleTemperatureEvent调用方式
+    static async handleTemperatureEvent(eventData) {
+        const topic = eventData.topic;
+        const payload = eventData.payload;
+        const timestamp = WebhookService.convertToBeijingTime(eventData.timestamp);
+        const device_id = topic.split('/')[1];
+
+        logger.info(`处理温度事件: 设备=${device_id}, 温度=${payload}`);
+
+        const room_number = eventData.room_number || 'default_room';
+        const room_name = eventData.room_name || '默认房间';
+
+        try {
+            await new Promise((resolve, reject) => {
+                Device.updateTemperature(
+                    device_id,
+                    payload,
+                    timestamp,
+                    room_number,  // 新增参数
+                    room_name,    // 新增参数
+                    (err) => {    // 保持回调函数存在
+                        if (err) return reject(err);
+                        resolve();
+                    }
+                );
+            });
+            // 删除重复的updateTemperature调用
+            const device = await new Promise((resolve) => {
+                Device.getDeviceById(device_id, (err, device) => {
+                    resolve(device || {});
+                });
+            });
+
+            logger.debug('开始处理加热器业务逻辑', {
+                device_id,
+                status: device.status,
+                temperature: payload,
+                switch_status: device.switch_status
+            });
+
+            await HeaterUsageService.handleHeaterUsage(
+                device_id,
+                device.status,
+                payload,
+                device.switch_status,
+                timestamp
+            );
+
+            logger.info(`设备 ${device_id} 温度处理完成`);
+        } catch (error) {
+            logger.error(`温度事件处理失败: ${error.message}`, {
+                device_id,
+                stack: error.stack
+            });
+        }  // 添加方法结束括号
+    } // 此处添加方法结束括号
+    // 处理开关指令事件
+    static handleSwitchEvent(eventData) {
+        const topic = eventData.topic;
+        const payload = eventData.payload;
+        const timestamp = WebhookService.convertToBeijingTime(eventData.timestamp);
+        const device_id = topic.split('/')[1];
+
+        logger.info(`处理开关事件: 设备=${device_id}, 状态=${payload}`);
+
+        // 修正参数传递
+        const room_number = eventData.room_number || 'N/A';
+        const room_name = eventData.room_name || '未命名房间';
+
+        Device.updateSwitchStatus(
+            device_id,
+            payload,  // 修正为使用 payload 而不是未定义的 switch_status
+            timestamp,
+            room_number,
+            room_name,
+            (err) => {
+                if (err) {
+                    logger.error('更新开关状态失败:', err);
+                    return;
+                }
+                logger.info(`设备 ${device_id} 开关状态更新成功`);
+            }
+        );
+    }
+
+    // 处理继电器状态事件
+    static handleRelayStateEvent(eventData) {
+        const topic = eventData.topic;
+        const payload = eventData.payload;
+        const timestamp = WebhookService.convertToBeijingTime(eventData.timestamp);
+        const device_id = topic.split('/')[1];
+
+        logger.info(`处理继电器状态事件: 设备=${device_id}, 状态=${payload}`);
+
+        Device.updateSwitchStatus(device_id, payload, timestamp, (err) => {
+            if (err) {
+                logger.error('更新继电器状态失败:', err);
+                return;
+            }
+            logger.info(`设备 ${device_id} 继电器状态更新成功: ${payload}`);
+        });
+    }
+
+    // 处理 GPIO 状态事件
+    static handleGpioStateEvent(eventData) {
+        const topic = eventData.topic;
+        const payload = eventData.payload;
+        const timestamp = WebhookService.convertToBeijingTime(eventData.timestamp);
+        const device_id = topic.split('/')[1];
+
+        logger.info(`处理GPIO状态事件: 设备=${device_id}, 状态=${payload}`);
+
+        Device.updateLevelStatus(device_id, payload, timestamp, (err) => {
+            if (err) {
+                logger.error('更新GPIO状态失败:', err);
+                return;
+            }
+            logger.info(`设备 ${device_id} GPIO状态更新成功: ${payload}`);
+        });
+    }
+
+    // 处理授权检查完成事件
+    static handleAuthzCheckEvent(eventData) {
+        const client_id = eventData.clientid || eventData.client_id;
+        const topic = eventData.topic;
+        const action = eventData.action;
+        const result = eventData.result || 'deny';
+        const timestamp = eventData.timestamp || Date.now();
+
+        logger.info(`处理授权检查事件: 客户端=${client_id}, 主题=${topic}`);
+        logger.debug('授权检查详情:', { action, result });
+
+        if (!client_id) {
+            logger.error('无效的授权检查事件: 缺少客户端ID');
+            return;
+        }
+
+        const authzData = {
+            client_id,
+            topic,
+            action,
+            result: result === 'allow' ? 'allow' : 'deny',
+            timestamp: WebhookService.convertToBeijingTime(timestamp)
+        };
+
+        AuthzCheck.insert(authzData, (err, result) => {
+            if (err) {
+                logger.error('记录授权检查事件失败:', err);
+                return;
+            }
+            logger.info(`授权检查事件记录成功: ID=${result.insertId}`);
+        });
+    }
+
+    // 处理消息传递事件
+    static handleMessageDeliveredEvent(eventData) {
+        const client_id = eventData.clientid || eventData.client_id || undefined;
+        const { topic, payload, timestamp } = eventData;
+        console.log('Message delivered:', {
+            client_id,
+            topic,
+            payload,
+            timestamp: WebhookService.convertToBeijingTime(timestamp) // 使用北京时间
+        });
+    }
+
+    // 处理消息丢弃事件
+    static handleMessageDroppedEvent(eventData) {
+        const client_id = eventData.clientid || eventData.client_id || undefined;
+        const { topic, payload, reason, timestamp } = eventData;
+        console.log('Message dropped:', {
+            client_id,
+            topic,
+            payload,
+            reason,
+            timestamp: WebhookService.convertToBeijingTime(timestamp) // 使用北京时间
+        });
+    }
+}
+
+// 导出 WebhookService 类
+module.exports = WebhookService;