Files
server/dist/managers/WebClientManager.js
wdvipa 450367dea2 111
2026-02-09 16:34:01 +08:00

385 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Logger_1 = __importDefault(require("../utils/Logger"));
/**
* Web客户端管理器
*/
class WebClientManager {
constructor(databaseService) {
this.clients = new Map();
this.socketToClient = new Map();
this.deviceControllers = new Map(); // deviceId -> clientId
// 🔧 添加请求速率限制 - 防止频繁重复请求
this.requestTimestamps = new Map(); // "clientId:deviceId" -> timestamp
this.REQUEST_COOLDOWN = 2000; // 2秒内不允许重复请求增加冷却时间
this.logger = new Logger_1.default('WebClientManager');
this.databaseService = databaseService;
}
/**
* ✅ 清理所有客户端记录(服务器重启时调用)
*/
clearAllClients() {
const clientCount = this.clients.size;
this.clients.clear();
this.socketToClient.clear();
this.deviceControllers.clear();
this.requestTimestamps.clear();
this.logger.info(`🧹 已清理所有客户端记录: ${clientCount} 个客户端`);
}
/**
* 设置Socket.IO实例
*/
setSocketIO(io) {
this.io = io;
}
/**
* 添加Web客户端
*/
addClient(clientInfo) {
// 🔧 检查是否已有相同Socket ID的客户端记录
const existingClientId = this.socketToClient.get(clientInfo.socketId);
if (existingClientId) {
this.logger.warn(`⚠️ Socket ${clientInfo.socketId} 已有客户端记录 ${existingClientId},清理旧记录`);
this.removeClient(existingClientId);
}
this.clients.set(clientInfo.id, clientInfo);
this.socketToClient.set(clientInfo.socketId, clientInfo.id);
this.logger.info(`Web客户端已添加: ${clientInfo.id} from ${clientInfo.ip}`);
}
/**
* 移除Web客户端
*/
removeClient(clientId) {
const client = this.clients.get(clientId);
if (client) {
this.clients.delete(clientId);
this.socketToClient.delete(client.socketId);
// 如果客户端正在控制设备,释放控制权
if (client.controllingDeviceId) {
this.logger.info(`🔓 客户端断开连接,自动释放设备控制权: ${clientId} -> ${client.controllingDeviceId}`);
this.releaseDeviceControl(client.controllingDeviceId);
}
// 清理请求时间戳记录
const keysToDelete = Array.from(this.requestTimestamps.keys()).filter(key => key.startsWith(clientId + ':'));
keysToDelete.forEach(key => this.requestTimestamps.delete(key));
this.logger.info(`Web客户端已移除: ${clientId}`);
return true;
}
return false;
}
/**
* 通过Socket ID移除客户端
*/
removeClientBySocketId(socketId) {
const clientId = this.socketToClient.get(socketId);
if (clientId) {
return this.removeClient(clientId);
}
return false;
}
/**
* 获取客户端信息
*/
getClient(clientId) {
return this.clients.get(clientId);
}
/**
* 通过Socket ID获取客户端
*/
getClientBySocketId(socketId) {
const clientId = this.socketToClient.get(socketId);
return clientId ? this.clients.get(clientId) : undefined;
}
/**
* 获取所有客户端
*/
getAllClients() {
return Array.from(this.clients.values());
}
/**
* 获取客户端数量
*/
getClientCount() {
return this.clients.size;
}
/**
* 获取客户端Socket
*/
getClientSocket(clientId) {
const client = this.clients.get(clientId);
if (client && this.io) {
return this.io.sockets.sockets.get(client.socketId);
}
return undefined;
}
/**
* 请求控制设备
*/
requestDeviceControl(clientId, deviceId) {
// 🔧 防止频繁重复请求
const requestKey = `${clientId}:${deviceId}`;
const now = Date.now();
const lastRequestTime = this.requestTimestamps.get(requestKey) || 0;
if (now - lastRequestTime < this.REQUEST_COOLDOWN) {
this.logger.debug(`🚫 请求过于频繁: ${clientId} -> ${deviceId} (间隔${now - lastRequestTime}ms < ${this.REQUEST_COOLDOWN}ms)`);
return {
success: false,
message: '请求过于频繁,请稍后再试'
};
}
// 获取客户端信息
const client = this.clients.get(clientId);
if (!client) {
this.logger.error(`❌ 客户端不存在: ${clientId}`);
return {
success: false,
message: '客户端不存在'
};
}
// ✅ 优化:先检查是否是重复请求(已经在控制此设备)
const currentController = this.deviceControllers.get(deviceId);
if (currentController === clientId) {
this.logger.debug(`🔄 客户端 ${clientId} 重复请求控制设备 ${deviceId},已在控制中`);
client.lastSeen = new Date();
// 更新请求时间戳,但返回成功(避免频繁日志)
this.requestTimestamps.set(requestKey, now);
return {
success: true,
message: '已在控制此设备'
};
}
// 记录请求时间戳(在检查重复控制后记录)
this.requestTimestamps.set(requestKey, now);
// 检查设备是否被其他客户端控制
if (currentController && currentController !== clientId) {
const controllerClient = this.clients.get(currentController);
this.logger.warn(`🚫 设备 ${deviceId} 控制权冲突: 当前控制者 ${currentController}, 请求者 ${clientId}`);
return {
success: false,
message: `设备正在被其他客户端控制 (${controllerClient?.ip || 'unknown'})`,
currentController
};
}
// 如果客户端已在控制其他设备,先释放
if (client.controllingDeviceId && client.controllingDeviceId !== deviceId) {
this.logger.info(`🔄 客户端 ${clientId} 切换控制设备: ${client.controllingDeviceId} -> ${deviceId}`);
this.releaseDeviceControl(client.controllingDeviceId);
}
// 建立控制关系
this.deviceControllers.set(deviceId, clientId);
client.controllingDeviceId = deviceId;
client.lastSeen = new Date();
// 🔐 如果客户端有用户ID将权限持久化到数据库
if (client.userId && this.databaseService) {
this.databaseService.grantUserDevicePermission(client.userId, deviceId, 'control');
this.logger.info(`🔐 用户 ${client.userId} 的设备 ${deviceId} 控制权限已持久化`);
}
this.logger.info(`🎮 客户端 ${clientId} 开始控制设备 ${deviceId}`);
return {
success: true,
message: '控制权获取成功'
};
}
/**
* 释放设备控制权
*/
releaseDeviceControl(deviceId) {
const controllerId = this.deviceControllers.get(deviceId);
if (controllerId) {
const client = this.clients.get(controllerId);
if (client) {
const previousDevice = client.controllingDeviceId;
client.controllingDeviceId = undefined;
this.logger.debug(`🔓 客户端 ${controllerId} 释放设备控制权: ${previousDevice}`);
}
else {
this.logger.warn(`⚠️ 控制设备 ${deviceId} 的客户端 ${controllerId} 不存在,可能已断开`);
}
this.deviceControllers.delete(deviceId);
this.logger.info(`🔓 设备 ${deviceId} 的控制权已释放 (之前控制者: ${controllerId})`);
return true;
}
else {
this.logger.debug(`🤷 设备 ${deviceId} 没有被控制,无需释放`);
return false;
}
}
/**
* 获取设备控制者
*/
getDeviceController(deviceId) {
return this.deviceControllers.get(deviceId);
}
/**
* 检查客户端是否有设备控制权
*/
hasDeviceControl(clientId, deviceId) {
// 🛡️ 记录权限检查审计日志
this.logPermissionOperation(clientId, deviceId, '权限检查');
// 🔐 获取客户端信息
const client = this.clients.get(clientId);
// 🆕 超级管理员绕过权限检查
if (client?.username) {
const superAdminUsername = process.env.SUPERADMIN_USERNAME || 'superadmin';
if (client.username === superAdminUsername) {
this.logger.info(`🔐 超级管理员 ${client.username} 绕过设备控制权限检查`);
return true;
}
}
// 🔐 首先检查内存中的控制权
const memoryControl = this.deviceControllers.get(deviceId) === clientId;
if (memoryControl) {
return true;
}
// 🔐 如果内存中没有控制权,检查数据库中的用户权限
if (client?.userId && this.databaseService) {
const hasPermission = this.databaseService.hasUserDevicePermission(client.userId, deviceId, 'control');
if (hasPermission) {
// 🔐 如果用户有权限,自动建立控制关系(允许权限恢复)
this.deviceControllers.set(deviceId, clientId);
client.controllingDeviceId = deviceId;
this.logger.info(`🔐 用户 ${client.userId} 基于数据库权限获得设备 ${deviceId} 控制权`);
return true;
}
}
return false;
}
/**
* 向指定客户端发送消息
*/
sendToClient(clientId, event, data) {
const socket = this.getClientSocket(clientId);
if (socket) {
socket.emit(event, data);
return true;
}
return false;
}
/**
* 向所有客户端广播消息
*/
broadcastToAll(event, data) {
if (this.io) {
let activeClients = 0;
// 只向Web客户端广播且过滤掉已断开的连接
for (const [socketId, clientId] of this.socketToClient.entries()) {
const socket = this.io.sockets.sockets.get(socketId);
if (socket && socket.connected) {
socket.emit(event, data);
activeClients++;
}
}
this.logger.debug(`📡 广播消息到 ${activeClients} 个活跃Web客户端: ${event}`);
}
}
/**
* 向控制指定设备的客户端发送消息
*/
sendToDeviceController(deviceId, event, data) {
const controllerId = this.deviceControllers.get(deviceId);
if (controllerId) {
return this.sendToClient(controllerId, event, data);
}
return false;
}
/**
* 更新客户端活跃时间
*/
updateClientActivity(socketId) {
const clientId = this.socketToClient.get(socketId);
if (clientId) {
const client = this.clients.get(clientId);
if (client) {
client.lastSeen = new Date();
}
}
}
/**
* 清理不活跃的客户端
*/
cleanupInactiveClients(timeoutMs = 600000) {
const now = Date.now();
const clientsToRemove = [];
for (const [clientId, client] of this.clients.entries()) {
if (now - client.lastSeen.getTime() > timeoutMs) {
clientsToRemove.push(clientId);
}
}
clientsToRemove.forEach(clientId => {
this.removeClient(clientId);
});
if (clientsToRemove.length > 0) {
this.logger.info(`已清理 ${clientsToRemove.length} 个不活跃的Web客户端`);
}
}
/**
* 获取客户端统计信息
*/
getClientStats() {
const clients = Array.from(this.clients.values());
return {
total: clients.length,
controlling: clients.filter(c => c.controllingDeviceId).length,
idle: clients.filter(c => !c.controllingDeviceId).length,
};
}
/**
* 🔐 恢复用户的设备权限
*/
restoreUserPermissions(userId, clientId) {
if (!this.databaseService) {
this.logger.warn('数据库服务未初始化,无法恢复用户权限');
return;
}
try {
// 获取用户的所有设备权限
const permissions = this.databaseService.getUserDevicePermissions(userId);
if (permissions.length > 0) {
this.logger.info(`🔐 为用户 ${userId} 恢复 ${permissions.length} 个设备权限`);
// 恢复第一个设备的控制权(优先恢复用户之前的权限)
for (const permission of permissions) {
if (permission.permissionType === 'control') {
// 直接恢复权限,不检查冲突(因为这是用户自己的权限恢复)
this.deviceControllers.set(permission.deviceId, clientId);
const client = this.clients.get(clientId);
if (client) {
client.controllingDeviceId = permission.deviceId;
this.logger.info(`🔐 用户 ${userId} 的设备 ${permission.deviceId} 控制权已恢复`);
break; // 只恢复第一个设备
}
}
}
}
}
catch (error) {
this.logger.error('恢复用户权限失败:', error);
}
}
/**
* 🔐 设置客户端用户信息
*/
setClientUserInfo(clientId, userId, username) {
const client = this.clients.get(clientId);
if (client) {
client.userId = userId;
client.username = username;
this.logger.info(`🔐 客户端 ${clientId} 用户信息已设置: ${username} (${userId})`);
// 🛡️ 记录安全审计日志
this.logger.info(`🛡️ 安全审计: 客户端 ${clientId} (IP: ${client.ip}) 绑定用户 ${username} (${userId})`);
}
}
/**
* 🛡️ 记录权限操作审计日志
*/
logPermissionOperation(clientId, deviceId, operation) {
const client = this.clients.get(clientId);
if (client) {
this.logger.info(`🛡️ 权限审计: 客户端 ${clientId} (用户: ${client.username || 'unknown'}, IP: ${client.ip}) 执行 ${operation} 操作,目标设备: ${deviceId}`);
}
}
}
exports.default = WebClientManager;
//# sourceMappingURL=WebClientManager.js.map