| New file |
| | |
| | | # 透析机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 |
| | | **状态**: 生产就绪 ✅ |
| New file |
| | |
| | | # 透析通讯服务 - 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+ |
| New file |
| | |
| | | { |
| | | "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服务器基础地址" |
| | | } |
| | | ] |
| | | } |
| New file |
| | |
| | | # 透析机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 |
| New file |
| | |
| | | # 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+ |
| New file |
| | |
| | | # 📖 第三方集成文档 |
| | | |
| | | **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 ✅* |
| | |
| | | { |
| | | "enabled": true, |
| | | "enabled": false, |
| | | "autoRegister": true |
| | | } |
| New file |
| | |
| | | // 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(); |
| New file |
| | |
| | | { |
| | | "enabled": true, |
| | | "port": 8080, |
| | | "host": "0.0.0.0", |
| | | "cors": { |
| | | "enabled": true, |
| | | "allowOrigin": "*" |
| | | }, |
| | | "rateLimit": { |
| | | "enabled": true, |
| | | "interval": 5000, |
| | | "allDevicesInterval": 60000 |
| | | } |
| | | } |
| New file |
| | |
| | | // 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; |
| | |
| | | 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) |
| | |
| | | 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); |
| | |
| | | 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}`; |
| | |
| | | const PORT = process.env.PORT || 10961; |
| | | server.listen(PORT, () => { |
| | | logger.info(`Socket 服务已启动,监听超级端口: ${PORT}`); |
| | | }); |
| | | }); |
| | | |
| | | // ========== 启动 HTTP 服务 ========== |
| | | const HTTP_PORT = process.env.HTTP_PORT || httpConfig.port || 8080; |
| | | const httpServer = new HttpServer(HTTP_PORT, httpConfig); |
| | | httpServer.start(); |
| New file |
| | |
| | | { |
| | | "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": "缓存和限流管理" |
| | | } |
| | | ] |
| | | } |
| New file |
| | |
| | | // 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(); |
| New file |
| | |
| | | // 快速验证属性映射 |
| | | 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('✅ 属性映射功能正常!'); |
| New file |
| | |
| | | // 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(); |
| New file |
| | |
| | | { |
| | | "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"} |
| | | ] |
| | | } |