// 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;
|