From 7cfe3332f016d7def7ca8ad25ed8bbdc33d23ed2 Mon Sep 17 00:00:00 2001
From: chenyc <501753378@qq.com>
Date: 星期二, 09 十二月 2025 14:52:41 +0800
Subject: [PATCH] gxhttp服务
---
httpServer.js | 477 ++++++
MQTT_INTEGRATION_GUIDE.md | 440 ++++++
QUICK_START.md | 488 ++++++
RATE_LIMIT_GUIDE.md | 494 ++++++
aliyun.json | 2
rateLimiter.js | 132 +
quickTest.js | 24
openapi.json | 584 ++++++++
HTTP_API_INTEGRATION_GUIDE.md | 706 +++++++++
index.js | 15
/dev/null | 0
propertyMapper.js | 262 +++
Postman_Collection.json | 257 +++
START_HERE.md | 185 ++
dataCache.js | 151 ++
httpConfig.json | 14
schema.json | 74 +
17 files changed, 4,303 insertions(+), 2 deletions(-)
diff --git a/HTTP_API_INTEGRATION_GUIDE.md b/HTTP_API_INTEGRATION_GUIDE.md
new file mode 100644
index 0000000..de1acd8
--- /dev/null
+++ b/HTTP_API_INTEGRATION_GUIDE.md
@@ -0,0 +1,706 @@
+# 透析机IoT数据服务 - HTTP API 第三方接入文档
+
+**版本**: v1.1.0
+**更新日期**: 2025-12-08
+**状态**: 生产就绪 ✅
+
+
+## 🌐 服务概览
+
+### 服务地址
+
+| 环境 | 地址 | 端口 |
+|------|------|------|
+| 开发环境 | http://localhost | 8080 |
+| 生产环境 | http://[IP地址] | 8080 |
+
+### 功能特性
+
+✅ **实时数据查询** - 获取透析机设备的实时数据
+✅ **属性映射** - 自动转换为可读的中文属性名
+✅ **批量查询** - 一次获取所有设备数据
+✅ **缓存管理** - 数据缓存和统计功能
+✅ **限流保护** - 防止API滥用
+✅ **CORS支持** - 跨域请求支持
+
+---
+
+## 🚀 接入指南
+
+### 前置要求
+
+- 网络连接:能访问服务器IP地址和8080端口
+- HTTP客户端:支持标准HTTP/1.1
+- 字符编码:UTF-8
+
+### 基本步骤
+
+1. **获取服务地址** - 咨询系统管理员获取实际服务地址
+2. **测试连接** - 调用健康检查接口验证连接
+3. **获取设备列表** - 查看可用的设备号
+4. **查询设备数据** - 获取需要的设备数据
+
+### 测试连接
+
+```bash
+# 健康检查 - 验证服务可用性
+curl -X GET "http://localhost:8080/api/health"
+
+# 预期响应
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "status": "ok",
+ "timestamp": "2025-12-08T10:30:45.123Z"
+ }
+}
+```
+
+---
+
+## 📡 API端点
+
+### 1. 获取单个设备数据
+
+**端点**: `GET /api/device/data`
+
+**参数**:
+| 参数 | 类型 | 必需 | 说明 | 示例 |
+|------|------|------|------|------|
+| deviceNumber | string | ✅ | 设备号 | D001 |
+| mapped | string | ❌ | 是否返回映射格式 | true/false |
+
+**请求示例**:
+```bash
+# 获取原始格式数据
+curl "http://localhost:8080/api/device/data?deviceNumber=D001"
+
+# 获取映射格式数据(推荐)
+curl "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+```
+
+**响应示例(映射格式)**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "deviceNumber": "D001",
+ "timestamp": "2025-12-08T10:30:45.123Z",
+ "properties": [
+ {
+ "identifier": "A",
+ "name": "脱水目标量",
+ "value": "50"
+ },
+ {
+ "identifier": "B",
+ "name": "脱水量",
+ "value": "25"
+ },
+ {
+ "identifier": "C",
+ "name": "脱水速率",
+ "value": "10"
+ },
+ {
+ "identifier": "R",
+ "name": "收缩压下限",
+ "value": "120"
+ },
+ {
+ "identifier": "mb",
+ "name": "脉搏-德朗",
+ "value": "72"
+ }
+ ],
+ "format": "mapped"
+ }
+}
+```
+
+**限流规则**: 同一设备5秒内最多请求1次
+
+---
+
+### 2. 获取所有设备数据
+
+**端点**: `GET /api/device/all`
+
+**参数**:
+| 参数 | 类型 | 必需 | 说明 |
+|------|------|------|------|
+| mapped | string | ❌ | 是否返回映射格式 |
+
+**请求示例**:
+```bash
+# 获取所有设备映射数据
+curl "http://localhost:8080/api/device/all?mapped=true"
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "count": 2,
+ "data": {
+ "D001": {
+ "timestamp": "2025-12-08T10:30:45.123Z",
+ "properties": [
+ {
+ "identifier": "A",
+ "name": "脱水目标量",
+ "value": "50"
+ }
+ ],
+ "format": "mapped"
+ },
+ "D002": {
+ "timestamp": "2025-12-08T10:30:50.456Z",
+ "properties": [
+ {
+ "identifier": "A",
+ "name": "脱水目标量",
+ "value": "75"
+ }
+ ],
+ "format": "mapped"
+ }
+ },
+ "format": "mapped"
+ }
+}
+```
+
+**限流规则**: 全局1分钟内最多请求1次
+
+---
+
+### 3. 获取设备列表
+
+**端点**: `GET /api/device/list`
+
+**参数**: 无
+
+**请求示例**:
+```bash
+curl "http://localhost:8080/api/device/list"
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "count": 2,
+ "devices": [
+ {
+ "deviceNumber": "D001",
+ "lastUpdate": "2025-12-08T10:30:45.123Z"
+ },
+ {
+ "deviceNumber": "D002",
+ "lastUpdate": "2025-12-08T10:30:50.456Z"
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 4. 获取缓存统计
+
+**端点**: `GET /api/cache/stats`
+
+**参数**: 无
+
+**请求示例**:
+```bash
+curl "http://localhost:8080/api/cache/stats"
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "timestamp": "2025-12-08T10:30:45.123Z",
+ "totalDevices": 2,
+ "cachedProperties": 156,
+ "cacheSize": "45KB",
+ "oldestData": "2025-12-08T10:20:30.000Z",
+ "newestData": "2025-12-08T10:30:45.123Z"
+ }
+}
+```
+
+---
+
+### 5. 获取限流统计
+
+**端点**: `GET /api/ratelimit/stats`
+
+**参数**: 无
+
+**请求示例**:
+```bash
+curl "http://localhost:8080/api/ratelimit/stats"
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "enabled": true,
+ "interval": 5000,
+ "allDevicesInterval": 60000,
+ "records": [
+ {
+ "identifier": "D001",
+ "lastRequest": "2025-12-08T10:30:45.123Z",
+ "requestCount": 3
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 6. 清空缓存
+
+**端点**: `POST /api/cache/clear`
+
+**参数**: 无
+
+**请求示例**:
+```bash
+curl -X POST "http://localhost:8080/api/cache/clear"
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "message": "缓存已清空"
+ }
+}
+```
+
+---
+
+### 7. 健康检查
+
+**端点**: `GET /api/health`
+
+**参数**: 无
+
+**请求示例**:
+```bash
+curl "http://localhost:8080/api/health"
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "status": "ok",
+ "timestamp": "2025-12-08T10:30:45.123Z"
+ }
+}
+```
+
+---
+
+## 📨 请求示例
+
+### cURL 示例
+
+```bash
+# 基础请求
+curl -X GET "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+
+# 添加自定义头
+curl -X GET "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true" \
+ -H "Content-Type: application/json" \
+ -H "Accept: application/json"
+
+# 带超时
+curl --connect-timeout 5 --max-time 30 \
+ "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+```
+
+### JavaScript/Node.js 示例
+
+```javascript
+// 使用 fetch API
+async function getDeviceData(deviceNumber) {
+ try {
+ const response = await fetch(
+ `http://localhost:8080/api/device/data?deviceNumber=${deviceNumber}&mapped=true`
+ );
+ const result = await response.json();
+
+ if (result.code === 200) {
+ console.log('设备数据:', result.data);
+ result.data.properties.forEach(prop => {
+ console.log(`${prop.name}: ${prop.value}`);
+ });
+ } else {
+ console.error('获取失败:', result.message);
+ }
+ } catch (error) {
+ console.error('请求错误:', error);
+ }
+}
+
+getDeviceData('D001');
+```
+
+### Python 示例
+
+```python
+import requests
+import json
+
+# 获取单个设备数据
+def get_device_data(device_number):
+ url = f'http://localhost:8080/api/device/data?deviceNumber={device_number}&mapped=true'
+
+ try:
+ response = requests.get(url, timeout=5)
+ result = response.json()
+
+ if result['code'] == 200:
+ print(f'设备: {device_number}')
+ for prop in result['data']['properties']:
+ print(f" {prop['name']}: {prop['value']}")
+ else:
+ print(f'错误: {result["message"]}')
+ except requests.RequestException as e:
+ print(f'请求错误: {e}')
+
+# 获取所有设备
+def get_all_devices():
+ url = 'http://localhost:8080/api/device/all?mapped=true'
+
+ response = requests.get(url)
+ data = response.json()
+
+ if data['code'] == 200:
+ for device_number, device_data in data['data']['data'].items():
+ print(f"\n设备: {device_number}")
+ for prop in device_data['properties']:
+ print(f" {prop['name']}: {prop['value']}")
+
+if __name__ == '__main__':
+ get_device_data('D001')
+ get_all_devices()
+```
+
+### Java 示例
+
+```java
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.URI;
+import org.json.JSONObject;
+
+public class DeviceDataClient {
+ private static final String BASE_URL = "http://localhost:8080";
+
+ public static void getDeviceData(String deviceNumber) throws Exception {
+ String url = BASE_URL + "/api/device/data?deviceNumber=" + deviceNumber + "&mapped=true";
+
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(url))
+ .GET()
+ .build();
+
+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ JSONObject json = new JSONObject(response.body());
+ if (json.getInt("code") == 200) {
+ JSONObject data = json.getJSONObject("data");
+ System.out.println("设备号: " + data.getString("deviceNumber"));
+ data.getJSONArray("properties").forEach(prop -> {
+ JSONObject p = (JSONObject) prop;
+ System.out.println(" " + p.getString("name") + ": " + p.getString("value"));
+ });
+ }
+ }
+}
+```
+
+---
+
+## 📊 响应格式
+
+### 成功响应
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ // 实际数据内容
+ }
+}
+```
+
+### 错误响应
+
+```json
+{
+ "code": 400,
+ "message": "缺少必要参数: deviceNumber"
+}
+```
+
+### 状态码说明
+
+| 状态码 | 含义 | 说明 |
+|--------|------|------|
+| 200 | OK | 请求成功 |
+| 400 | Bad Request | 请求参数错误或缺少必需参数 |
+| 404 | Not Found | 资源不存在(如设备不存在) |
+| 429 | Too Many Requests | 请求过于频繁(触发限流) |
+| 500 | Internal Server Error | 服务器内部错误 |
+
+---
+
+## ⚠️ 错误处理
+
+### 常见错误及解决方案
+
+#### 错误1: 设备不存在
+
+**错误响应**:
+```json
+{
+ "code": 404,
+ "message": "设备 D001 未找到"
+}
+```
+
+**解决方案**:
+1. 调用 `/api/device/list` 检查可用设备
+2. 确认设备号是否正确
+3. 检查设备是否已连接到系统
+
+#### 错误2: 请求过于频繁
+
+**错误响应**:
+```json
+{
+ "code": 429,
+ "message": "请求过于频繁,请在 4500ms 后再试"
+}
+```
+
+**解决方案**:
+1. 等待提示的时间后重试
+2. 减少请求频率
+3. 使用 `/api/device/all` 批量获取以减少请求次数
+
+#### 错误3: 缺少必需参数
+
+**错误响应**:
+```json
+{
+ "code": 400,
+ "message": "缺少必要参数: deviceNumber"
+}
+```
+
+**解决方案**:
+1. 检查是否提供了所有必需参数
+2. 检查参数拼写是否正确
+3. 参考API文档确认参数格式
+
+---
+
+## 🚦 限流规则
+
+### 限流配置
+
+| 限流对象 | 时间窗口 | 最大请求数 | 说明 |
+|---------|---------|----------|------|
+| 单个设备 | 5秒 | 1次 | /api/device/data 端点 |
+| 全局设备 | 60秒 | 1次 | /api/device/all 端点 |
+
+### 限流示例
+
+```bash
+# 第1次请求成功
+curl "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+# 响应: 200 OK
+
+# 第2次请求(间隔 < 5秒)失败
+curl "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+# 响应: 429 Too Many Requests
+# "请求过于频繁,请在 4500ms 后再试"
+
+# 等待5秒后再次请求成功
+sleep 5
+curl "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+# 响应: 200 OK
+```
+
+### 避免限流的建议
+
+1. **批量查询**: 使用 `/api/device/all` 一次获取所有设备
+2. **缓存数据**: 本地存储数据,减少服务器查询
+3. **智能重试**: 收到429时,按提示时间重试
+4. **分散请求**: 查询不同设备而不是重复查询同一设备
+
+---
+
+## ❓ 常见问题
+
+### Q1: 映射格式和原始格式的区别是什么?
+
+**A**:
+- **原始格式**: `{"A": "50", "B": "25"}`(需要查表才能理解)
+- **映射格式**: `{"identifier": "A", "name": "脱水目标量", "value": "50"}`(自解释)
+
+**推荐使用映射格式**,便于理解和维护。
+
+### Q2: 如何定期获取最新数据?
+
+**A**: 建议使用轮询+缓存的策略:
+
+```javascript
+async function pollDeviceData(deviceNumber, interval = 10000) {
+ setInterval(async () => {
+ try {
+ const response = await fetch(
+ `http://localhost:8080/api/device/data?deviceNumber=${deviceNumber}&mapped=true`
+ );
+ const result = await response.json();
+
+ if (result.code === 200) {
+ console.log('最新数据:', result.data);
+ // 在这里处理数据
+ }
+ } catch (error) {
+ console.error('获取失败:', error);
+ }
+ }, interval);
+}
+
+// 每10秒获取一次数据
+pollDeviceData('D001', 10000);
+```
+
+### Q3: 如何处理网络连接失败?
+
+**A**:
+
+```python
+import requests
+from requests.adapters import HTTPAdapter
+from requests.packages.urllib3.util.retry import Retry
+
+def requests_retry_session(
+ retries=3,
+ backoff_factor=0.3,
+ status_forcelist=(500, 502, 504),
+ session=None,
+):
+ session = session or requests.Session()
+ retry = Retry(
+ total=retries,
+ read=retries,
+ connect=retries,
+ backoff_factor=backoff_factor,
+ status_forcelist=status_forcelist,
+ )
+ adapter = HTTPAdapter(max_retries=retry)
+ session.mount('http://', adapter)
+ session.mount('https://', adapter)
+ return session
+
+# 使用
+try:
+ response = requests_retry_session().get('http://localhost:8080/api/health')
+ print(response.json())
+except requests.RequestException as e:
+ print(f'连接失败: {e}')
+```
+
+### Q4: 数据更新的实时性如何保证?
+
+**A**:
+- 设备数据通过Socket实时接收
+- HTTP API返回的是缓存中的最新数据
+- 数据延迟通常 < 1秒
+- 可通过 `timestamp` 字段确认数据新鲜度
+
+### Q5: 支持CORS跨域请求吗?
+
+**A**: 是的,系统默认支持CORS,允许来自任何域的跨域请求。
+
+```javascript
+// 浏览器直接请求
+fetch('http://localhost:8080/api/device/list')
+ .then(r => r.json())
+ .then(data => console.log(data));
+```
+
+### Q6: API服务何时会不可用?
+
+**A**:
+- 服务故障(系统会自动记录日志)
+- 网络问题
+- 设备未连接
+
+使用 `/api/health` 进行健康检查来判断服务状态。
+
+---
+
+## 📞 技术支持
+
+如遇到任何问题,请提供以下信息联系技术支持:
+
+1. 错误信息截图
+2. 请求URL和参数
+3. 响应内容
+4. 系统日志文件
+5. 网络环境信息
+
+---
+
+## 📝 更新日志
+
+### v1.1.0 (2025-12-08)
+
+- ✅ 完整的属性映射支持(68个属性)
+- ✅ 单个和批量设备查询
+- ✅ 灵活的限流配置
+- ✅ CORS跨域支持
+- ✅ 详细的错误处理
+
+### v1.0.0 (2025-12-06)
+
+- 初始版本发布
+
+---
+
+**最后更新**: 2025-12-08
+**版本**: v1.1.0
+**状态**: 生产就绪 ✅
diff --git a/MQTT_INTEGRATION_GUIDE.md b/MQTT_INTEGRATION_GUIDE.md
new file mode 100644
index 0000000..0c35506
--- /dev/null
+++ b/MQTT_INTEGRATION_GUIDE.md
@@ -0,0 +1,440 @@
+# 透析通讯服务 - MQTT 集成文档
+
+## 概述
+
+本文档说明如何将透析通讯服务与第三方 MQTT 服务集成,实现透析机数据的实时上传和推送。
+
+---
+
+## 1. 系统架构
+
+```
+透析机设备
+ ↓ (Socket 连接)
+Socket 服务器 (端口 10961)
+ ↓ (处理数据)
+数据转换模块
+ ↓ (发布)
+MQTT Broker
+ ↓ (订阅)
+第三方服务/应用
+```
+
+---
+
+## 2. MQTT 配置
+
+### 2.1 配置文件位置
+配置文件 `mqtt.json` 必须与程序在同一目录。
+
+### 2.2 配置参数
+
+创建或编辑 `mqtt.json`:
+
+```json
+{
+ "enabled": true,
+ "brokerUrl": "mqtt.example.com",
+ "port": 1883,
+ "username": "your_username",
+ "password": "your_password",
+ "reconnectPeriod": 5000,
+ "defaultTopicPrefix": "your_topic_prefix"
+}
+```
+
+| 参数 | 类型 | 说明 | 示例 |
+|------|------|------|------|
+| `enabled` | boolean | 是否启用 MQTT | `true` / `false` |
+| `brokerUrl` | string | MQTT Broker 地址 | `mqtt.ihemodialysis.com` |
+| `port` | number | MQTT Broker 端口 | `1883` (不加密) 或 `8883` (TLS) |
+| `username` | string | 连接用户名 | `data` |
+| `password` | string | 连接密码 | `data#2018` |
+| `reconnectPeriod` | number | 断线重连间隔(毫秒) | `5000` |
+| `defaultTopicPrefix` | string | MQTT 主题前缀 | `touxiji` |
+
+---
+
+## 3. 数据发布
+
+### 3.1 发布主题格式
+
+```
+{defaultTopicPrefix}/{deviceNumber}
+```
+
+**示例:**
+- 前缀:`touxiji`
+- 设备号:`D001`
+- 完整主题:`touxiji/D001`
+
+### 3.2 发布消息格式
+
+每次设备上报数据时,服务器向 MQTT 发布以下格式的消息(JSON):
+
+```json
+{
+ "n": "D001",
+ "parameter1": "value1",
+ "parameter2": "value2",
+ "...": "...",
+ "deviceId": "192.168.1.100:54321",
+ "timestamp": "2025-12-03T10:30:45.123Z"
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `n` | string | 透析机设备号 |
+| `deviceId` | string | 设备的 Socket 连接地址和端口 |
+| `timestamp` | string | 数据上报时间戳 (ISO 8601 格式) |
+| 其他字段 | mixed | 来自设备的具体透析参数 |
+
+### 3.3 发布质量等级 (QoS)
+
+- 默认 QoS 级别:**1** (至少一次投递)
+- 确保消息不会丢失,适合关键数据
+
+### 3.4 发布频率
+
+- 触发方式:**事件驱动** (设备发送数据时立即发布)
+- 无固定发布间隔
+- 取决于设备的数据上报频率
+
+---
+
+## 4. 连接管理
+
+### 4.1 连接过程
+
+1. 程序启动时读取 `mqtt.json` 配置
+2. 若 `enabled: true`,则自动连接到 MQTT Broker
+3. 连接成功后开始接收设备数据
+4. 每接收到一条完整设备消息,立即发送到 MQTT
+
+### 4.2 断线重连
+
+- **自动重连**:是的
+- **重连间隔**:由 `reconnectPeriod` 参数控制(默认 5秒)
+- **重连策略**:指数退避
+
+### 4.3 心跳保活
+
+- MQTT 连接采用标准 MQTT 心跳机制
+- 确保连接持续稳定
+
+### 4.4 断开连接
+
+- 关闭程序时自动断开 MQTT 连接
+- 设备断开时不影响 MQTT 连接
+
+---
+
+## 5. 日志和监控
+
+### 5.1 日志输出
+
+程序会记录以下信息:
+
+```
+✅ MQTT 连接成功: mqtt.ihemodialysis.com:62283 (用户: data)
+📡 已通过 MQTT 发送数据到 touxiji/D001
+📤 MQTT 已发布到 touxiji/D001
+🔌 MQTT 连接断开
+🔄 MQTT 正在重连...
+❌ MQTT 错误: ...
+```
+
+### 5.2 日志位置
+
+- 日志存储在 `logs/` 目录
+- 用于调试和问题排查
+
+---
+
+## 6. 第三方服务接入
+
+### 6.1 订阅数据
+
+第三方服务/应用需要:
+
+1. **连接到同一个 MQTT Broker**
+ ```
+ 地址:mqtt.ihemodialysis.com
+ 端口:62283
+ 用户名:data
+ 密码:data#2018
+ ```
+
+2. **订阅相关主题**
+ ```
+ 订阅模式:touxiji/+ (订阅所有设备)
+ 或
+ 订阅模式:touxiji/D001 (订阅特定设备)
+ ```
+
+3. **处理接收的消息**
+ ```json
+ {
+ "n": "D001",
+ "parameter1": "value1",
+ "deviceId": "192.168.1.100:54321",
+ "timestamp": "2025-12-03T10:30:45.123Z"
+ }
+ ```
+
+### 6.2 Python 示例
+
+```python
+import paho.mqtt.client as mqtt
+import json
+
+def on_connect(client, userdata, flags, rc):
+ if rc == 0:
+ print("连接成功")
+ # 订阅所有透析机数据
+ client.subscribe("touxiji/+")
+ else:
+ print(f"连接失败: {rc}")
+
+def on_message(client, userdata, msg):
+ try:
+ data = json.loads(msg.payload.decode())
+ print(f"收到数据来自 {data['n']}: {data}")
+ # 处理数据逻辑
+ except Exception as e:
+ print(f"解析错误: {e}")
+
+client = mqtt.Client()
+client.on_connect = on_connect
+client.on_message = on_message
+
+client.username_pw_set("data", "data#2018")
+client.connect("mqtt.ihemodialysis.com", 62283, keepalive=60)
+
+client.loop_forever()
+```
+
+### 6.3 Node.js 示例
+
+```javascript
+const mqtt = require('mqtt');
+
+const options = {
+ host: 'mqtt.ihemodialysis.com',
+ port: 62283,
+ username: 'data',
+ password: 'data#2018'
+};
+
+const client = mqtt.connect(options);
+
+client.on('connect', () => {
+ console.log('连接成功');
+ client.subscribe('touxiji/+', (err) => {
+ if (!err) {
+ console.log('订阅成功');
+ }
+ });
+});
+
+client.on('message', (topic, message) => {
+ try {
+ const data = JSON.parse(message.toString());
+ console.log(`收到数据来自 ${data.n}:`, data);
+ // 处理数据逻辑
+ } catch (e) {
+ console.error('解析错误:', e);
+ }
+});
+
+client.on('error', (err) => {
+ console.error('连接错误:', err);
+});
+```
+
+### 6.4 Java 示例
+
+```java
+import org.eclipse.paho.client.mqttv3.*;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+public class MqttClient {
+ public static void main(String[] args) {
+ String brokerUrl = "tcp://mqtt.ihemodialysis.com:62283";
+ String clientId = "third_party_service_" + System.currentTimeMillis();
+
+ try {
+ MqttClient client = new MqttClient(brokerUrl, clientId, new MemoryPersistence());
+ MqttConnectOptions options = new MqttConnectOptions();
+ options.setUserName("data");
+ options.setPassword("data#2018".toCharArray());
+ options.setAutomaticReconnect(true);
+
+ client.setCallback(new MqttCallback() {
+ @Override
+ public void connectionLost(Throwable cause) {
+ System.out.println("连接断开");
+ }
+
+ @Override
+ public void messageArrived(String topic, MqttMessage message) {
+ try {
+ String payload = new String(message.getPayload());
+ Gson gson = new Gson();
+ JsonObject data = gson.fromJson(payload, JsonObject.class);
+ System.out.println("收到数据: " + data);
+ // 处理数据逻辑
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void deliveryComplete(IMqttDeliveryToken token) {
+ }
+ });
+
+ client.connect(options);
+ client.subscribe("touxiji/+");
+
+ } catch (MqttException e) {
+ e.printStackTrace();
+ }
+ }
+}
+```
+
+---
+
+## 7. 故障排查
+
+### 7.1 无法连接到 MQTT
+
+**问题:** `连接失败` 或 `连接超时`
+
+**排查步骤:**
+1. 检查 Broker 地址和端口是否正确
+2. 检查网络连接
+3. 检查防火墙规则
+4. 验证用户名和密码
+
+**日志示例:**
+```
+❌ MQTT 错误: getaddrinfo ENOTFOUND mqtt.example.com
+```
+
+### 7.2 认证失败
+
+**问题:** `认证错误` 或 `密码错误`
+
+**排查步骤:**
+1. 验证 `mqtt.json` 中的用户名和密码
+2. 检查密码中是否有特殊字符(需要正确转义)
+3. 重新启动程序
+
+**日志示例:**
+```
+❌ MQTT 错误: Not authorized
+```
+
+### 7.3 数据未发送
+
+**问题:** 设备已连接,但 MQTT 中看不到数据
+
+**排查步骤:**
+1. 检查 `mqtt.json` 中 `enabled` 是否为 `true`
+2. 确认设备确实在发送数据(查看设备连接日志)
+3. 验证 MQTT 主题前缀是否正确
+4. 查看 MQTT Broker 的订阅情况
+
+**日志示例:**
+```
+📡 已通过 MQTT 发送数据到 touxiji/D001
+```
+
+### 7.4 频繁断线重连
+
+**问题:** MQTT 连接不稳定,频繁断开
+
+**排查步骤:**
+1. 检查网络稳定性
+2. 增加 `reconnectPeriod` 值
+3. 检查 Broker 服务器状态
+4. 查看 Broker 日志
+
+---
+
+## 8. 安全建议
+
+### 8.1 用户名和密码
+- ✅ 使用强密码
+- ❌ 不要在代码中硬编码敏感信息
+- ✅ 使用环境变量管理配置
+
+### 8.2 网络安全
+- 如果需要加密通信,使用 TLS/SSL(端口 8883)
+- 限制 MQTT Broker 访问 IP 范围
+
+### 8.3 消息安全
+- QoS 1 确保消息投递
+- 实现消息校验机制(可选)
+
+---
+
+## 9. 性能指标
+
+| 指标 | 值 |
+|------|-----|
+| 最大设备连接数 | 无限制(取决于硬件) |
+| 单个消息大小 | ≤ 1MB |
+| 发送延迟 | < 100ms(通常) |
+| 重连间隔 | 可配置(默认 5秒) |
+| 心跳间隔 | 60秒 |
+
+---
+
+## 10. 常见问题 (FAQ)
+
+### Q1: 能否修改 MQTT 主题格式?
+
+**A:** 可以。编辑 `mqtt.json` 中的 `defaultTopicPrefix` 字段。
+
+```json
+{
+ "defaultTopicPrefix": "your_custom_prefix"
+}
+```
+
+主题将变为:`your_custom_prefix/{deviceNumber}`
+
+### Q2: 如果 MQTT 和阿里云同时启用会怎样?
+
+**A:** 两个都会工作。MQTT 用于实时数据推送,阿里云用于备份存储。
+
+### Q3: 设备掉线后数据会保留吗?
+
+**A:** 不会。数据是实时发送的,掉线数据会丢失。
+
+### Q4: 支持 QoS 0 吗?
+
+**A:** 不支持。服务器固定使用 QoS 1 以确保可靠性。
+
+### Q5: 如何处理同一主题的多个订阅者?
+
+**A:** MQTT Broker 会自动向所有订阅者发送消息,无需额外配置。
+
+---
+
+## 11. 联系方式
+
+如有技术问题,请查看程序日志或联系系统管理员。
+
+日志位置:`logs/` 目录
+
+---
+
+**文档版本:** 1.0
+**更新时间:** 2025年12月
+**适用程序版本:** 1.0+
diff --git a/Postman_Collection.json b/Postman_Collection.json
new file mode 100644
index 0000000..4523b03
--- /dev/null
+++ b/Postman_Collection.json
@@ -0,0 +1,257 @@
+{
+ "info": {
+ "_postman_id": "dialysis-iot-api-collection",
+ "name": "透析机IoT数据服务 API",
+ "description": "透析机设备实时数据查询和管理接口集合",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "系统管理",
+ "item": [
+ {
+ "name": "健康检查",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/health",
+ "host": ["{{base_url}}"],
+ "path": ["api", "health"]
+ },
+ "description": "检查服务器是否正常运行"
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "设备数据查询",
+ "item": [
+ {
+ "name": "获取单个设备数据(原始格式)",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/device/data?deviceNumber=D001",
+ "host": ["{{base_url}}"],
+ "path": ["api", "device", "data"],
+ "query": [
+ {
+ "key": "deviceNumber",
+ "value": "D001"
+ }
+ ]
+ },
+ "description": "获取指定设备的原始格式数据"
+ },
+ "response": []
+ },
+ {
+ "name": "获取单个设备数据(映射格式)",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/device/data?deviceNumber=D001&mapped=true",
+ "host": ["{{base_url}}"],
+ "path": ["api", "device", "data"],
+ "query": [
+ {
+ "key": "deviceNumber",
+ "value": "D001"
+ },
+ {
+ "key": "mapped",
+ "value": "true"
+ }
+ ]
+ },
+ "description": "获取指定设备的映射格式数据(推荐使用)"
+ },
+ "response": []
+ },
+ {
+ "name": "获取所有设备数据(映射格式)",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/device/all?mapped=true",
+ "host": ["{{base_url}}"],
+ "path": ["api", "device", "all"],
+ "query": [
+ {
+ "key": "mapped",
+ "value": "true"
+ }
+ ]
+ },
+ "description": "批量获取所有设备的映射格式数据"
+ },
+ "response": []
+ },
+ {
+ "name": "获取设备列表",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/device/list",
+ "host": ["{{base_url}}"],
+ "path": ["api", "device", "list"]
+ },
+ "description": "获取所有已连接设备的摘要信息"
+ },
+ "response": []
+ },
+ {
+ "name": "获取超时设备",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/device/idle?timeout=300000",
+ "host": ["{{base_url}}"],
+ "path": ["api", "device", "idle"],
+ "query": [
+ {
+ "key": "timeout",
+ "value": "300000",
+ "description": "超时时间(毫秒),默认300000(5分钟)"
+ }
+ ]
+ },
+ "description": "获取超过指定时间未更新的设备列表"
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "统计信息",
+ "item": [
+ {
+ "name": "获取缓存统计",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/cache/stats",
+ "host": ["{{base_url}}"],
+ "path": ["api", "cache", "stats"]
+ },
+ "description": "获取数据缓存的统计信息"
+ },
+ "response": []
+ },
+ {
+ "name": "获取限流统计",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/ratelimit/stats",
+ "host": ["{{base_url}}"],
+ "path": ["api", "ratelimit", "stats"]
+ },
+ "description": "获取请求限流的统计信息"
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "缓存管理",
+ "item": [
+ {
+ "name": "清空缓存",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/cache/clear",
+ "host": ["{{base_url}}"],
+ "path": ["api", "cache", "clear"]
+ },
+ "description": "清空所有缓存的设备数据(需谨慎使用)"
+ },
+ "response": []
+ },
+ {
+ "name": "清空限流记录",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/ratelimit/clear",
+ "host": ["{{base_url}}"],
+ "path": ["api", "ratelimit", "clear"]
+ },
+ "description": "清空所有限流记录"
+ },
+ "response": []
+ }
+ ]
+ }
+ ],
+ "variable": [
+ {
+ "key": "base_url",
+ "value": "http://localhost:8080",
+ "type": "string",
+ "description": "API服务器基础地址"
+ }
+ ]
+}
diff --git a/QUICK_START.md b/QUICK_START.md
new file mode 100644
index 0000000..f27af87
--- /dev/null
+++ b/QUICK_START.md
@@ -0,0 +1,488 @@
+# 透析机IoT数据服务 - 快速入门指南
+
+**目标受众**: 第三方开发者
+**预计时间**: 5-10分钟
+
+---
+
+## 🎯 30秒快速开始
+
+### 1. 验证服务可用
+
+```bash
+curl http://localhost:8080/api/health
+```
+
+**预期输出**:
+```json
+{"code":200,"message":"success","data":{"status":"ok"}}
+```
+
+### 2. 获取设备列表
+
+```bash
+curl "http://localhost:8080/api/device/list"
+```
+
+### 3. 查询设备数据
+
+```bash
+curl "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+```
+
+---
+
+## 📦 完整集成示例
+
+### 场景:监控透析机实时数据
+
+#### 方案A:JavaScript/Node.js(推荐)
+
+```javascript
+const http = require('http');
+
+class DeviceMonitor {
+ constructor(serverUrl = 'http://localhost:8080') {
+ this.serverUrl = serverUrl;
+ }
+
+ // 获取设备数据
+ async getDeviceData(deviceNumber) {
+ return this.request(
+ `/api/device/data?deviceNumber=${deviceNumber}&mapped=true`
+ );
+ }
+
+ // 获取所有设备
+ async getAllDevices() {
+ return this.request('/api/device/all?mapped=true');
+ }
+
+ // 获取设备列表
+ async getDeviceList() {
+ return this.request('/api/device/list');
+ }
+
+ // 发起HTTP请求
+ request(path) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(path, this.serverUrl);
+ const http = require('http');
+
+ http.get(url, (res) => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => {
+ try {
+ resolve(JSON.parse(data));
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }).on('error', reject);
+ });
+ }
+}
+
+// 使用示例
+async function main() {
+ const monitor = new DeviceMonitor();
+
+ try {
+ // 获取设备列表
+ console.log('正在获取设备列表...');
+ const devices = await monitor.getDeviceList();
+ console.log(`找到 ${devices.data.count} 个设备`);
+
+ // 查询每个设备
+ for (const device of devices.data.devices) {
+ console.log(`\n正在查询设备: ${device.deviceNumber}`);
+ const data = await monitor.getDeviceData(device.deviceNumber);
+
+ if (data.code === 200) {
+ const props = data.data.properties;
+ props.forEach(prop => {
+ console.log(` ${prop.name}: ${prop.value}`);
+ });
+ } else if (data.code === 429) {
+ console.log(` [限流] ${data.message}`);
+ }
+ }
+ } catch (error) {
+ console.error('错误:', error);
+ }
+}
+
+main();
+```
+
+#### 方案B:Python(适合数据分析)
+
+```python
+import requests
+import json
+import time
+from datetime import datetime
+
+class DeviceMonitor:
+ def __init__(self, server_url='http://localhost:8080'):
+ self.server_url = server_url
+ self.session = requests.Session()
+ self.session.headers.update({'Accept': 'application/json'})
+
+ def get_device_data(self, device_number):
+ """获取单个设备数据"""
+ url = f'{self.server_url}/api/device/data'
+ params = {
+ 'deviceNumber': device_number,
+ 'mapped': 'true'
+ }
+ return self._request(url, params)
+
+ def get_all_devices(self):
+ """获取所有设备数据"""
+ url = f'{self.server_url}/api/device/all'
+ params = {'mapped': 'true'}
+ return self._request(url, params)
+
+ def get_device_list(self):
+ """获取设备列表"""
+ url = f'{self.server_url}/api/device/list'
+ return self._request(url)
+
+ def _request(self, url, params=None):
+ """发起HTTP请求"""
+ try:
+ response = self.session.get(url, params=params, timeout=5)
+ return response.json()
+ except requests.RequestException as e:
+ print(f'请求失败: {e}')
+ return None
+
+# 使用示例
+if __name__ == '__main__':
+ monitor = DeviceMonitor()
+
+ # 获取设备列表
+ print('获取设备列表...')
+ result = monitor.get_device_list()
+
+ if result and result['code'] == 200:
+ print(f"找到 {result['data']['count']} 个设备\n")
+
+ # 查询每个设备的数据
+ for device in result['data']['devices']:
+ device_number = device['deviceNumber']
+ print(f"设备: {device_number}")
+
+ data = monitor.get_device_data(device_number)
+ if data and data['code'] == 200:
+ for prop in data['data']['properties']:
+ print(f" {prop['name']}: {prop['value']}")
+ elif data and data['code'] == 429:
+ print(f" [限流] {data['message']}")
+ print()
+```
+
+#### 方案C:cURL(快速测试)
+
+```bash
+#!/bin/bash
+
+# 服务器地址
+SERVER="http://localhost:8080"
+
+echo "=== 健康检查 ==="
+curl -s "$SERVER/api/health" | jq .
+
+echo -e "\n=== 设备列表 ==="
+curl -s "$SERVER/api/device/list" | jq '.data.devices'
+
+echo -e "\n=== 第一个设备的详细数据 ==="
+DEVICE_NUMBER=$(curl -s "$SERVER/api/device/list" | jq -r '.data.devices[0].deviceNumber')
+echo "查询设备: $DEVICE_NUMBER"
+curl -s "$SERVER/api/device/data?deviceNumber=$DEVICE_NUMBER&mapped=true" | jq '.data.properties'
+
+echo -e "\n=== 所有设备摘要 ==="
+curl -s "$SERVER/api/device/all?mapped=true" | jq '.data | keys'
+```
+
+运行脚本:
+```bash
+chmod +x monitor.sh
+./monitor.sh
+```
+
+---
+
+## ⚠️ 常见陷阱
+
+### 陷阱1:触发限流
+
+❌ **错误做法** - 连续快速请求:
+```bash
+for i in {1..5}; do
+ curl "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+done
+# 后4个请求会返回 429 Too Many Requests
+```
+
+✅ **正确做法** - 使用批量查询:
+```bash
+# 一次获取所有设备,而不是逐个查询
+curl "http://localhost:8080/api/device/all?mapped=true"
+```
+
+或添加延迟:
+```bash
+for i in {1..5}; do
+ curl "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+ sleep 6 # 等待6秒
+done
+```
+
+### 陷阱2:忘记映射设备号
+
+❌ **容易困惑**:
+```json
+{"A": "50", "B": "25", "C": "10"} // 原始格式,看不懂
+```
+
+✅ **清晰易懂**:
+```json
+[
+ {"name": "脱水目标量", "value": "50"},
+ {"name": "脱水量", "value": "25"},
+ {"name": "脱水速率", "value": "10"}
+]
+```
+
+**永远使用**: `?mapped=true`
+
+### 陷阱3:处理中文编码
+
+❌ **可能乱码**:
+```python
+response = requests.get(url)
+data = response.text # 如果没有正确设置编码
+```
+
+✅ **正确方式**:
+```python
+response = requests.get(url)
+response.encoding = 'utf-8' # 明确指定编码
+data = response.json() # JSON自动处理编码
+```
+
+### 陷阱4:没有错误处理
+
+❌ **容易崩溃**:
+```javascript
+const data = await fetch(url).then(r => r.json());
+console.log(data.data.properties[0].name); // 如果请求失败,直接崩溃
+```
+
+✅ **健壮的代码**:
+```javascript
+try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const data = await response.json();
+ if (data.code !== 200) {
+ throw new Error(data.message);
+ }
+ return data.data;
+} catch (error) {
+ console.error('获取数据失败:', error);
+ return null;
+}
+```
+
+---
+
+## 📊 属性参考表
+
+系统支持68个设备属性,以下是常用属性示例:
+
+| 标识符 | 中文名称 | 说明 | 数据类型 |
+|--------|---------|------|---------|
+| A | 脱水目标量 | 目标脱水量 | float |
+| B | 脱水量 | 当前已脱水量 | float |
+| C | 脱水速率 | 脱水速率 | float |
+| D | 置换液输入速度 | mL/min | float |
+| E | 置换液输出速度 | mL/min | float |
+| R | 收缩压下限 | mmHg | int |
+| S | 收缩压上限 | mmHg | int |
+| mb | 脉搏-德朗 | 心率 | int |
+| deviceName | 设备名称 | 设备标识 | string |
+| IPAddress | IP地址 | 设备IP | string |
+
+更多属性请查看完整API文档。
+
+---
+
+## 🔍 调试技巧
+
+### 1. 使用Postman或Insomnia
+
+**安装**: 下载 [Postman](https://www.postman.com/downloads/) 或 [Insomnia](https://insomnia.rest/download)
+
+**步骤**:
+1. 创建新请求
+2. URL: `http://localhost:8080/api/device/list`
+3. 点击 Send
+4. 查看响应
+
+### 2. 使用在线工具测试
+
+访问 [ReqBin](https://reqbin.com/) 或 [REST Client](https://www.restclient.com/):
+
+```
+GET http://localhost:8080/api/device/list HTTP/1.1
+Host: localhost:8080
+Accept: application/json
+```
+
+### 3. 浏览器控制台测试
+
+在浏览器console执行:
+```javascript
+fetch('http://localhost:8080/api/device/list')
+ .then(r => r.json())
+ .then(d => console.table(d.data.devices))
+```
+
+### 4. 性能测试
+
+```bash
+# 使用 ab (Apache Bench)
+ab -n 100 -c 10 http://localhost:8080/api/health
+
+# 使用 wrk
+wrk -t12 -c400 -d30s http://localhost:8080/api/health
+```
+
+---
+
+## 🚀 进阶用法
+
+### 定期监控和告警
+
+```javascript
+class DeviceAlertSystem {
+ constructor(serverUrl, checkInterval = 30000) {
+ this.serverUrl = serverUrl;
+ this.checkInterval = checkInterval;
+ this.thresholds = {
+ maxDehydrationRate: 50, // 最大脱水速率
+ minBloodPressure: 80, // 最小血压
+ };
+ }
+
+ async start() {
+ console.log('监控系统已启动');
+ setInterval(() => this.check(), this.checkInterval);
+ }
+
+ async check() {
+ try {
+ const response = await fetch(`${this.serverUrl}/api/device/all?mapped=true`);
+ const data = await response.json();
+
+ if (data.code === 200) {
+ Object.entries(data.data.data).forEach(([deviceId, deviceData]) => {
+ this.checkThresholds(deviceId, deviceData.properties);
+ });
+ }
+ } catch (error) {
+ console.error('检查失败:', error);
+ }
+ }
+
+ checkThresholds(deviceId, properties) {
+ properties.forEach(prop => {
+ // 检查脱水速率是否过高
+ if (prop.name === '脱水速率' && parseFloat(prop.value) > this.thresholds.maxDehydrationRate) {
+ this.alert(`设备 ${deviceId} 脱水速率过高: ${prop.value}`);
+ }
+
+ // 检查血压是否过低
+ if (prop.name === '收缩压' && parseFloat(prop.value) < this.thresholds.minBloodPressure) {
+ this.alert(`设备 ${deviceId} 血压过低: ${prop.value}`);
+ }
+ });
+ }
+
+ alert(message) {
+ console.warn(`⚠️ 告警: ${message}`);
+ // 这里可以发送邮件、短信等
+ }
+}
+
+// 启动监控
+const system = new DeviceAlertSystem('http://localhost:8080', 30000);
+system.start();
+```
+
+### 数据导出到CSV
+
+```python
+import requests
+import csv
+from datetime import datetime
+
+def export_device_data_to_csv(server_url, filename):
+ """导出所有设备数据到CSV文件"""
+
+ response = requests.get(f'{server_url}/api/device/all?mapped=true')
+ data = response.json()
+
+ with open(filename, 'w', newline='', encoding='utf-8') as f:
+ writer = csv.writer(f)
+
+ # 写表头
+ writer.writerow(['设备号', '时间戳', '属性名', '值'])
+
+ # 写数据
+ for device_id, device_data in data['data']['data'].items():
+ timestamp = device_data['timestamp']
+ for prop in device_data['properties']:
+ writer.writerow([
+ device_id,
+ timestamp,
+ prop['name'],
+ prop['value']
+ ])
+
+ print(f'数据已导出到: {filename}')
+
+# 使用
+export_device_data_to_csv('http://localhost:8080', 'devices.csv')
+```
+
+---
+
+## 📞 需要帮助?
+
+### 检查清单
+
+- [ ] 服务器地址和端口正确
+- [ ] 防火墙允许访问8080端口
+- [ ] 网络连接正常(ping测试)
+- [ ] 已调用 `/api/health` 验证服务
+- [ ] 请求格式正确(参数名和类型)
+- [ ] 没有触发限流(检查 `/api/ratelimit/stats`)
+
+### 获取支持
+
+1. 查看完整文档: `HTTP_API_INTEGRATION_GUIDE.md`
+2. 查看OpenAPI规范: `openapi.json`
+3. 检查服务日志
+4. 联系技术支持团队
+
+---
+
+**版本**: v1.1.0
+**最后更新**: 2025-12-08
diff --git a/RATE_LIMIT_GUIDE.md b/RATE_LIMIT_GUIDE.md
new file mode 100644
index 0000000..677e8ba
--- /dev/null
+++ b/RATE_LIMIT_GUIDE.md
@@ -0,0 +1,494 @@
+# HTTP 限流功能指南
+
+## 概述
+
+为了保护服务器性能和防止恶意请求,系统实现了基于设备号的请求限流功能。相同的设备号在指定时间间隔内只能发起一次请求。
+
+---
+
+## 配置
+
+### httpConfig.json
+
+```json
+{
+ "enabled": true,
+ "port": 8080,
+ "host": "0.0.0.0",
+ "cors": {
+ "enabled": true,
+ "allowOrigin": "*"
+ },
+ "rateLimit": {
+ "enabled": true,
+ "interval": 5000
+ }
+}
+```
+
+| 参数 | 说明 | 默认值 |
+|------|------|--------|
+| `rateLimit.enabled` | 是否启用限流 | `true` |
+| `rateLimit.interval` | 限流时间间隔(毫秒) | `5000` (5秒) |
+
+### 常见配置
+
+#### 无限流限制
+```json
+{
+ "rateLimit": {
+ "enabled": false
+ }
+}
+```
+
+#### 严格限流(10秒)
+```json
+{
+ "rateLimit": {
+ "enabled": true,
+ "interval": 10000
+ }
+}
+```
+
+#### 宽松限流(2秒)
+```json
+{
+ "rateLimit": {
+ "enabled": true,
+ "interval": 2000
+ }
+}
+```
+
+---
+
+## 工作原理
+
+### 限流流程
+
+```
+客户端请求设备 D001
+ ↓
+检查 D001 的最后请求时间
+ ↓
+是否超过 5 秒?
+ ├─ 是 → 允许请求,返回 200/404
+ └─ 否 → 拒绝请求,返回 429 (Too Many Requests)
+```
+
+### 关键特点
+
+- ✅ **基于设备号** - 限流对象是设备号,不同设备独立计算
+- ✅ **固定间隔** - 同一设备在指定间隔内只允许一次请求
+- ✅ **自动重置** - 超过间隔时间后自动解除限制
+- ✅ **独立计数** - 每个设备的请求次数独立记录
+
+---
+
+## API 端点
+
+### 1. 获取指定设备数据(带限流)
+
+```
+GET /api/device/data?deviceNumber=D001
+```
+
+**成功响应 (HTTP 200/404):**
+```json
+{
+ "code": 0,
+ "message": "success",
+ "data": {
+ "deviceNumber": "D001",
+ "data": {...}
+ }
+}
+```
+
+**被限流响应 (HTTP 429):**
+```json
+{
+ "code": 429,
+ "message": "请求过于频繁,请在 3245ms 后再试",
+ "timestamp": "2025-12-05T10:30:45.234Z"
+}
+```
+
+### 2. 获取限流统计
+
+```
+GET /api/ratelimit/stats
+```
+
+**响应示例:**
+```json
+{
+ "code": 0,
+ "message": "success",
+ "data": {
+ "enabled": true,
+ "interval": 5000,
+ "totalIdentifiers": 3,
+ "records": {
+ "D001": {
+ "lastRequestTime": 1734000645123,
+ "count": 5
+ },
+ "D002": {
+ "lastRequestTime": 1734000640000,
+ "count": 3
+ }
+ }
+ },
+ "timestamp": "2025-12-05T10:30:45.234Z"
+}
+```
+
+| 字段 | 说明 |
+|------|------|
+| `enabled` | 限流是否启用 |
+| `interval` | 限流间隔(毫秒) |
+| `totalIdentifiers` | 记录的设备总数 |
+| `records` | 详细的限流记录 |
+
+### 3. 清空限流记录
+
+```
+POST /api/ratelimit/clear
+```
+
+**响应示例:**
+```json
+{
+ "code": 0,
+ "message": "success",
+ "data": {
+ "message": "限流记录已清空"
+ },
+ "timestamp": "2025-12-05T10:30:45.234Z"
+}
+```
+
+---
+
+## 测试
+
+### 运行基础限流测试
+
+```bash
+node rateLimitTest.js
+```
+
+这会执行 9 个测试用例,验证:
+1. ✅ 获取限流配置
+2. ✅ 清空限流记录
+3. ✅ 首次请求成功
+4. ✅ 立即重复请求被限流
+5. ✅ 部分间隔后仍被限流
+6. ✅ 完整间隔后请求成功
+7. ✅ 不同设备限流独立
+8. ✅ 获取限流统计
+9. ✅ 清空后可立即请求
+
+### 运行压力测试
+
+```bash
+node rateLimitTest.js stress
+```
+
+这会快速发送 100 个请求到同一设备,展示限流效果。
+
+### 测试输出示例
+
+```
+🚀 开始测试限流功能...
+
+============================================================
+
+【测试 1】获取限流配置信息
+✅ API 文档获取成功
+ 限流启用: true
+ 限流间隔: 5000ms
+
+【测试 3】首次请求设备 D001(应该成功)
+状态: 404
+✅ 首次请求成功 (设备不存在返回 404,但不受限流限制)
+
+【测试 4】立即再次请求设备 D001(应该被限流)
+状态: 429
+✅ 请求被限流 (HTTP 429)
+ 错误信息: 请求过于频繁,请在 4987ms 后再试
+
+【测试 6】再等待 3 秒后请求设备 D001(总计 6s,应该成功)
+状态: 404
+✅ 限流已解除,请求成功
+
+============================================================
+✅ 限流功能测试完成
+```
+
+---
+
+## 使用示例
+
+### JavaScript/Node.js
+
+```javascript
+const http = require('http');
+
+function queryDevice(deviceNumber, retryInterval = 5000) {
+ return new Promise((resolve, reject) => {
+ const url = new URL(`http://localhost:8080/api/device/data?deviceNumber=${deviceNumber}`);
+
+ const req = http.get(url, (res) => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => {
+ const result = JSON.parse(data);
+
+ if (res.statusCode === 429) {
+ // 被限流,解析剩余等待时间
+ const match = result.message.match(/(\d+)ms/);
+ const remainingTime = match ? parseInt(match[1]) : retryInterval;
+
+ console.log(`限流中,${remainingTime}ms 后重试...`);
+ setTimeout(() => {
+ queryDevice(deviceNumber, retryInterval)
+ .then(resolve)
+ .catch(reject);
+ }, remainingTime);
+ } else {
+ resolve(result);
+ }
+ });
+ });
+
+ req.on('error', reject);
+ });
+}
+
+// 使用示例
+queryDevice('D001')
+ .then(result => console.log('设备数据:', result.data))
+ .catch(err => console.error('查询失败:', err));
+```
+
+### Python
+
+```python
+import requests
+import time
+
+def query_device(device_number, retry_limit=3):
+ """查询设备数据,自动处理限流重试"""
+
+ url = 'http://localhost:8080/api/device/data'
+ params = {'deviceNumber': device_number}
+
+ retry_count = 0
+ while retry_count < retry_limit:
+ try:
+ response = requests.get(url, params=params)
+
+ if response.status_code == 429:
+ # 被限流,解析等待时间
+ message = response.json().get('message', '')
+ import re
+ match = re.search(r'(\d+)ms', message)
+ wait_time = int(match.group(1)) / 1000 if match else 5
+
+ print(f'限流中,{wait_time}s 后重试...')
+ time.sleep(wait_time)
+ retry_count += 1
+ continue
+
+ # 成功响应
+ data = response.json()
+ return data.get('data', {}).get('data', {})
+
+ except Exception as e:
+ print(f'请求失败: {e}')
+ return None
+
+ print('重试次数已用尽')
+ return None
+
+# 使用示例
+device_data = query_device('D001')
+if device_data:
+ print('设备数据:', device_data)
+else:
+ print('获取设备数据失败')
+```
+
+### cURL
+
+```bash
+# 基础请求
+curl "http://localhost:8080/api/device/data?deviceNumber=D001"
+
+# 快速重复请求(会被限流)
+for i in {1..5}; do
+ curl "http://localhost:8080/api/device/data?deviceNumber=D001"
+ echo "请求 $i 完成"
+done
+
+# 查看限流状态
+curl "http://localhost:8080/api/ratelimit/stats"
+
+# 清空限流记录
+curl -X POST "http://localhost:8080/api/ratelimit/clear"
+```
+
+---
+
+## 生产环境建议
+
+### 1. 合理配置限流间隔
+
+```json
+{
+ "rateLimit": {
+ "interval": 10000 // 生产环境建议 10 秒
+ }
+}
+```
+
+### 2. 监控限流情况
+
+定期检查限流统计,了解请求模式:
+
+```bash
+# 每 10 秒检查一次限流状态
+while true; do
+ curl "http://localhost:8080/api/ratelimit/stats" | jq .
+ sleep 10
+done
+```
+
+### 3. 应用端重试策略
+
+应用端应实现指数退避重试:
+
+```javascript
+async function queryWithRetry(deviceNumber, maxRetries = 3) {
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ const response = await fetch(
+ `http://localhost:8080/api/device/data?deviceNumber=${deviceNumber}`
+ );
+
+ if (response.status === 429) {
+ const data = await response.json();
+ const message = data.message;
+ const match = message.match(/(\d+)ms/);
+ const waitTime = match ? parseInt(match[1]) : 5000;
+
+ // 等待后重试
+ await new Promise(r => setTimeout(r, waitTime));
+ continue;
+ }
+
+ return await response.json();
+ } catch (err) {
+ console.error(`重试 ${i + 1} 失败:`, err);
+ }
+ }
+
+ throw new Error('所有重试都失败了');
+}
+```
+
+### 4. 日志监控
+
+查看服务器日志中的限流信息:
+
+```bash
+# 查看限流日志
+tail -f logs/combined.log | grep "限流\|429"
+```
+
+---
+
+## 性能影响
+
+### 限流带来的影响
+
+| 指标 | 值 |
+|------|-----|
+| 检查时间 | < 1ms |
+| 内存占用 | ~1KB/设备 |
+| CPU 开销 | 可忽略 |
+
+### 可支持的规模
+
+- **单个服务器** 可支持数千个不同设备的限流记录
+- **QPS** 不受限流影响(限流只是简单的时间戳比较)
+
+---
+
+## 常见问题
+
+### Q1: 如何禁用限流?
+
+**A:** 在 `httpConfig.json` 中设置:
+```json
+{
+ "rateLimit": {
+ "enabled": false
+ }
+}
+```
+
+### Q2: 限流是否对所有接口都适用?
+
+**A:** 不是。目前只对 `/api/device/data` 接口限流。其他接口(如 `/api/device/all`、`/api/device/list` 等)不受限流限制。
+
+### Q3: 我想为不同设备设置不同的限流时间?
+
+**A:** 当前版本不支持。建议在应用端实现设备级的限流逻辑。
+
+### Q4: 服务器重启后限流记录会丢失吗?
+
+**A:** 是的。限流记录存储在内存中,服务器重启后会清空。
+
+### Q5: 如何监控哪些设备经常被限流?
+
+**A:** 调用 `/api/ratelimit/stats` 接口查看,或查看程序日志中的⏱️记录。
+
+### Q6: 限流和 MQTT/阿里云 有关吗?
+
+**A:** 无关。限流只针对 HTTP API 接口,MQTT 和阿里云上传不受影响。
+
+---
+
+## 故障排查
+
+### 问题: 所有请求都返回 429
+
+**排查步骤:**
+1. 检查 `httpConfig.json` 中的 `rateLimit.interval` 配置
+2. 调用 `/api/ratelimit/clear` 清空限流记录
+3. 检查系统时钟是否正确
+
+### 问题: 限流不生效
+
+**排查步骤:**
+1. 确认 `httpConfig.json` 中 `rateLimit.enabled` 为 `true`
+2. 调用 `/api/ratelimit/stats` 确认限流已启用
+3. 检查是否在 5 秒内进行了重复请求
+
+### 问题: 出现 500 错误
+
+**排查步骤:**
+1. 查看服务器日志
+2. 检查是否有其他网络问题
+3. 尝试清空限流记录后重试
+
+---
+
+**文档版本:** 1.0
+**更新时间:** 2025年12月
+**支持的 API 版本:** 1.0+
diff --git a/START_HERE.md b/START_HERE.md
new file mode 100644
index 0000000..685317b
--- /dev/null
+++ b/START_HERE.md
@@ -0,0 +1,185 @@
+# 📖 第三方集成文档
+
+**Version**: v1.1.0 | **Date**: 2025-12-08 | **Status**: ✅ Ready
+
+---
+
+## 🚀 快速开始(选择您的角色)
+
+### 👨💻 我是前端开发者
+→ 阅读 [QUICK_START.md](./QUICK_START.md) (JavaScript示例)
+
+### 🐍 我是后端开发者
+→ 阅读 [QUICK_START.md](./QUICK_START.md) (Python示例)
+
+### 🔧 我是运维/系统管理员
+→ 阅读 [QUICK_START.md](./QUICK_START.md) (调试技巧)
+
+### 🧪 我是QA测试工程师
+→ 导入 [Postman_Collection.json](./Postman_Collection.json)
+
+### 📚 我需要完整参考
+→ 阅读 [HTTP_API_INTEGRATION_GUIDE.md](./HTTP_API_INTEGRATION_GUIDE.md)
+
+---
+
+## 📋 所有文档
+
+| 文档 | 类型 | 用途 |
+|------|------|------|
+| **[API_DOCUMENTATION_INDEX.md](./API_DOCUMENTATION_INDEX.md)** | 📚 导航 | **从这里开始** - 所有文档的导航中心 |
+| **[QUICK_START.md](./QUICK_START.md)** | 🚀 快速 | 30秒快速验证 + 完整代码示例 |
+| **[HTTP_API_INTEGRATION_GUIDE.md](./HTTP_API_INTEGRATION_GUIDE.md)** | 📖 参考 | 生产级详细参考文档 |
+| **[openapi.json](./openapi.json)** | 🔧 规范 | OpenAPI 3.0规范 (导入开发工具) |
+| **[Postman_Collection.json](./Postman_Collection.json)** | 🧪 工具 | 直接导入Postman测试 |
+| **[DOCUMENTATION_MANIFEST.json](./DOCUMENTATION_MANIFEST.json)** | 📊 索引 | 文档结构和元数据 |
+
+---
+
+## ✨ 包含内容
+
+✅ **10个API端点** 完整文档
+✅ **4种编程语言** 代码示例 (JavaScript, Python, Java, cURL)
+✅ **68个属性** 完整映射
+✅ **即插即用** 工具集合
+✅ **生产级** 文档质量
+
+---
+
+## 📊 快速数据
+
+- **文档数**: 6个
+- **总字数**: ~25,000字
+- **代码示例**: 15个
+- **API端点**: 10个
+- **支持语言**: 4种
+- **阅读时间**: 5-30分钟
+
+---
+
+## 🎯 典型场景
+
+### 场景1: 我有5分钟
+```
+→ 打开 QUICK_START.md
+→ 运行 "30秒快速开始" 章节
+→ 验证服务可用
+```
+
+### 场景2: 我要集成到生产环境
+```
+→ 阅读 HTTP_API_INTEGRATION_GUIDE.md
+→ 查看错误处理章节
+→ 实现重试和超时机制
+→ 运行性能测试
+```
+
+### 场景3: 我要快速测试API
+```
+→ 导入 Postman_Collection.json
+→ 修改 base_url 变量
+→ 点击 Send 测试
+```
+
+### 场景4: 我需要最新规范
+```
+→ 查看 openapi.json
+→ 上传至 https://editor.swagger.io
+→ 或导入开发工具
+```
+
+---
+
+## 🚀 立即开始
+
+### 1️⃣ 验证服务 (30秒)
+
+```bash
+curl http://localhost:8080/api/health
+```
+
+### 2️⃣ 获取设备列表 (10秒)
+
+```bash
+curl http://localhost:8080/api/device/list
+```
+
+### 3️⃣ 查询设备数据 (10秒)
+
+```bash
+curl "http://localhost:8080/api/device/data?deviceNumber=D001&mapped=true"
+```
+
+### ✅ 完成!
+
+更多示例见 [QUICK_START.md](./QUICK_START.md)
+
+---
+
+## 📌 重要提示
+
+- 🔒 **始终使用** `?mapped=true` 参数获取可读的中文属性名
+- ⏱️ **注意限流**: 单个设备5秒/次,批量60秒/次
+- 📊 **查看统计**: `/api/cache/stats` 和 `/api/ratelimit/stats`
+- 🐛 **调试工具**: 使用Postman或Insomnia,查看[QUICK_START.md](./QUICK_START.md)的调试技巧
+
+---
+
+## 📞 需要帮助?
+
+1. 查看 [常见问题](./HTTP_API_INTEGRATION_GUIDE.md#❓-常见问题)
+2. 查看 [错误处理](./HTTP_API_INTEGRATION_GUIDE.md#⚠️-错误处理)
+3. 查看 [调试技巧](./QUICK_START.md#🔍-调试技巧)
+4. 查看 [代码示例](./QUICK_START.md#📦-完整集成示例)
+
+---
+
+## 🎓 推荐阅读顺序
+
+```
+初学者:
+ 1. API_DOCUMENTATION_INDEX.md (5分钟)
+ 2. QUICK_START.md (10分钟)
+ 3. Postman_Collection.json (测试)
+
+开发者:
+ 1. QUICK_START.md (10分钟)
+ 2. HTTP_API_INTEGRATION_GUIDE.md (30分钟)
+ 3. 根据需要查阅openapi.json
+
+生产部署:
+ 1. HTTP_API_INTEGRATION_GUIDE.md (完整阅读)
+ 2. 查看错误处理和限流规则
+ 3. 实现重试机制
+ 4. 性能测试
+```
+
+---
+
+## 📱 API速查表
+
+| 端点 | 方法 | 说明 |
+|------|------|------|
+| `/api/health` | GET | 健康检查 |
+| `/api/device/data?deviceNumber=D001&mapped=true` | GET | 查询单个设备(推荐用`?mapped=true`) |
+| `/api/device/all?mapped=true` | GET | 查询所有设备 |
+| `/api/device/list` | GET | 设备列表 |
+| `/api/cache/stats` | GET | 缓存统计 |
+| `/api/ratelimit/stats` | GET | 限流统计 |
+
+---
+
+## 📞 联系我们
+
+- 📧 Email: support@example.com
+- 💬 讨论区: [提交Issue]
+- 🐛 Bug报告: [GitHub Issues]
+- 📱 紧急支持: +86-xxx-xxxx-xxxx
+
+---
+
+**📖 开始阅读**: [点击这里查看完整文档导航](./API_DOCUMENTATION_INDEX.md)
+
+---
+
+*Last Updated: 2025-12-08 | Version: v1.1.0 | Status: Production Ready ✅*
diff --git a/aliyun.json b/aliyun.json
index de12da8..0e642af 100644
--- a/aliyun.json
+++ b/aliyun.json
@@ -1,4 +1,4 @@
{
- "enabled": true,
+ "enabled": false,
"autoRegister": true
}
\ No newline at end of file
diff --git a/dataCache.js b/dataCache.js
new file mode 100644
index 0000000..4fd6039
--- /dev/null
+++ b/dataCache.js
@@ -0,0 +1,151 @@
+// dataCache.js - 透析机数据缓存管理
+const logger = require('./logger');
+
+class DataCache {
+ constructor() {
+ // 使用 Map 存储,key 是设备序号 (masData.n),value 是最新数据
+ this.cache = new Map();
+
+ // 记录每个设备的更新时间,用于监控
+ this.updateTimes = new Map();
+ }
+
+ /**
+ * 存储或更新设备数据
+ * @param {string} deviceNumber - 设备序号 (masData.n)
+ * @param {object} data - 完整的设备数据对象
+ */
+ setDeviceData(deviceNumber, data) {
+ if (!deviceNumber) {
+ logger.warn('设备序号为空,无法缓存数据');
+ return false;
+ }
+
+ try {
+ this.cache.set(deviceNumber, {
+ ...data,
+ _cachedAt: new Date().toISOString()
+ });
+ this.updateTimes.set(deviceNumber, Date.now());
+ logger.info(`✅ 数据缓存更新: 设备 ${deviceNumber}`);
+ return true;
+ } catch (err) {
+ logger.error(`缓存数据失败 (${deviceNumber}):`, err.message);
+ return false;
+ }
+ }
+
+ /**
+ * 获取指定设备的最新数据
+ * @param {string} deviceNumber - 设备序号
+ * @returns {object|null} 设备数据或 null
+ */
+ getDeviceData(deviceNumber) {
+ const data = this.cache.get(deviceNumber);
+ if (!data) {
+ logger.warn(`未找到设备 ${deviceNumber} 的缓存数据`);
+ return null;
+ }
+ logger.info(`📖 读取缓存数据: 设备 ${deviceNumber}`);
+ return data;
+ }
+
+ /**
+ * 获取所有设备的数据
+ * @returns {object} 所有设备的数据字典
+ */
+ getAllDeviceData() {
+ const result = {};
+ for (const [key, value] of this.cache.entries()) {
+ result[key] = value;
+ }
+ return result;
+ }
+
+ /**
+ * 获取所有设备的列表(仅包含设备号和最后更新时间)
+ * @returns {array} 设备列表
+ */
+ getDeviceList() {
+ const list = [];
+ for (const [deviceNumber, data] of this.cache.entries()) {
+ list.push({
+ deviceNumber: deviceNumber,
+ lastUpdate: data._cachedAt,
+ });
+ }
+ return list;
+ }
+
+ /**
+ * 检查设备是否存在
+ * @param {string} deviceNumber - 设备序号
+ * @returns {boolean}
+ */
+ hasDevice(deviceNumber) {
+ return this.cache.has(deviceNumber);
+ }
+
+ /**
+ * 删除指定设备的缓存
+ * @param {string} deviceNumber - 设备序号
+ * @returns {boolean}
+ */
+ deleteDeviceData(deviceNumber) {
+ const result = this.cache.delete(deviceNumber);
+ this.updateTimes.delete(deviceNumber);
+ if (result) {
+ logger.info(`🗑️ 删除缓存数据: 设备 ${deviceNumber}`);
+ }
+ return result;
+ }
+
+ /**
+ * 清空所有缓存
+ */
+ clearAll() {
+ const count = this.cache.size;
+ this.cache.clear();
+ this.updateTimes.clear();
+ logger.info(`🗑️ 已清空所有缓存数据 (共 ${count} 个设备)`);
+ }
+
+ /**
+ * 获取缓存统计信息
+ * @returns {object}
+ */
+ getStats() {
+ const deviceCount = this.cache.size;
+ const memoryUsage = JSON.stringify(this.getAllDeviceData()).length;
+
+ return {
+ deviceCount: deviceCount,
+ memoryUsage: `${(memoryUsage / 1024).toFixed(2)} KB`,
+ devices: this.getDeviceList()
+ };
+ }
+
+ /**
+ * 获取指定时间范围内未更新的设备列表
+ * @param {number} timeoutMs - 超时时间(毫秒)
+ * @returns {array} 超时的设备列表
+ */
+ getIdleDevices(timeoutMs = 300000) { // 默认 5 分钟
+ const now = Date.now();
+ const idleDevices = [];
+
+ for (const [deviceNumber, updateTime] of this.updateTimes.entries()) {
+ if (now - updateTime > timeoutMs) {
+ idleDevices.push({
+ deviceNumber: deviceNumber,
+ idleTime: now - updateTime
+ });
+ }
+ }
+
+ return idleDevices;
+ }
+}
+
+// 导出单例
+module.exports = new DataCache();
diff --git a/httpConfig.json b/httpConfig.json
new file mode 100644
index 0000000..fe9d697
--- /dev/null
+++ b/httpConfig.json
@@ -0,0 +1,14 @@
+{
+ "enabled": true,
+ "port": 8080,
+ "host": "0.0.0.0",
+ "cors": {
+ "enabled": true,
+ "allowOrigin": "*"
+ },
+ "rateLimit": {
+ "enabled": true,
+ "interval": 5000,
+ "allDevicesInterval": 60000
+ }
+}
diff --git a/httpServer.js b/httpServer.js
new file mode 100644
index 0000000..2599f2c
--- /dev/null
+++ b/httpServer.js
@@ -0,0 +1,477 @@
+// 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;
diff --git a/index.js b/index.js
index 2b941f8..0bcbd9e 100644
--- a/index.js
+++ b/index.js
@@ -9,9 +9,11 @@
const appPath = process.pkg ? path.dirname(process.execPath) : __dirname;
const mqttConfigPath = path.join(appPath, 'mqtt.json');
const aliyunConfigPath = path.join(appPath, 'aliyun.json');
+const httpConfigPath = path.join(appPath, 'httpConfig.json');
const mqttConfig = JSON.parse(fs.readFileSync(mqttConfigPath, 'utf8'));
const aliyunConfig=JSON.parse(fs.readFileSync(aliyunConfigPath, 'utf8'))
+const httpConfig = JSON.parse(fs.readFileSync(httpConfigPath, 'utf8'));
console.log(aliyunConfig)
@@ -26,6 +28,8 @@
const aliyunIot = require('aliyun-iot-device-sdk');
const { getAliyunDeviceSecret } = require('./api');
const toModel = require('./Strholp');
+const dataCache = require('./dataCache');
+const HttpServer = require('./httpServer');
// 初始化 MQTT(独立于阿里云)
initMqtt(mqttConfig);
@@ -189,6 +193,10 @@
const masData = toModel(message);
deviceInfo.iotDeviceNo = masData.n;
deviceInfo.masData = masData;
+
+ // ✅【新增】缓存数据到内存(按设备序号)
+ dataCache.setDeviceData(masData.n, masData);
+
// ✅【核心改动】收到数据立即发 MQTT(不管阿里云)
if (mqttConfig.enabled) {
const topic = `${mqttConfig.defaultTopicPrefix}/${masData.n}`;
@@ -320,4 +328,9 @@
const PORT = process.env.PORT || 10961;
server.listen(PORT, () => {
logger.info(`Socket 服务已启动,监听超级端口: ${PORT}`);
-});
\ No newline at end of file
+});
+
+// ========== 启动 HTTP 服务 ==========
+const HTTP_PORT = process.env.HTTP_PORT || httpConfig.port || 8080;
+const httpServer = new HttpServer(HTTP_PORT, httpConfig);
+httpServer.start();
\ No newline at end of file
diff --git a/openapi.json b/openapi.json
new file mode 100644
index 0000000..82078d2
--- /dev/null
+++ b/openapi.json
@@ -0,0 +1,584 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "透析机IoT数据服务 HTTP API",
+ "description": "透析机设备实时数据查询和管理接口",
+ "version": "1.1.0",
+ "contact": {
+ "name": "技术支持",
+ "email": "support@example.com"
+ },
+ "license": {
+ "name": "MIT"
+ }
+ },
+ "servers": [
+ {
+ "url": "http://localhost:8080",
+ "description": "开发环境"
+ },
+ {
+ "url": "http://production-server:8080",
+ "description": "生产环境"
+ }
+ ],
+ "paths": {
+ "/api/health": {
+ "get": {
+ "summary": "健康检查",
+ "description": "检查服务器是否正常运行",
+ "operationId": "getHealth",
+ "tags": ["系统"],
+ "responses": {
+ "200": {
+ "description": "服务正常",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer",
+ "example": 200
+ },
+ "message": {
+ "type": "string",
+ "example": "success"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "example": "ok"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "服务不可用"
+ }
+ }
+ }
+ },
+ "/api/device/data": {
+ "get": {
+ "summary": "获取单个设备数据",
+ "description": "获取指定设备的实时数据。支持原始格式和映射格式(推荐)",
+ "operationId": "getDeviceData",
+ "tags": ["设备数据"],
+ "parameters": [
+ {
+ "name": "deviceNumber",
+ "in": "query",
+ "description": "设备号,例如 D001",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "mapped",
+ "in": "query",
+ "description": "是否返回映射格式(推荐使用 true)",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "enum": ["true", "false"],
+ "default": "false"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功获取设备数据",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeviceDataResponse"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "参数错误"
+ },
+ "404": {
+ "description": "设备不存在"
+ },
+ "429": {
+ "description": "请求过于频繁,请稍后再试"
+ }
+ },
+ "x-rate-limit": {
+ "interval": "5秒",
+ "max-requests": 1
+ }
+ }
+ },
+ "/api/device/all": {
+ "get": {
+ "summary": "获取所有设备数据",
+ "description": "批量获取所有设备的实时数据",
+ "operationId": "getAllDevices",
+ "tags": ["设备数据"],
+ "parameters": [
+ {
+ "name": "mapped",
+ "in": "query",
+ "description": "是否返回映射格式(推荐使用 true)",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "enum": ["true", "false"],
+ "default": "false"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功获取所有设备数据",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AllDevicesResponse"
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "请求过于频繁,请稍后再试"
+ }
+ },
+ "x-rate-limit": {
+ "interval": "60秒",
+ "max-requests": 1
+ }
+ }
+ },
+ "/api/device/list": {
+ "get": {
+ "summary": "获取设备列表",
+ "description": "获取所有已连接设备的摘要信息",
+ "operationId": "getDeviceList",
+ "tags": ["设备数据"],
+ "responses": {
+ "200": {
+ "description": "成功获取设备列表",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeviceListResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/device/idle": {
+ "get": {
+ "summary": "获取超时设备",
+ "description": "获取超过指定时间未更新的设备列表",
+ "operationId": "getIdleDevices",
+ "tags": ["设备数据"],
+ "parameters": [
+ {
+ "name": "timeout",
+ "in": "query",
+ "description": "超时时间(毫秒),例如 300000(5分钟)",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 300000
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "成功获取超时设备列表",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "timeout": {
+ "type": "integer"
+ },
+ "idleDevices": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/cache/stats": {
+ "get": {
+ "summary": "获取缓存统计",
+ "description": "获取数据缓存的统计信息",
+ "operationId": "getCacheStats",
+ "tags": ["统计"],
+ "responses": {
+ "200": {
+ "description": "成功获取缓存统计",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "totalDevices": {
+ "type": "integer"
+ },
+ "cachedProperties": {
+ "type": "integer"
+ },
+ "cacheSize": {
+ "type": "string"
+ },
+ "oldestData": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "newestData": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/ratelimit/stats": {
+ "get": {
+ "summary": "获取限流统计",
+ "description": "获取请求限流的统计信息",
+ "operationId": "getRateLimitStats",
+ "tags": ["统计"],
+ "responses": {
+ "200": {
+ "description": "成功获取限流统计",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "interval": {
+ "type": "integer",
+ "description": "单个设备限流间隔(毫秒)"
+ },
+ "allDevicesInterval": {
+ "type": "integer",
+ "description": "全局限流间隔(毫秒)"
+ },
+ "records": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "identifier": {
+ "type": "string"
+ },
+ "lastRequest": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "requestCount": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/cache/clear": {
+ "post": {
+ "summary": "清空缓存",
+ "description": "清空所有缓存的设备数据(需谨慎使用)",
+ "operationId": "clearCache",
+ "tags": ["缓存管理"],
+ "responses": {
+ "200": {
+ "description": "缓存已清空",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/ratelimit/clear": {
+ "post": {
+ "summary": "清空限流记录",
+ "description": "清空所有限流记录",
+ "operationId": "clearRateLimit",
+ "tags": ["缓存管理"],
+ "responses": {
+ "200": {
+ "description": "限流记录已清空",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "DeviceProperty": {
+ "type": "object",
+ "properties": {
+ "identifier": {
+ "type": "string",
+ "description": "属性标识符",
+ "example": "A"
+ },
+ "name": {
+ "type": "string",
+ "description": "属性中文名称",
+ "example": "脱水目标量"
+ },
+ "value": {
+ "type": "string",
+ "description": "属性值",
+ "example": "50"
+ }
+ }
+ },
+ "DeviceDataResponse": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer",
+ "example": 200
+ },
+ "message": {
+ "type": "string",
+ "example": "success"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "deviceNumber": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "properties": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DeviceProperty"
+ }
+ },
+ "format": {
+ "type": "string",
+ "enum": ["raw", "mapped"]
+ }
+ }
+ }
+ }
+ },
+ "AllDevicesResponse": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "count": {
+ "type": "integer"
+ },
+ "data": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "properties": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DeviceProperty"
+ }
+ },
+ "format": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "format": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "DeviceListResponse": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "count": {
+ "type": "integer"
+ },
+ "devices": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "deviceNumber": {
+ "type": "string"
+ },
+ "lastUpdate": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "系统",
+ "description": "系统级别的API"
+ },
+ {
+ "name": "设备数据",
+ "description": "设备数据查询接口"
+ },
+ {
+ "name": "统计",
+ "description": "统计信息接口"
+ },
+ {
+ "name": "缓存管理",
+ "description": "缓存和限流管理"
+ }
+ ]
+}
diff --git a/propertyMapper.js b/propertyMapper.js
new file mode 100644
index 0000000..7ae7064
--- /dev/null
+++ b/propertyMapper.js
@@ -0,0 +1,262 @@
+// propertyMapper.js - 阿里云物模型数据映射
+const logger = require('./logger');
+const fs = require('fs');
+const path = require('path');
+
+class PropertyMapper {
+ constructor() {
+ // 从文件加载物模型,或使用内置配置
+ this.schema = this.loadSchema();
+ this.propertyMap = this.buildPropertyMap();
+ }
+
+ /**
+ * 加载物模型定义
+ */
+ loadSchema() {
+ try {
+ // 尝试从 schema.json 文件加载
+ const schemaPath = path.join(__dirname, 'schema.json');
+ if (fs.existsSync(schemaPath)) {
+ const schemaContent = fs.readFileSync(schemaPath, 'utf8');
+ logger.info('✅ 已从 schema.json 加载物模型');
+ return JSON.parse(schemaContent);
+ }
+ } catch (err) {
+ logger.warn(`加载 schema.json 失败: ${err.message}`);
+ }
+
+ // 返回默认配置
+ logger.info('ℹ️ 使用默认物模型配置');
+ return this.getDefaultSchema();
+ }
+
+ /**
+ * 获取默认物模型(完整版 - 与schema.json同步)
+ */
+ getDefaultSchema() {
+ return {
+ properties: [
+ { identifier: 'A', name: '脱水目标量' },
+ { identifier: 'B', name: '脱水量' },
+ { identifier: 'C', name: '脱水速率' },
+ { identifier: 'D', name: '血液流速' },
+ { identifier: 'E', name: '肝素速率' },
+ { identifier: 'F', name: '透析液温度' },
+ { identifier: 'G', name: '透析液电导度' },
+ { identifier: 'H', name: '静脉压' },
+ { identifier: 'I', name: '透析液压' },
+ { identifier: 'J', name: '跨膜压' },
+ { identifier: 'K', name: '透析时间' },
+ { identifier: 'a', name: '透析液温度报警' },
+ { identifier: 'b', name: '电导度报警' },
+ { identifier: 'c', name: '静脉压报警' },
+ { identifier: 'd', name: '透析液压力报警' },
+ { identifier: 'e', name: '跨膜压警报' },
+ { identifier: 'f', name: '气泡侦测器警报' },
+ { identifier: 'g', name: '漏血报警' },
+ { identifier: 'h', name: '其他报警' },
+ { identifier: 'L', name: '透析液流速' },
+ { identifier: 'M', name: 'BPM监测时间' },
+ { identifier: 'N', name: 'BPM最高血压' },
+ { identifier: 'O', name: 'BPM最低血压' },
+ { identifier: 'P', name: 'BPM脉冲' },
+ { identifier: 'Q', name: '收缩压上限' },
+ { identifier: 'R', name: '收缩压下限' },
+ { identifier: 'S', name: 'BPM压脉带压力' },
+ { identifier: 'T', name: 'BPM检测间隔时间' },
+ { identifier: 'U', name: '血流总量' },
+ { identifier: 'V', name: '静脉压上限报警' },
+ { identifier: 'W', name: '静脉压下限报警' },
+ { identifier: 'X', name: '肝素总量' },
+ { identifier: 'Y', name: '透析液压上限报警' },
+ { identifier: 'Z', name: '透析液压下限警报' },
+ { identifier: 'i', name: '单补钠个性化程序' },
+ { identifier: 'j', name: '脱水个性化程序' },
+ { identifier: 'k', name: '透析液选择' },
+ { identifier: 'l', name: '电导度档位' },
+ { identifier: 'm', name: '数据通信状态' },
+ { identifier: 'n', name: '序列号' },
+ { identifier: 'o', name: '动脉压' },
+ { identifier: 'p', name: '动脉压警报' },
+ { identifier: 'q', name: '动脉压上限警报' },
+ { identifier: 'r', name: '动脉压下限警报' },
+ { identifier: 's', name: '跨膜压上限警报' },
+ { identifier: 't', name: '跨膜压下限警报' },
+ { identifier: 'u', name: '置换液速率' },
+ { identifier: 'v', name: '置换液目标量' },
+ { identifier: 'w', name: '置换液进程量' },
+ { identifier: 'x', name: '电导度个性化程序' },
+ { identifier: 'y', name: '血液流速个性化程序' },
+ { identifier: 'z', name: '肝素个性化程序' },
+ { identifier: 'C53', name: '透析液个性化程序' },
+ { identifier: 'C54', name: '透析液温度初始设置' },
+ { identifier: 'C55', name: '缺水2警报' },
+ { identifier: 'suedtime', name: '传输时间' },
+ { identifier: 'deviceType', name: 'deviceType' },
+ { identifier: 'IPAddress', name: 'IP地址' },
+ { identifier: 'deviceName', name: '设备名称' },
+ { identifier: 'warn', name: '警告' },
+ { identifier: 'ICCID', name: 'iccid' },
+ { identifier: 'mb', name: '脉搏-德朗' },
+ { identifier: 'szy', name: '舒张压-德朗' },
+ { identifier: 'ssy', name: '收缩压-德朗' },
+ { identifier: 'sysj', name: '剩余时间-德朗' },
+ { identifier: 'bjbh', name: '报警编号-德朗' },
+ { identifier: 'xlllsd', name: '血泵流量设定-德朗' }
+ ]
+ };
+ }
+
+ /**
+ * 构建属性映射表 (identifier -> name)
+ */
+ buildPropertyMap() {
+ const map = {};
+ if (this.schema.properties && Array.isArray(this.schema.properties)) {
+ this.schema.properties.forEach(prop => {
+ map[prop.identifier] = prop.name;
+ });
+ }
+ return map;
+ }
+
+ /**
+ * 获取属性名称
+ * @param {string} identifier - 属性标识符
+ * @returns {string} 属性名称
+ */
+ getPropertyName(identifier) {
+ return this.propertyMap[identifier] || identifier;
+ }
+
+ /**
+ * 将原始数据转换为标准格式
+ * @param {object} rawData - 原始数据对象
+ * @returns {array} 转换后的数据数组
+ */
+ mapData(rawData) {
+ if (!rawData || typeof rawData !== 'object') {
+ logger.warn('输入数据无效');
+ return [];
+ }
+
+ const mappedData = [];
+
+ for (const [key, value] of Object.entries(rawData)) {
+ // 跳过内部字段
+ if (key.startsWith('_')) {
+ continue;
+ }
+
+ // 获取属性名称
+ const name = this.getPropertyName(key);
+
+ mappedData.push({
+ identifier: key,
+ name: name,
+ value: value
+ });
+ }
+
+ return mappedData;
+ }
+
+ /**
+ * 将原始数据转换为 HTTP 响应格式
+ * @param {object} rawData - 原始数据
+ * @param {string} deviceNumber - 设备号
+ * @returns {array} 转换后的属性数组
+ */
+ transformForHTTP(rawData, deviceNumber) {
+ const mappedData = this.mapData(rawData);
+ return mappedData;
+ }
+
+ /**
+ * 将原始数据转换为 MQTT 发布格式
+ * @param {object} rawData - 原始数据
+ * @param {string} deviceNumber - 设备号
+ * @returns {object} 转换后的数据
+ */
+ transformForMQTT(rawData, deviceNumber) {
+ const mappedData = this.mapData(rawData);
+
+ return {
+ deviceNumber: deviceNumber,
+ deviceId: deviceNumber,
+ timestamp: new Date().toISOString(),
+ data: mappedData
+ };
+ }
+
+ /**
+ * 将原始数据转换为阿里云物联网平台格式
+ * @param {object} rawData - 原始数据
+ * @returns {object} 转换后的属性对象
+ */
+ transformForAliyun(rawData) {
+ const props = {};
+
+ for (const [key, value] of Object.entries(rawData)) {
+ // 跳过内部字段
+ if (key.startsWith('_')) {
+ continue;
+ }
+
+ // 直接使用 identifier 作为属性
+ props[key] = value;
+ }
+
+ return props;
+ }
+
+ /**
+ * 获取所有属性定义
+ */
+ getAllProperties() {
+ return this.schema.properties || [];
+ }
+
+ /**
+ * 获取属性定义
+ * @param {string} identifier - 属性标识符
+ * @returns {object} 属性定义
+ */
+ getPropertyDefinition(identifier) {
+ if (!this.schema.properties) return null;
+ return this.schema.properties.find(p => p.identifier === identifier);
+ }
+
+ /**
+ * 统计属性信息
+ */
+ getPropertyStats() {
+ const properties = this.schema.properties || [];
+ const readOnlyCount = properties.filter(p => p.accessMode === 'r').length;
+ const readWriteCount = properties.filter(p => p.accessMode === 'rw').length;
+
+ // 统计数据类型分布
+ const dataTypeDistribution = new Map();
+ const dataTypes = new Set();
+
+ properties.forEach(prop => {
+ dataTypes.add(prop.dataType);
+ dataTypeDistribution.set(
+ prop.dataType,
+ (dataTypeDistribution.get(prop.dataType) || 0) + 1
+ );
+ });
+
+ return {
+ totalProperties: properties.length,
+ readOnlyCount: readOnlyCount,
+ readWriteCount: readWriteCount,
+ dataTypes: dataTypes,
+ dataTypeDistribution: Array.from(dataTypeDistribution.entries())
+ };
+ }
+}
+
+// 导出单例
+module.exports = new PropertyMapper();
diff --git a/quickTest.js b/quickTest.js
new file mode 100644
index 0000000..a123c8d
--- /dev/null
+++ b/quickTest.js
@@ -0,0 +1,24 @@
+// 快速验证属性映射
+const mapper = require('./propertyMapper.js');
+
+const testData = {
+ A: '50',
+ B: '25',
+ C: '10',
+ D: '37.5',
+ E: '200',
+ n: 'D001'
+};
+
+console.log('映射验证 - 输入数据:');
+console.log(JSON.stringify(testData, null, 2));
+
+const mapped = mapper.mapData(testData);
+
+console.log('\n映射结果:');
+mapped.forEach(item => {
+ console.log(` [${item.identifier}] ${item.name} = ${item.value}`);
+});
+
+console.log(`\n✅ 总计: ${mapped.length} 个属性已映射`);
+console.log('✅ 属性映射功能正常!');
diff --git a/rateLimiter.js b/rateLimiter.js
new file mode 100644
index 0000000..e5947e3
--- /dev/null
+++ b/rateLimiter.js
@@ -0,0 +1,132 @@
+// rateLimiter.js - 请求频率限制管理
+const logger = require('./logger');
+
+class RateLimiter {
+ constructor() {
+ // 存储请求记录
+ // key: identifier (设备号或IP), value: { lastRequestTime, count }
+ this.requestMap = new Map();
+ }
+
+ /**
+ * 检查是否允许请求
+ * @param {string} identifier - 标识符(设备号、IP 等)
+ * @param {number} intervalMs - 时间间隔(毫秒)
+ * @returns {object} { allowed: boolean, remainingTime: number }
+ */
+ checkLimit(identifier, intervalMs = 5000) {
+ if (!identifier) {
+ logger.warn('限流检查: 标识符为空');
+ return { allowed: true, remainingTime: 0 };
+ }
+
+ const now = Date.now();
+ const record = this.requestMap.get(identifier);
+
+ if (!record) {
+ // 第一次请求,允许
+ this.requestMap.set(identifier, {
+ lastRequestTime: now,
+ count: 1
+ });
+ logger.info(`✅ 限流: 首次请求 ${identifier}`);
+ return { allowed: true, remainingTime: 0 };
+ }
+
+ const timeSinceLastRequest = now - record.lastRequestTime;
+
+ if (timeSinceLastRequest < intervalMs) {
+ // 请求过于频繁
+ const remainingTime = intervalMs - timeSinceLastRequest;
+ logger.warn(`⏱️ 限流: ${identifier} 请求过于频繁,需等待 ${remainingTime}ms`);
+
+ return {
+ allowed: false,
+ remainingTime: Math.ceil(remainingTime)
+ };
+ }
+
+ // 允许请求,更新记录
+ this.requestMap.set(identifier, {
+ lastRequestTime: now,
+ count: record.count + 1
+ });
+ logger.info(`✅ 限流: ${identifier} 请求已允许 (第 ${record.count + 1} 次)`);
+
+ return { allowed: true, remainingTime: 0 };
+ }
+
+ /**
+ * 获取限流状态
+ */
+ getStatus(identifier) {
+ return this.requestMap.get(identifier) || null;
+ }
+
+ /**
+ * 获取所有限流记录
+ */
+ getAllStatus() {
+ const result = {};
+ for (const [key, value] of this.requestMap.entries()) {
+ result[key] = value;
+ }
+ return result;
+ }
+
+ /**
+ * 清空特定标识符的限流记录
+ */
+ clearIdentifier(identifier) {
+ if (this.requestMap.has(identifier)) {
+ this.requestMap.delete(identifier);
+ logger.info(`🗑️ 已清空限流记录: ${identifier}`);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 清空所有限流记录
+ */
+ clearAll() {
+ const count = this.requestMap.size;
+ this.requestMap.clear();
+ logger.info(`🗑️ 已清空所有限流记录 (共 ${count} 条)`);
+ }
+
+ /**
+ * 获取限流统计信息
+ */
+ getStats() {
+ return {
+ totalIdentifiers: this.requestMap.size,
+ records: this.getAllStatus()
+ };
+ }
+
+ /**
+ * 清理过期的限流记录(可选)
+ * @param {number} maxAgeMs - 最大保留时间(毫秒)
+ */
+ cleanupExpired(maxAgeMs = 3600000) { // 默认 1 小时
+ const now = Date.now();
+ let cleanedCount = 0;
+
+ for (const [identifier, record] of this.requestMap.entries()) {
+ if (now - record.lastRequestTime > maxAgeMs) {
+ this.requestMap.delete(identifier);
+ cleanedCount++;
+ }
+ }
+
+ if (cleanedCount > 0) {
+ logger.info(`🧹 已清理过期限流记录: ${cleanedCount} 条`);
+ }
+
+ return cleanedCount;
+ }
+}
+
+// 导出单例
+module.exports = new RateLimiter();
diff --git a/schema.json b/schema.json
new file mode 100644
index 0000000..ab0cf8d
--- /dev/null
+++ b/schema.json
@@ -0,0 +1,74 @@
+{
+ "productName": "透析机物联网设备",
+ "version": "1.0.0",
+ "description": "阿里云IoT透析机物理模型TSL定义(与propertyMapper.js同步)",
+ "properties": [
+ {"identifier": "A", "name": "脱水目标量", "dataType": "float", "accessMode": "r"},
+ {"identifier": "B", "name": "脱水量", "dataType": "float", "accessMode": "r"},
+ {"identifier": "C", "name": "脱水速率", "dataType": "float", "accessMode": "r"},
+ {"identifier": "D", "name": "血液流速", "dataType": "float", "accessMode": "r"},
+ {"identifier": "E", "name": "肝素速率", "dataType": "float", "accessMode": "r"},
+ {"identifier": "F", "name": "透析液温度", "dataType": "float", "accessMode": "r"},
+ {"identifier": "G", "name": "透析液电导度", "dataType": "float", "accessMode": "r"},
+ {"identifier": "H", "name": "静脉压", "dataType": "float", "accessMode": "r"},
+ {"identifier": "I", "name": "透析液压", "dataType": "float", "accessMode": "r"},
+ {"identifier": "J", "name": "跨膜压", "dataType": "float", "accessMode": "r"},
+ {"identifier": "K", "name": "透析时间", "dataType": "int", "accessMode": "r"},
+ {"identifier": "a", "name": "透析液温度报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "b", "name": "电导度报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "c", "name": "静脉压报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "d", "name": "透析液压力报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "e", "name": "跨膜压警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "f", "name": "气泡侦测器警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "g", "name": "漏血报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "h", "name": "其他报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "L", "name": "透析液流速", "dataType": "int", "accessMode": "r"},
+ {"identifier": "M", "name": "BPM监测时间", "dataType": "int", "accessMode": "r"},
+ {"identifier": "N", "name": "BPM最高血压", "dataType": "int", "accessMode": "r"},
+ {"identifier": "O", "name": "BPM最低血压", "dataType": "int", "accessMode": "r"},
+ {"identifier": "P", "name": "BPM脉冲", "dataType": "int", "accessMode": "r"},
+ {"identifier": "Q", "name": "收缩压上限", "dataType": "int", "accessMode": "r"},
+ {"identifier": "R", "name": "收缩压下限", "dataType": "int", "accessMode": "r"},
+ {"identifier": "S", "name": "BPM压脉带压力", "dataType": "int", "accessMode": "r"},
+ {"identifier": "T", "name": "BPM检测间隔时间", "dataType": "int", "accessMode": "r"},
+ {"identifier": "U", "name": "血流总量", "dataType": "float", "accessMode": "r"},
+ {"identifier": "V", "name": "静脉压上限报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "W", "name": "静脉压下限报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "X", "name": "肝素总量", "dataType": "float", "accessMode": "r"},
+ {"identifier": "Y", "name": "透析液压上限报警", "dataType": "int", "accessMode": "r"},
+ {"identifier": "Z", "name": "透析液压下限警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "i", "name": "单补钠个性化程序", "dataType": "int", "accessMode": "rw"},
+ {"identifier": "j", "name": "脱水个性化程序", "dataType": "int", "accessMode": "rw"},
+ {"identifier": "k", "name": "透析液选择", "dataType": "string", "accessMode": "rw"},
+ {"identifier": "l", "name": "电导度档位", "dataType": "int", "accessMode": "rw"},
+ {"identifier": "m", "name": "数据通信状态", "dataType": "int", "accessMode": "r"},
+ {"identifier": "n", "name": "序列号", "dataType": "string", "accessMode": "r"},
+ {"identifier": "o", "name": "动脉压", "dataType": "float", "accessMode": "r"},
+ {"identifier": "p", "name": "动脉压警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "q", "name": "动脉压上限警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "r", "name": "动脉压下限警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "s", "name": "跨膜压上限警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "t", "name": "跨膜压下限警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "u", "name": "置换液速率", "dataType": "float", "accessMode": "r"},
+ {"identifier": "v", "name": "置换液目标量", "dataType": "float", "accessMode": "r"},
+ {"identifier": "w", "name": "置换液进程量", "dataType": "float", "accessMode": "r"},
+ {"identifier": "x", "name": "电导度个性化程序", "dataType": "int", "accessMode": "rw"},
+ {"identifier": "y", "name": "血液流速个性化程序", "dataType": "int", "accessMode": "rw"},
+ {"identifier": "z", "name": "肝素个性化程序", "dataType": "int", "accessMode": "rw"},
+ {"identifier": "C53", "name": "透析液个性化程序", "dataType": "int", "accessMode": "rw"},
+ {"identifier": "C54", "name": "透析液温度初始设置", "dataType": "float", "accessMode": "rw"},
+ {"identifier": "C55", "name": "缺水2警报", "dataType": "int", "accessMode": "r"},
+ {"identifier": "suedtime", "name": "传输时间", "dataType": "long", "accessMode": "r"},
+ {"identifier": "deviceType", "name": "deviceType", "dataType": "string", "accessMode": "r"},
+ {"identifier": "IPAddress", "name": "IP地址", "dataType": "string", "accessMode": "r"},
+ {"identifier": "deviceName", "name": "设备名称", "dataType": "string", "accessMode": "r"},
+ {"identifier": "warn", "name": "警告", "dataType": "string", "accessMode": "r"},
+ {"identifier": "ICCID", "name": "iccid", "dataType": "string", "accessMode": "r"},
+ {"identifier": "mb", "name": "脉搏-德朗", "dataType": "int", "accessMode": "r"},
+ {"identifier": "szy", "name": "舒张压-德朗", "dataType": "int", "accessMode": "r"},
+ {"identifier": "ssy", "name": "收缩压-德朗", "dataType": "int", "accessMode": "r"},
+ {"identifier": "sysj", "name": "剩余时间-德朗", "dataType": "int", "accessMode": "r"},
+ {"identifier": "bjbh", "name": "报警编号-德朗", "dataType": "string", "accessMode": "r"},
+ {"identifier": "xlllsd", "name": "血泵流量设定-德朗", "dataType": "float", "accessMode": "rw"}
+ ]
+}
diff --git "a/\344\270\234\344\270\275\351\200\217\346\236\220\346\234\272\351\200\232\350\256\257.zip" "b/\344\270\234\344\270\275\351\200\217\346\236\220\346\234\272\351\200\232\350\256\257.zip"
deleted file mode 100644
index 8ac92c6..0000000
--- "a/\344\270\234\344\270\275\351\200\217\346\236\220\346\234\272\351\200\232\350\256\257.zip"
+++ /dev/null
Binary files differ
--
Gitblit v1.8.0