// httpServer.js - HTTP 接口服务 const http = require('http'); const url = require('url'); const logger = require('./logger'); const dataCache = require('./dataCache'); const rateLimiter = require('./rateLimiter'); const propertyMapper = require('./propertyMapper'); class HttpServer { constructor(port = 8080, config = {}) { this.port = port; this.config = { enabled: config.enabled !== false, host: config.host || '0.0.0.0', cors: { enabled: config.cors?.enabled !== false, allowOrigin: config.cors?.allowOrigin || '*' }, rateLimit: { enabled: config.rateLimit?.enabled !== false, interval: config.rateLimit?.interval || 5000, // 默认 5 秒 allDevicesInterval: config.rateLimit?.allDevicesInterval || 60000 // 默认 1 分钟 }, propertyMapping: { enabled: config.propertyMapping?.enabled !== false, includeRawData: config.propertyMapping?.includeRawData === true } }; this.server = null; } /** * 启动 HTTP 服务 */ start() { if (!this.config.enabled) { logger.info('🔴 HTTP 服务已禁用'); return; } this.server = http.createServer((req, res) => { this.handleRequest(req, res); }); this.server.on('error', (err) => { logger.error(`HTTP 服务错误: ${err.message}`); }); this.server.listen(this.port, this.config.host, () => { logger.info(`🌐 HTTP 服务已启动,监听地址: ${this.config.host}:${this.port}`); }); } /** * 处理 HTTP 请求 */ handleRequest(req, res) { const parsedUrl = url.parse(req.url, true); const pathname = parsedUrl.pathname; const query = parsedUrl.query; // 设置通用响应头 res.setHeader('Content-Type', 'application/json; charset=utf-8'); // 根据配置设置 CORS 头 if (this.config.cors.enabled) { res.setHeader('Access-Control-Allow-Origin', this.config.cors.allowOrigin); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); } // 处理 OPTIONS 请求(CORS 预检) if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } try { logger.info(`📥 收到请求: ${req.method} ${pathname}${req.url.includes('?') ? '?' + req.url.split('?')[1] : ''}`); switch (pathname) { // 获取指定设备的最新数据 case '/api/device/data': this.handleGetDeviceData(req, res, query); break; // 获取所有设备的数据 case '/api/device/all': this.handleGetAllDevices(req, res, query); break; // 获取设备列表 case '/api/device/list': this.handleGetDeviceList(req, res); break; // 获取缓存统计信息 case '/api/cache/stats': this.handleGetStats(req, res); break; // 清空缓存 case '/api/cache/clear': this.handleClearCache(req, res); break; // 获取限流统计信息 case '/api/ratelimit/stats': this.handleRateLimitStats(req, res); break; // 清空限流记录 case '/api/ratelimit/clear': this.handleRateLimitClear(req, res); break; // 获取空闲设备列表 case '/api/device/idle': this.handleGetIdleDevices(req, res, query); break; // 健康检查 case '/api/health': this.handleHealth(req, res); break; // 根路径 case '/': this.handleRoot(req, res); break; default: this.sendError(res, 404, '接口不存在'); } } catch (err) { logger.error(`请求处理错误: ${err.message}`); this.sendError(res, 500, '服务器内部错误'); } } /** * 获取指定设备的最新数据 * GET /api/device/data?deviceNumber=D001 * GET /api/device/data?deviceNumber=D001&mapped=true (启用属性映射) */ handleGetDeviceData(req, res, query) { const deviceNumber = query.deviceNumber; const enableMapping = query.mapped === 'true'; // 支持查询参数控制映射 if (!deviceNumber) { this.sendError(res, 400, '缺少必要参数: deviceNumber'); return; } // ✅【新增】限流检查 - 根据设备号进行限流 if (this.config.rateLimit.enabled) { const limitCheck = rateLimiter.checkLimit(deviceNumber, this.config.rateLimit.interval); if (!limitCheck.allowed) { this.sendError(res, 429, `请求过于频繁,请在 ${limitCheck.remainingTime}ms 后再试`); return; } } const data = dataCache.getDeviceData(deviceNumber); if (!data) { this.sendError(res, 404, `设备 ${deviceNumber} 未找到`); return; } // ✅【新增】属性映射处理 let responseData = { deviceNumber: deviceNumber, timestamp: data.timestamp || new Date().toISOString(), data: data }; // 如果启用了映射,或配置中默认启用了映射 const useMappedFormat = enableMapping || this.config.propertyMapping.enabled; if (useMappedFormat) { try { const mappedData = propertyMapper.transformForHTTP(data, deviceNumber); responseData = { deviceNumber: deviceNumber, timestamp: data.timestamp || new Date().toISOString(), properties: mappedData, // [{identifier, name, value}, ...] format: 'mapped' }; // 如果配置允许,也包含原始数据 if (this.config.propertyMapping.includeRawData) { responseData.rawData = data; } } catch (err) { logger.error(`属性映射失败: ${err.message}`); // 映射失败时回退到原始格式 responseData.format = 'raw'; responseData.mappingError = err.message; } } this.sendSuccess(res, responseData); } /** * 获取所有设备的数据 * GET /api/device/all * GET /api/device/all?mapped=true (启用属性映射) */ handleGetAllDevices(req, res, query) { const enableMapping = query?.mapped === 'true'; // 支持查询参数控制映射 // ✅【新增】限流检查 - 所有设备使用统一标识符,间隔 1 分钟 if (this.config.rateLimit.enabled) { const limitCheck = rateLimiter.checkLimit('__all_devices__', this.config.rateLimit.allDevicesInterval); if (!limitCheck.allowed) { this.sendError(res, 429, `请求过于频繁,请在 ${limitCheck.remainingTime}ms 后再试`); return; } } const allData = dataCache.getAllDeviceData(); if (Object.keys(allData).length === 0) { this.sendSuccess(res, { message: '暂无设备数据', count: 0, data: {}, format: 'raw' }); return; } // ✅【新增】属性映射处理 const useMappedFormat = enableMapping || this.config.propertyMapping.enabled; if (useMappedFormat) { try { const mappedDevices = {}; for (const [deviceNumber, rawData] of Object.entries(allData)) { mappedDevices[deviceNumber] = { timestamp: rawData.timestamp || new Date().toISOString(), properties: propertyMapper.transformForHTTP(rawData, deviceNumber), format: 'mapped' }; if (this.config.propertyMapping.includeRawData) { mappedDevices[deviceNumber].rawData = rawData; } } this.sendSuccess(res, { count: Object.keys(mappedDevices).length, data: mappedDevices, format: 'mapped' }); return; } catch (err) { logger.error(`批量属性映射失败: ${err.message}`); // 映射失败时回退到原始格式 } } this.sendSuccess(res, { count: Object.keys(allData).length, data: allData, format: 'raw' }); } /** * 获取设备列表(简化版,仅包含基本信息) * GET /api/device/list */ handleGetDeviceList(req, res) { const deviceList = dataCache.getDeviceList(); this.sendSuccess(res, { count: deviceList.length, devices: deviceList }); } /** * 获取缓存统计信息 * GET /api/cache/stats */ handleGetStats(req, res) { const stats = dataCache.getStats(); this.sendSuccess(res, { timestamp: new Date().toISOString(), ...stats }); } /** * 清空所有缓存 * POST /api/cache/clear */ handleClearCache(req, res) { if (req.method !== 'POST' && req.method !== 'GET') { this.sendError(res, 405, '方法不允许'); return; } dataCache.clearAll(); this.sendSuccess(res, { message: '缓存已清空' }); } /** * 获取空闲设备列表 * GET /api/device/idle?timeout=300000 */ handleGetIdleDevices(req, res, query) { const timeout = parseInt(query.timeout) || 300000; // 默认 5 分钟 const idleDevices = dataCache.getIdleDevices(timeout); this.sendSuccess(res, { timeout: timeout, count: idleDevices.length, devices: idleDevices }); } /** * 健康检查 * GET /api/health */ handleHealth(req, res) { this.sendSuccess(res, { status: 'ok', timestamp: new Date().toISOString() }); } /** * 获取限流统计信息 * GET /api/ratelimit/stats */ handleRateLimitStats(req, res) { const stats = rateLimiter.getStats(); this.sendSuccess(res, { enabled: this.config.rateLimit.enabled, interval: this.config.rateLimit.interval, ...stats }); } /** * 清空限流记录 * POST /api/ratelimit/clear */ handleRateLimitClear(req, res) { if (req.method !== 'POST' && req.method !== 'GET') { this.sendError(res, 405, '方法不允许'); return; } rateLimiter.clearAll(); this.sendSuccess(res, { message: '限流记录已清空' }); } /** * 根路径 - 返回 API 文档 * GET / */ handleRoot(req, res) { const apiDocs = { service: '透析机数据 HTTP API', version: '1.0.0', timestamp: new Date().toISOString(), rateLimit: { enabled: this.config.rateLimit.enabled, deviceDataInterval: `${this.config.rateLimit.interval}ms`, allDevicesInterval: `${this.config.rateLimit.allDevicesInterval}ms` }, endpoints: { '获取指定设备数据': { method: 'GET', path: '/api/device/data', params: { deviceNumber: '设备序号' }, example: '/api/device/data?deviceNumber=D001', rateLimit: `${this.config.rateLimit.interval}ms (按设备号)` }, '获取所有设备数据': { method: 'GET', path: '/api/device/all', description: '返回所有设备的完整数据', rateLimit: `${this.config.rateLimit.allDevicesInterval}ms (全局)` }, '获取设备列表': { method: 'GET', path: '/api/device/list', description: '返回设备列表(简化版)' }, '获取缓存统计': { method: 'GET', path: '/api/cache/stats', description: '返回缓存使用情况' }, '获取限流状态': { method: 'GET', path: '/api/ratelimit/stats', description: '返回限流统计信息' }, '清空缓存': { method: 'POST', path: '/api/cache/clear', description: '清空所有缓存数据' }, '清空限流记录': { method: 'POST', path: '/api/ratelimit/clear', description: '清空所有限流记录' }, '获取空闲设备': { method: 'GET', path: '/api/device/idle', params: { timeout: '超时时间(毫秒,默认300000)' }, example: '/api/device/idle?timeout=300000' }, '健康检查': { method: 'GET', path: '/api/health' } } }; this.sendSuccess(res, apiDocs); } /** * 发送成功响应 */ sendSuccess(res, data) { res.writeHead(200); res.end(JSON.stringify({ code: 0, message: 'success', data: data, timestamp: new Date().toISOString() })); } /** * 发送错误响应 */ sendError(res, statusCode, message) { res.writeHead(statusCode); res.end(JSON.stringify({ code: statusCode, message: message, timestamp: new Date().toISOString() })); } /** * 停止 HTTP 服务 */ stop() { if (this.server) { this.server.close(); logger.info('HTTP 服务已停止'); } } } module.exports = HttpServer;