Merge branch 'dev/0.8_dataBridge2' into dev/0.9_cockpit
# Conflicts: # .claude/settings.local.json # fastbee-plugs/fastbee-mqtt-client/src/main/java/com/fastbee/mqttclient/PubMqttCallBack.java
This commit is contained in:
commit
0f96d0236d
@ -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)"
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,13 @@ TARGET_PORT = 8888 # 请根据实际情况修改
|
||||
|
||||
# 元数组 modbus 指令
|
||||
MODBUS_DATA_HEX = (
|
||||
"7e80D20C4C6A55D57e",
|
||||
"01030400110018AA3C"
|
||||
"7e80DFEB502006887e",
|
||||
"01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99"
|
||||
)
|
||||
|
||||
# 重要-四川远德MQTT推送 (倾角计)
|
||||
# "7e803042593632517e",
|
||||
# "01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99"
|
||||
# 深部位移计
|
||||
# "7e80DD33D43DFC5B7e",
|
||||
# "0103204135317841ab8e39bf27bccd3f1fb19ac2b231bbc0b6f82040ae3207c3f9f80efa23",
|
||||
45
bin/script/send_sip_register_attack.py
Normal file
45
bin/script/send_sip_register_attack.py
Normal file
@ -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: <sip:{sip_id}@{target_ip}>;tag=12345\r
|
||||
To: <sip:{sip_id}@{target_ip}>\r
|
||||
Call-ID: test-{sip_id}@{local_ip}\r
|
||||
CSeq: 1 REGISTER\r
|
||||
Contact: <sip:{sip_id}@{local_ip}:{local_port}>\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'}")
|
||||
@ -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。
|
||||
|
||||
138
docs/scripts/data_bridge_script.groovy
Normal file
138
docs/scripts/data_bridge_script.groovy
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:";
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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升级实时数据
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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("初始化成功");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PlatformUserAuth> 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<PlatformUserAuth> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
);
|
||||
|
||||
|
||||
|
||||
@ -63,6 +63,12 @@
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis缓存 -->
|
||||
<dependency>
|
||||
<groupId>com.fastbee</groupId>
|
||||
<artifactId>fastbee-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@ -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<SipDeviceChannel> 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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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<RequestSchemaField> 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<RequestSchemaField> 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<FieldMappingItem> 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<ExtraMappingConfig.ItemFieldMapping> 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<RequestSchemaField> 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<RequestSchemaField> 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<PlatformDeviceSubMapping> 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<RequestSchemaField> 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<RequestSchemaField> requestSchema) {
|
||||
if (!StringUtils.hasText(payload)) {
|
||||
return null;
|
||||
}
|
||||
@ -415,12 +397,12 @@ public class DcpPlatformAdapter {
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping());
|
||||
List<FieldMappingItem> 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<FieldMappingItem> 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 {
|
||||
* <p>
|
||||
* 使用子设备 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<RequestSchemaField> 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 {
|
||||
* <p>
|
||||
* 复用主表 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<RequestSchemaField> 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<FieldMappingItem> 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<RequestSchemaField> requestSchema, JSONObject reportData,
|
||||
List<FieldMappingItem> 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<FieldMappingItem> 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<RequestSchemaField> requestSchema, JSONObject reportData, String fieldName,
|
||||
List<ExtraMappingConfig.ItemFieldMapping> itemFieldMappings, String subDeviceId) {
|
||||
RequestSchemaField arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName);
|
||||
List<RequestSchemaField> 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<ExtraMappingConfig.ItemFieldMapping> itemFieldMappings, String subDeviceId) {
|
||||
Map<String, Object> 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<ExtraMappingConfig.ItemFieldMapping> 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<RequestSchemaField> 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<RequestSchemaField> 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<RequestSchemaField> 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<FieldMappingItem> 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<FieldMappingItem> 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<ExtraMappingConfig.ItemFieldMapping> 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<String, Object> extractItemValues(JSONObject reportData, JSONArray itemFieldMappings) {
|
||||
private Map<String, Object> extractItemValues(JSONObject reportData,
|
||||
List<ExtraMappingConfig.ItemFieldMapping> itemFieldMappings) {
|
||||
Map<String, Object> 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<FieldMappingItem> 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<ExtraMappingConfig.ItemFieldMapping> 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<FieldMappingItem> 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<ExtraMappingConfig.ItemFieldMapping> 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<String, Object> 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<RequestSchemaField> 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<FieldMappingItem> fieldMappings = mapping.getFieldMappingParsed();
|
||||
String fieldName = resolveArrayFieldName(requestSchema);
|
||||
List<ExtraMappingConfig.ItemFieldMapping> 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.<PlatformFlow>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.<PlatformFlow>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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<FieldMappingItem> fieldMappingParsed;
|
||||
|
||||
/** extraMapping 类型安全解析 */
|
||||
@TableField(exist = false)
|
||||
@JsonIgnore
|
||||
private transient ExtraMappingConfig extraMappingParsed;
|
||||
|
||||
public List<FieldMappingItem> getFieldMappingParsed() {
|
||||
if (fieldMappingParsed == null && StringUtils.hasText(this.fieldMapping)) {
|
||||
this.fieldMappingParsed = JSON.parseArray(this.fieldMapping, FieldMappingItem.class);
|
||||
}
|
||||
return fieldMappingParsed;
|
||||
}
|
||||
|
||||
public void setFieldMappingParsed(List<FieldMappingItem> 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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<String, ParamOverrideConfig> paramMappingParsed;
|
||||
|
||||
/** resultSave 类型安全解析 */
|
||||
@TableField(exist = false)
|
||||
@JsonIgnore
|
||||
private transient List<ResultSaveField> resultSaveParsed;
|
||||
|
||||
public Map<String, ParamOverrideConfig> getParamMappingParsed() {
|
||||
if (paramMappingParsed == null && StringUtils.hasText(this.paramMapping)) {
|
||||
this.paramMappingParsed = JSON.parseObject(this.paramMapping,
|
||||
new TypeReference<Map<String,ParamOverrideConfig>>(){}.getType());
|
||||
}
|
||||
return paramMappingParsed;
|
||||
}
|
||||
|
||||
public void setParamMappingParsed(Map<String, ParamOverrideConfig> parsed) {
|
||||
this.paramMappingParsed = parsed;
|
||||
this.paramMapping = parsed == null ? null : JSON.toJSONString(parsed);
|
||||
}
|
||||
|
||||
public List<ResultSaveField> getResultSaveParsed() {
|
||||
if (resultSaveParsed == null && StringUtils.hasText(this.resultSave)) {
|
||||
this.resultSaveParsed = JSON.parseArray(this.resultSave, ResultSaveField.class);
|
||||
}
|
||||
return resultSaveParsed;
|
||||
}
|
||||
|
||||
public void setResultSaveParsed(List<ResultSaveField> parsed) {
|
||||
this.resultSaveParsed = parsed;
|
||||
this.resultSave = parsed == null ? null : JSON.toJSONString(parsed);
|
||||
}
|
||||
|
||||
@ApiModelProperty("备注")
|
||||
private String remark;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<RequestSchemaField> requestSchemaParsed;
|
||||
|
||||
/** responseSchema 类型安全解析 */
|
||||
@TableField(exist = false)
|
||||
@JsonIgnore
|
||||
private transient List<ResponseSchemaField> responseSchemaParsed;
|
||||
|
||||
public List<RequestSchemaField> getRequestSchemaParsed() {
|
||||
if (requestSchemaParsed == null && StringUtils.hasText(this.requestSchema)) {
|
||||
this.requestSchemaParsed = JSON.parseArray(this.requestSchema, RequestSchemaField.class);
|
||||
}
|
||||
return requestSchemaParsed;
|
||||
}
|
||||
|
||||
public void setRequestSchemaParsed(List<RequestSchemaField> parsed) {
|
||||
this.requestSchemaParsed = parsed;
|
||||
this.requestSchema = parsed == null ? null : JSON.toJSONString(parsed);
|
||||
}
|
||||
|
||||
public List<ResponseSchemaField> getResponseSchemaParsed() {
|
||||
if (responseSchemaParsed == null && StringUtils.hasText(this.responseSchema)) {
|
||||
this.responseSchemaParsed = JSON.parseArray(this.responseSchema, ResponseSchemaField.class);
|
||||
}
|
||||
return responseSchemaParsed;
|
||||
}
|
||||
|
||||
public void setResponseSchemaParsed(List<ResponseSchemaField> parsed) {
|
||||
this.responseSchemaParsed = parsed;
|
||||
this.responseSchema = parsed == null ? null : JSON.toJSONString(parsed);
|
||||
}
|
||||
|
||||
@ApiModelProperty("排序")
|
||||
private Integer sortOrder;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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<PlatformDeviceSubMapping> 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<PlatformDeviceSubMapping> 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<PlatformDeviceSubMapping> 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<PlatformDeviceSubMapping> getMonitorSubMappings() {
|
||||
return monitorSubMappings;
|
||||
}
|
||||
|
||||
public void setMonitorSubMappings(List<PlatformDeviceSubMapping> monitorSubMappings) {
|
||||
this.monitorSubMappings = monitorSubMappings;
|
||||
}
|
||||
|
||||
public String getSubMappingOnboardConfig() {
|
||||
return subMappingOnboardConfig;
|
||||
}
|
||||
|
||||
@ -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<String, Object> resolveParams(PlatformInterface iface, PlatformFlowStep step, FlowContext ctx) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
if (!StringUtils.hasText(iface.getRequestSchema())) {
|
||||
List<RequestSchemaField> schema = iface.getRequestSchemaParsed();
|
||||
if (schema == null || schema.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 合并接口默认 schema 和步骤覆盖 mapping
|
||||
JSONArray schema = JSON.parseArray(iface.getRequestSchema());
|
||||
Map<String, Object> overrides = StringUtils.hasText(step.getParamMapping())
|
||||
? JSON.parseObject(step.getParamMapping(), Map.class)
|
||||
: Collections.emptyMap();
|
||||
Map<String, ParamOverrideConfig> 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<String, Object> ov = (Map<String, Object>) 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<PlatformDeviceSubMapping> 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<String> resolveGeneratedFieldNames(PlatformInterface iface, PlatformFlowStep step) {
|
||||
LinkedHashMap<String, String> 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<RequestSchemaField> 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<String, ParamOverrideConfig> overrides = step.getParamMappingParsed();
|
||||
if (overrides != null) {
|
||||
for (Map.Entry<String, ParamOverrideConfig> 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<String> 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<String> 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<String> 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<String> 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<String> collectCascadedSubDeviceSensorIds(FlowContext ctx) {
|
||||
List<PlatformDeviceSubMapping> suffix = collectCascadedSubDeviceSuffix(ctx);
|
||||
Collections.reverse(suffix);
|
||||
return suffix.stream()
|
||||
.map(sub -> resolveOnboardSensorId(sub.getOnboardConfig(), sub.getSubSerialNumber()))
|
||||
.filter(StringUtils::hasText)
|
||||
List<String> 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<Object> collectCascadedSubDeviceDepths(FlowContext ctx, JSONObject fieldDef) {
|
||||
private List<Object> collectCascadedSubDeviceDepths(FlowContext ctx) {
|
||||
List<PlatformDeviceSubMapping> monitors = collectCascadedSubDeviceMonitors(ctx);
|
||||
int currentIndex = resolveCurrentMonitorIndex(ctx, monitors);
|
||||
List<Object> allDepths = resolveConfiguredDepths(fieldDef);
|
||||
if (allDepths.size() < monitors.size()) {
|
||||
throw new RuntimeException("串联子设备深度数组配置数量不足,监测点数量="
|
||||
+ monitors.size() + ",深度数量=" + allDepths.size());
|
||||
List<Object> 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<Object> 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<PlatformDeviceSubMapping> subMappings = subMappingMapper.selectByParentId(mapping.getId());
|
||||
List<PlatformDeviceSubMapping> monitors = subMappings == null ? Collections.emptyList() : subMappings.stream()
|
||||
.filter(PlatformDeviceSubMapping::isMonitor)
|
||||
.collect(Collectors.toList());
|
||||
List<PlatformDeviceSubMapping> monitors = ctx.getMonitorSubMappings();
|
||||
if (monitors == null) {
|
||||
List<PlatformDeviceSubMapping> 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<Object> 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<List<String>> 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<List<String>> 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<List<String>> 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<String, SubDeviceSensorBinding> 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<ExtraMappingConfig.ArrayFieldConfig> 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<ExtraMappingConfig.SensorIdMapping> 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<String, SubDeviceSensorBinding> bindings,
|
||||
JSONArray mappings, String referenceSerialNumber) {
|
||||
List<ExtraMappingConfig.SubDeviceMapping> 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<String, Object> params, String path) {
|
||||
Iterator<Map.Entry<String, Object>> it = params.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<String, Object> 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<ResponseSchemaField> 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<ResultSaveField> 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<PlatformFlow> wrapper =
|
||||
Wrappers.<PlatformFlow>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.<PlatformFlow>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PlatformDeviceSubMapping> 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<PlatformDeviceSubMapping> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PlatformUserAuth> {
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*/
|
||||
List<PlatformUserAuth> selectList(PlatformUserAuth query);
|
||||
|
||||
/**
|
||||
* 根据平台ID查询已初始化的用户列表
|
||||
*/
|
||||
List<PlatformUserAuth> selectInitializedByPlatformId(@Param("platformId") Long platformId);
|
||||
|
||||
/**
|
||||
* 根据平台ID和用户ID查询
|
||||
*/
|
||||
PlatformUserAuth selectByPlatformAndUserId(@Param("platformId") Long platformId, @Param("platformUserId") String platformUserId);
|
||||
|
||||
Integer countEffectivePlatformUser(@Param("platformId") Long platformId);
|
||||
}
|
||||
@ -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<ItemFieldMapping> itemFieldMappings;
|
||||
|
||||
/** 批量推送数组字段子设备映射配置 */
|
||||
private List<ArrayFieldConfig> 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<SubDeviceMapping> 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<SensorIdMapping> sensorIdMappings;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SensorIdMapping implements Serializable {
|
||||
private String serialNumber;
|
||||
private String sensorId;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
@ -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<RequestSchemaField> itemSchema;
|
||||
|
||||
/** type=array 时的数组模式:single/reference_pair */
|
||||
private String arrayMode;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -10,21 +10,9 @@ public interface IPlatformFlowService extends IService<PlatformFlow> {
|
||||
|
||||
List<PlatformFlow> selectList(PlatformFlow query);
|
||||
|
||||
/** 查询某平台指定类型的流程 */
|
||||
PlatformFlow selectByPlatformAndType(Long platformId, Integer flowType);
|
||||
|
||||
/** 查询某平台指定类型+传感器类型的流程(设备入网用) */
|
||||
PlatformFlow selectByPlatformTypeAndSensor(Long platformId, Integer flowType, String sensorType);
|
||||
|
||||
/** 查询流程的有序步骤列表 */
|
||||
List<PlatformFlowStep> selectSteps(Long flowId);
|
||||
|
||||
/** 手动触发流程执行 */
|
||||
void executeManually(Long flowId);
|
||||
|
||||
/** 手动触发流程执行(设备入网流程需传 mappingId) */
|
||||
void executeManually(Long flowId, Long mappingId);
|
||||
|
||||
/** 保存流程及步骤(含步骤的新增/更新/删除) */
|
||||
void saveWithSteps(PlatformFlow flow, List<PlatformFlowStep> steps);
|
||||
}
|
||||
|
||||
@ -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<PlatformInstance> {
|
||||
|
||||
List<PlatformInstance> 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);
|
||||
}
|
||||
|
||||
@ -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<PlatformUserAuth> {
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*/
|
||||
List<PlatformUserAuth> selectList(PlatformUserAuth query);
|
||||
|
||||
/**
|
||||
* 根据平台ID查询已初始化的用户列表
|
||||
*/
|
||||
List<PlatformUserAuth> 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<java.util.Map<String, Object>> getProjectsByUserAuthId(Long userAuthId);
|
||||
}
|
||||
@ -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<PlatformDevice
|
||||
private PlatformFlowExecutor flowExecutor;
|
||||
@Autowired
|
||||
private IPlatformDeviceSubMappingService subMappingService;
|
||||
@Autowired
|
||||
private PlatformUserAuthMapper platformUserAuthMapper;
|
||||
|
||||
@Override
|
||||
public List<PlatformDeviceMapping> selectList(PlatformDeviceMapping query) {
|
||||
@ -77,6 +78,11 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
|
||||
throw new RuntimeException("平台不存在: " + mapping.getPlatformId());
|
||||
}
|
||||
|
||||
PlatformUserAuth platformUserAuth = platformUserAuthMapper.selectById(mapping.getPlatformUserAuthId());
|
||||
if (platformUserAuth == null) {
|
||||
throw new RuntimeException("平台 :" + mapping.getPlatformId() + "不存在用户授权信息,请到平台管理-用户-新增用户信息");
|
||||
}
|
||||
|
||||
PlatformFlow onboardFlow = flowMapper.selectOne(Wrappers.<PlatformFlow>lambdaQuery()
|
||||
.eq(PlatformFlow::getPlatformId, mapping.getPlatformId())
|
||||
.eq(PlatformFlow::getFlowType, 2)
|
||||
@ -88,17 +94,17 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
|
||||
}
|
||||
|
||||
if (mapping.getDeviceType() != null && mapping.getDeviceType() == 1) {
|
||||
onboardGateway(mapping, platform, onboardFlow);
|
||||
onboardGateway(mapping, platform, platformUserAuth, onboardFlow);
|
||||
} else {
|
||||
onboardDirect(mapping, platform, onboardFlow);
|
||||
onboardDirect(mapping, platform, platformUserAuth, onboardFlow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直连设备入网:结果写主表 platform_device_id / platform_point_id
|
||||
*/
|
||||
private void onboardDirect(PlatformDeviceMapping mapping, PlatformInstance platform, PlatformFlow onboardFlow) {
|
||||
FlowContext ctx = new FlowContext(platform, mapping, null);
|
||||
private void onboardDirect(PlatformDeviceMapping mapping, PlatformInstance platform, PlatformUserAuth platformUserAuth, PlatformFlow onboardFlow) {
|
||||
FlowContext ctx = new FlowContext(platform, mapping, platformUserAuth);
|
||||
flowExecutor.execute(onboardFlow, ctx);
|
||||
|
||||
// persistResultSave 已在流程执行中回填了主表字段,重新读取
|
||||
@ -112,7 +118,7 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
|
||||
/**
|
||||
* 网关设备入网:遍历子表中 device_role=monitor 且未入网的行,逐行执行入网流程
|
||||
*/
|
||||
private void onboardGateway(PlatformDeviceMapping mapping, PlatformInstance platform, PlatformFlow onboardFlow) {
|
||||
private void onboardGateway(PlatformDeviceMapping mapping, PlatformInstance platform, PlatformUserAuth platformUserAuth, PlatformFlow onboardFlow) {
|
||||
List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId());
|
||||
PlatformDeviceSubMapping reference = resolveGatewayReference(mapping, subMappings);
|
||||
List<PlatformDeviceSubMapping> monitors = subMappings.stream()
|
||||
@ -128,11 +134,8 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
|
||||
|
||||
for (int i = 0; i < monitors.size(); i++) {
|
||||
PlatformDeviceSubMapping monitor = monitors.get(i);
|
||||
FlowContext ctx = new FlowContext(platform, mapping, null);
|
||||
ctx.setCurrentMonitorSerialNumber(monitor.getSubSerialNumber());
|
||||
ctx.setCurrentMonitorDeviceName(monitor.getSubDeviceName());
|
||||
ctx.setCurrentMonitorIndex(i);
|
||||
ctx.setSubMappingOnboardConfig(monitor.getOnboardConfig());
|
||||
FlowContext ctx = new FlowContext(platform, platformUserAuth, mapping, monitors, monitor.getSubSerialNumber(),
|
||||
monitor.getSubDeviceName(), i, monitor.getOnboardConfig());
|
||||
if (reference != null) {
|
||||
ctx.setCurrentReferenceSerialNumber(reference.getSubSerialNumber());
|
||||
ctx.setCurrentReferenceDeviceName(reference.getSubDeviceName());
|
||||
|
||||
@ -3,17 +3,8 @@ 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.common.utils.spring.SpringUtils;
|
||||
import com.fastbee.iot.platform.domain.PlatformDeviceMapping;
|
||||
import com.fastbee.iot.platform.domain.PlatformFlow;
|
||||
import com.fastbee.iot.platform.domain.PlatformFlowStep;
|
||||
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.PlatformDeviceMappingMapper;
|
||||
import com.fastbee.iot.platform.mapper.PlatformFlowMapper;
|
||||
import com.fastbee.iot.platform.mapper.PlatformFlowStepMapper;
|
||||
import com.fastbee.iot.platform.mapper.PlatformInstanceMapper;
|
||||
import com.fastbee.iot.platform.domain.*;
|
||||
import com.fastbee.iot.platform.mapper.*;
|
||||
import com.fastbee.iot.platform.service.IPlatformFlowService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -28,10 +19,6 @@ public class PlatformFlowServiceImpl extends ServiceImpl<PlatformFlowMapper, Pla
|
||||
|
||||
@Autowired
|
||||
private PlatformFlowStepMapper stepMapper;
|
||||
@Autowired
|
||||
private PlatformInstanceMapper instanceMapper;
|
||||
@Autowired
|
||||
private PlatformFlowExecutor flowExecutor;
|
||||
|
||||
@Override
|
||||
public List<PlatformFlow> selectList(PlatformFlow query) {
|
||||
@ -43,58 +30,11 @@ public class PlatformFlowServiceImpl extends ServiceImpl<PlatformFlowMapper, Pla
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformFlow selectByPlatformAndType(Long platformId, Integer flowType) {
|
||||
return this.getOne(Wrappers.<PlatformFlow>lambdaQuery()
|
||||
.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.<PlatformFlow>lambdaQuery()
|
||||
.eq(PlatformFlow::getPlatformId, platformId)
|
||||
.eq(PlatformFlow::getFlowType, flowType)
|
||||
.eq(PlatformFlow::getSensorType, sensorType)
|
||||
.eq(PlatformFlow::getEnable, "1")
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PlatformFlowStep> 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<PlatformFlowStep> steps) {
|
||||
|
||||
@ -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<PlatformInstanceMapper, PlatformInstance>
|
||||
implements IPlatformInstanceService {
|
||||
|
||||
@Autowired
|
||||
private PlatformFlowMapper flowMapper;
|
||||
@Autowired
|
||||
private PlatformFlowExecutor flowExecutor;
|
||||
private PlatformUserAuthMapper platformUserAuthMapper;
|
||||
|
||||
@Override
|
||||
public List<PlatformInstance> selectList(PlatformInstance query) {
|
||||
@ -34,45 +27,12 @@ public class PlatformInstanceServiceImpl extends ServiceImpl<PlatformInstanceMap
|
||||
.eq(StringUtils.hasText(query.getEnable()), PlatformInstance::getEnable, query.getEnable())
|
||||
.like(StringUtils.hasText(query.getName()), PlatformInstance::getName, query.getName())
|
||||
.orderByAsc(PlatformInstance::getId);
|
||||
return this.list(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Long platformId) {
|
||||
PlatformInstance platform = this.getById(platformId);
|
||||
if (platform == null) throw new RuntimeException("平台不存在: " + platformId);
|
||||
PlatformFlow initFlow = flowMapper.selectOne(Wrappers.<PlatformFlow>lambdaQuery()
|
||||
.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<PlatformInstance> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PlatformUserAuthMapper, PlatformUserAuth>
|
||||
implements IPlatformUserAuthService {
|
||||
|
||||
@Autowired
|
||||
private PlatformInstanceMapper instanceMapper;
|
||||
@Autowired
|
||||
private PlatformFlowMapper flowMapper;
|
||||
@Autowired
|
||||
private PlatformFlowExecutor flowExecutor;
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Override
|
||||
public List<PlatformUserAuth> selectList(PlatformUserAuth query) {
|
||||
return baseMapper.selectList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PlatformUserAuth> 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.<PlatformFlow>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<Map<String, Object>> 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<Map<String, Object>>) 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<Map<String, Object>> result = parseProjectsResponse(responseBody);
|
||||
redisCache.setCacheObject(key, result, 10, TimeUnit.MINUTES);
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> parseProjectsResponse(String responseBody) {
|
||||
String trimmed = responseBody.trim();
|
||||
List<Map<String, Object>> 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<Map<String, Object>>) data;
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
return rawList.stream()
|
||||
.map(m -> {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("id", m.get("id"));
|
||||
item.put("name", m.get("name"));
|
||||
return item;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@ -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<PlatformDeviceSubMapping> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,50 +14,119 @@ public class MqttClientFactory {
|
||||
|
||||
private static final Map<String, MqttAsyncClient> CLIENT_CACHE = new ConcurrentHashMap<>();
|
||||
private static final Map<String, MqttConnectOptions> CLIENT_OPTIONS_CACHE = new ConcurrentHashMap<>();
|
||||
// 每个key独立的锁对象,避免同一bridge并发创建/重连时产生连接泄漏
|
||||
private static final Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<BridgeMapper, Bridge> 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<BridgeMapper, Bridge> 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<BridgeMapper, Bridge> 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<BridgeMapper, Bridge> 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) {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -3,28 +3,31 @@
|
||||
<mapper namespace="com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper">
|
||||
|
||||
<resultMap type="com.fastbee.iot.platform.domain.PlatformDeviceMapping" id="PlatformDeviceMappingResult">
|
||||
<result property="id" column="id"/>
|
||||
<result property="platformId" column="platform_id"/>
|
||||
<result property="deviceId" column="device_id"/>
|
||||
<result property="serialNumber" column="serial_number"/>
|
||||
<result property="deviceName" column="device_name"/>
|
||||
<result property="platformDeviceId" column="platform_device_id"/>
|
||||
<result property="platformPointId" column="platform_point_id"/>
|
||||
<result property="sensorType" column="sensor_type"/>
|
||||
<result property="deviceType" column="device_type"/>
|
||||
<result property="pushInterfaceId" column="push_interface_id"/>
|
||||
<result property="fieldMapping" column="field_mapping"/>
|
||||
<result property="extraMapping" column="extra_mapping"/>
|
||||
<result property="netStatus" column="net_status"/>
|
||||
<result property="enable" column="enable"/>
|
||||
<result property="pushMode" column="push_mode"/>
|
||||
<result property="pushInterval" column="push_interval"/>
|
||||
<result property="lastPushTime" column="last_push_time"/>
|
||||
<result property="remark" column="remark"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
<result property="id" column="id"/>
|
||||
<result property="platformId" column="platform_id"/>
|
||||
<result property="platformUserAuthId" column="platform_user_auth_id"/>
|
||||
<result property="platformProjectId" column="platform_project_id"/>
|
||||
<result property="platformProjectName" column="platform_project_name"/>
|
||||
<result property="deviceId" column="device_id"/>
|
||||
<result property="serialNumber" column="serial_number"/>
|
||||
<result property="deviceName" column="device_name"/>
|
||||
<result property="platformDeviceId" column="platform_device_id"/>
|
||||
<result property="platformPointId" column="platform_point_id"/>
|
||||
<result property="sensorType" column="sensor_type"/>
|
||||
<result property="deviceType" column="device_type"/>
|
||||
<result property="pushInterfaceId" column="push_interface_id"/>
|
||||
<result property="fieldMapping" column="field_mapping"/>
|
||||
<result property="extraMapping" column="extra_mapping"/>
|
||||
<result property="netStatus" column="net_status"/>
|
||||
<result property="enable" column="enable"/>
|
||||
<result property="pushMode" column="push_mode"/>
|
||||
<result property="pushInterval" column="push_interval"/>
|
||||
<result property="lastPushTime" column="last_push_time"/>
|
||||
<result property="remark" column="remark"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectBySerialNumber" resultMap="PlatformDeviceMappingResult">
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.fastbee.iot.platform.mapper.PlatformUserAuthMapper">
|
||||
|
||||
<resultMap type="com.fastbee.iot.platform.domain.PlatformUserAuth" id="PlatformUserAuthResult">
|
||||
<id property="id" column="id"/>
|
||||
<result property="platformId" column="platform_id"/>
|
||||
<result property="platformUserId" column="platform_user_id"/>
|
||||
<result property="platformUserName" column="platform_user_name"/>
|
||||
<result property="authConfig" column="auth_config"/>
|
||||
<result property="accessToken" column="access_token"/>
|
||||
<result property="tokenExpiresAt" column="token_expires_at"/>
|
||||
<result property="initStatus" column="init_status"/>
|
||||
<result property="extraConfig" column="extra_config"/>
|
||||
<result property="enable" column="enable"/>
|
||||
<result property="remark" column="remark"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectColumns">
|
||||
id, platform_id, platform_user_id, platform_user_name, auth_config, access_token,
|
||||
token_expires_at, init_status, extra_config, enable, remark,
|
||||
create_by, create_time, update_by, update_time
|
||||
</sql>
|
||||
|
||||
<select id="selectList" parameterType="com.fastbee.iot.platform.domain.PlatformUserAuth" resultMap="PlatformUserAuthResult">
|
||||
select <include refid="selectColumns"/>
|
||||
from platform_user_auth
|
||||
<where>
|
||||
<if test="platformId != null">
|
||||
AND platform_id = #{platformId}
|
||||
</if>
|
||||
<if test="platformUserId != null and platformUserId != ''">
|
||||
AND platform_user_id like concat('%', #{platformUserId}, '%')
|
||||
</if>
|
||||
<if test="platformUserName != null and platformUserName != ''">
|
||||
AND platform_user_name like concat('%', #{platformUserName}, '%')
|
||||
</if>
|
||||
<if test="initStatus != null">
|
||||
AND init_status = #{initStatus}
|
||||
</if>
|
||||
<if test="enable != null and enable != ''">
|
||||
AND enable = #{enable}
|
||||
</if>
|
||||
</where>
|
||||
order by id desc
|
||||
</select>
|
||||
|
||||
<select id="selectInitializedByPlatformId" parameterType="Long" resultMap="PlatformUserAuthResult">
|
||||
select <include refid="selectColumns"/>
|
||||
from platform_user_auth
|
||||
where platform_id = #{platformId}
|
||||
and init_status = 1
|
||||
and enable = '1'
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<select id="selectByPlatformAndUserId" resultMap="PlatformUserAuthResult">
|
||||
select <include refid="selectColumns"/>
|
||||
from platform_user_auth
|
||||
where platform_id = #{platformId}
|
||||
and platform_user_id = #{platformUserId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="countEffectivePlatformUser" resultType="java.lang.Integer">
|
||||
SELECT count(1) FROM platform_user_auth WHERE platform_id = #{platformId} AND init_status = 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@ -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},
|
||||
|
||||
@ -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`);
|
||||
@ -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' })
|
||||
}
|
||||
|
||||
@ -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' })
|
||||
}
|
||||
|
||||
33
yack-web/src/api/platform/userAuth.js
Normal file
33
yack-web/src/api/platform/userAuth.js
Normal file
@ -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' })
|
||||
}
|
||||
@ -114,7 +114,7 @@
|
||||
<el-divider content-position="left">入参定义(request_schema)</el-divider>
|
||||
<div class="schema-tip">
|
||||
定义接口需要的入参字段。参数来源使用业务化名称:
|
||||
固定值、认证参数、平台参数、上一步结果、当前设备、自动UUID、子设备传感器列表、串联子设备传感器列表、串联子设备深度数组、基准点成对传感器列表、设备物模型、上报时间。数组字段可配置组包模式。
|
||||
固定值、认证参数、平台参数、上一步结果、当前设备、自动UUID、子设备传感器列表、串联子设备传感器列表、入网深度数组、基准点成对传感器列表、设备物模型、上报时间。数组字段可配置组包模式。
|
||||
</div>
|
||||
<el-table :data="requestSchemaRows" size="mini" border style="width:100%;margin-top:8px">
|
||||
<el-table-column label="字段名" width="130">
|
||||
@ -145,7 +145,7 @@
|
||||
<el-option label="自动UUID" value="uuid" />
|
||||
<el-option label="子设备传感器列表" value="subDeviceSensorIds" />
|
||||
<el-option label="串联子设备传感器列表" value="cascadedSubDeviceSensorIds" />
|
||||
<el-option label="串联子设备深度数组" value="cascadedSubDeviceDepths" />
|
||||
<el-option label="入网深度数组" value="cascadedSubDeviceDepths" />
|
||||
<el-option label="基准点成对传感器列表" value="referenceSubDeviceSensorIdPairs" />
|
||||
<el-option label="设备物模型" value="thingsModel" />
|
||||
<el-option label="上报时间" value="ts" />
|
||||
@ -170,6 +170,7 @@
|
||||
<el-input v-else-if="scope.row.source === 'context'" v-model="scope.row.contextKey" size="mini" placeholder="流程页可选择,如 step1.id" />
|
||||
<!-- device: 设备字段 -->
|
||||
<el-select v-else-if="scope.row.source === 'device'" v-model="scope.row.field" size="mini" style="width:100%">
|
||||
<el-option label="平台项目ID" value="platform_project_id" />
|
||||
<el-option label="平台设备ID" value="platform_device_id" />
|
||||
<el-option label="平台测点ID" value="platform_point_id" />
|
||||
<el-option label="系统设备编号" value="serial_number" />
|
||||
|
||||
@ -64,6 +64,12 @@
|
||||
<el-table-column label="平台" width="120">
|
||||
<template slot-scope="scope">{{ platformName(scope.row.platformId) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="项目" min-width="120" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.platformProjectName">{{ scope.row.platformProjectName }}</span>
|
||||
<span v-else style="color:#909399">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备名称" min-width="140">
|
||||
<template slot-scope="scope">{{ scope.row.deviceName || deviceName(scope.row.deviceId) }}</template>
|
||||
</el-table-column>
|
||||
@ -128,6 +134,16 @@
|
||||
<el-option v-for="p in platformList" :key="p.id" :label="p.name" :value="p.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属用户" prop="platformUserAuthId">
|
||||
<el-select v-model="form.platformUserAuthId" placeholder="请选择已初始化的用户" style="width:100%" @change="onUserAuthChange" :disabled="!form.platformId || form.netStatus === 1" clearable>
|
||||
<el-option v-for="u in initializedUserList" :key="u.id" :label="u.platformUserName || u.platformUserId" :value="u.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属项目" prop="platformProjectId">
|
||||
<el-select v-model="form.platformProjectId" placeholder="请选择项目" style="width:100%" @change="onProjectChange" :disabled="!form.platformUserAuthId || form.netStatus === 1" clearable>
|
||||
<el-option v-for="proj in projectOptions" :key="proj.id" :label="proj.name" :value="String(proj.id)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系统设备" prop="deviceId">
|
||||
<el-select v-model="form.deviceId" placeholder="选择直连或网关设备,支持名称/编号搜索" filterable remote clearable
|
||||
:remote-method="searchDevice" :loading="deviceSearchLoading" style="width:100%"
|
||||
@ -229,6 +245,23 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template v-if="form.sensorType === 'deep_inclino'">
|
||||
<el-table-column label="传感器深度(m)" min-width="140">
|
||||
<template slot-scope="scope">
|
||||
<el-input-number v-model="scope.row.sensorDepth" :min="0" :precision="2" size="mini" style="width:100%" @change="refreshOnboardConfig" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="孔深(m)" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-input-number v-model="scope.row.holeDepth" :min="0" :precision="2" size="mini" style="width:100%" @change="syncHoleDepthToAllMonitors" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="孔口高程(m)" min-width="140">
|
||||
<template slot-scope="scope">
|
||||
<el-input-number v-model="scope.row.holeElevation" :min="0" :precision="2" size="mini" style="width:100%" @change="syncHoleElevationToAllMonitors" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column label="入网参数 (JSON)" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.onboardConfig" size="mini" :placeholder="getOnboardConfigPlaceholder(scope.row)" />
|
||||
@ -270,18 +303,16 @@
|
||||
<!-- 标量字段映射 -->
|
||||
<el-form-item v-if="fieldMappingRows.length > 0" label="字段映射">
|
||||
<el-table :data="fieldMappingRows" size="mini" border style="width:100%">
|
||||
<el-table-column label="平台字段" min-width="160">
|
||||
<template slot-scope="scope">{{ scope.row.platformField }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物模型标识" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-select v-model="scope.row.identifier" size="mini" placeholder="选择物模型字段" filterable style="width:100%">
|
||||
<el-option v-for="tm in uniqueThingsModelOptions" :key="tm.identifier" :label="`${tm.modelName}(${tm.identifier})`" :value="tm.identifier" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平台字段" min-width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.platformField" size="mini" placeholder="平台字段名" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="fieldMappingRows.splice(scope.$index,1)" />
|
||||
@ -306,6 +337,7 @@
|
||||
<script>
|
||||
import { listPlatformDeviceMapping, addPlatformDeviceMapping, updatePlatformDeviceMapping, delPlatformDeviceMapping, onboardDevice, listSubMappings, syncSubMappings } from '@/api/platform/deviceMapping'
|
||||
import { listPlatformInstance } from '@/api/platform/instance'
|
||||
import { getInitializedUsers, getProjectsByUserAuthId } from '@/api/platform/userAuth'
|
||||
import { listPlatformInterface } from '@/api/platform/interface'
|
||||
import { listPlatformFlow, getPlatformFlow } from '@/api/platform/flow'
|
||||
import { listDevice, listThingsModel, getDevice, listDeviceShort } from '@/api/iot/device'
|
||||
@ -323,6 +355,8 @@ export default {
|
||||
subDeviceOptions: [],
|
||||
deviceSearchLoading: false, thingsModelOptions: [],
|
||||
interfaceOptions: [],
|
||||
projectOptions: [],
|
||||
initializedUserList: [],
|
||||
open: false, title: '',
|
||||
fieldMappingRows: [],
|
||||
// 网关子设备映射行
|
||||
@ -333,9 +367,16 @@ export default {
|
||||
subMappingCache: {},
|
||||
sensorTypeDictOptions: [],
|
||||
queryParams: { pageNum: 1, pageSize: 20, platformId: null, serialNumber: '', sensorType: '', netStatus: null },
|
||||
form: { enable: '1', deviceType: 0, reportIntervalSec: 300, deviceName: '' },
|
||||
form: {
|
||||
enable: '1', deviceType: 0, reportIntervalSec: 300, deviceName: '',
|
||||
platformId: null, platformUserAuthId: null, platformProjectId: null, platformProjectName: '',
|
||||
pushInterfaceId: null, pushMode: 1, pushInterval: 5,
|
||||
deviceId: null, serialNumber: '', sensorType: '', remark: ''
|
||||
},
|
||||
rules: {
|
||||
platformId: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
||||
platformUserAuthId: [{ required: true, message: '请选择用户', trigger: 'change' }],
|
||||
platformProjectId: [{ required: true, message: '请选择所属项目', trigger: 'change' }],
|
||||
deviceId: [{ required: true, message: '请选择设备', trigger: 'change' }],
|
||||
sensorType: [{ required: true, message: '请选择或输入设备类型', trigger: 'change' }],
|
||||
pushInterfaceId: [{ required: true, message: '请选择推送接口', trigger: 'change' }]
|
||||
@ -433,7 +474,11 @@ export default {
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
|
||||
handleAdd() {
|
||||
this.form = { enable: '1', deviceType: 0, pushInterfaceId: null, pushMode: 1, pushInterval: 5, reportIntervalSec: 300, deviceName: '' }
|
||||
this.form = {
|
||||
enable: '1', deviceType: 0, pushInterfaceId: null, pushMode: 1, pushInterval: 5, reportIntervalSec: 300, deviceName: '',
|
||||
platformId: null, platformUserAuthId: null, platformProjectId: null, platformProjectName: '',
|
||||
deviceId: null, serialNumber: '', sensorType: '', remark: ''
|
||||
}
|
||||
this.fieldMappingRows = []
|
||||
this.subMappingRows = []
|
||||
this.itemFieldMappingRows = []
|
||||
@ -441,6 +486,8 @@ export default {
|
||||
this.subDeviceOptions = []
|
||||
this.thingsModelOptions = []
|
||||
this.interfaceOptions = []
|
||||
this.projectOptions = []
|
||||
this.initializedUserList = []
|
||||
this.title = '新增设备绑定'; this.open = true
|
||||
},
|
||||
handleUpdate(row) {
|
||||
@ -456,14 +503,28 @@ export default {
|
||||
this.itemFieldMappingRows = extra.itemFieldMappings || []
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
// 加载该平台的接口列表
|
||||
// 加载该平台的接口列表和用户列表
|
||||
if (row.platformId) {
|
||||
this.loadInterfaces(row.platformId)
|
||||
this.loadInitializedUsers(row.platformId)
|
||||
}
|
||||
// 加载用户的项目列表
|
||||
if (row.platformUserAuthId) {
|
||||
this.loadProjectsByUserAuth(row.platformUserAuthId)
|
||||
}
|
||||
// 加载子设备映射(网关设备)
|
||||
if (row.deviceType === 1 && row.id) {
|
||||
listSubMappings(row.id).then(res => {
|
||||
this.subMappingRows = (res.data || []).map(s => ({ ...s, subDeviceName: s.subDeviceName || s.subSerialNumber }))
|
||||
this.subMappingRows = (res.data || []).map(s => {
|
||||
const config = this.safeParseJson(s.onboardConfig, {})
|
||||
return {
|
||||
...s,
|
||||
subDeviceName: s.subDeviceName || s.subSerialNumber,
|
||||
sensorDepth: config.sensorDepth != null ? config.sensorDepth : null,
|
||||
holeDepth: config.holeDepth != null ? config.holeDepth : null,
|
||||
holeElevation: config.holeElevation != null ? config.holeElevation : null
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
this.loadDeviceContext(row.deviceId)
|
||||
@ -485,7 +546,43 @@ export default {
|
||||
onPlatformChange(platformId) {
|
||||
this.interfaceOptions = []
|
||||
this.form.pushInterfaceId = null
|
||||
if (platformId) this.loadInterfaces(platformId)
|
||||
this.initializedUserList = []
|
||||
this.form.platformUserAuthId = null
|
||||
this.projectOptions = []
|
||||
this.form.platformProjectId = null
|
||||
this.form.platformProjectName = ''
|
||||
if (platformId) {
|
||||
this.loadInterfaces(platformId)
|
||||
this.loadInitializedUsers(platformId)
|
||||
}
|
||||
},
|
||||
loadInitializedUsers(platformId) {
|
||||
if (!platformId) { this.initializedUserList = []; return }
|
||||
getInitializedUsers(platformId).then(res => {
|
||||
this.initializedUserList = res.data || []
|
||||
}).catch(() => { this.initializedUserList = [] })
|
||||
},
|
||||
onUserAuthChange(userAuthId) {
|
||||
this.projectOptions = []
|
||||
this.form.platformProjectId = null
|
||||
this.form.platformProjectName = ''
|
||||
if (userAuthId) {
|
||||
this.loadProjectsByUserAuth(userAuthId)
|
||||
}
|
||||
},
|
||||
loadProjectsByUserAuth(userAuthId) {
|
||||
if (!userAuthId) { this.projectOptions = []; return }
|
||||
getProjectsByUserAuthId(userAuthId).then(res => {
|
||||
this.projectOptions = res.data || []
|
||||
}).catch(() => { this.projectOptions = [] })
|
||||
},
|
||||
onProjectChange(projectId) {
|
||||
if (projectId) {
|
||||
const proj = this.projectOptions.find(p => String(p.id) === String(projectId))
|
||||
this.form.platformProjectName = proj ? proj.name : ''
|
||||
} else {
|
||||
this.form.platformProjectName = ''
|
||||
}
|
||||
},
|
||||
onDeviceTypeChange(val) {
|
||||
if (val === 1 && this.subMappingRows.length === 0 && this.subDeviceOptions.length > 0) {
|
||||
@ -508,6 +605,15 @@ export default {
|
||||
})
|
||||
},
|
||||
onPushInterfaceChange(interfaceId) {
|
||||
// 根据接口的 isBatchPush 自动设置推送模式:0=实时推送(1),1=定时推送(2)
|
||||
if (interfaceId) {
|
||||
const iface = this.interfaceOptions.find(x => x.id === interfaceId)
|
||||
if (iface) {
|
||||
this.form.pushMode = iface.isBatchPush === 1 ? 2 : 1
|
||||
}
|
||||
} else {
|
||||
this.form.pushMode = 1
|
||||
}
|
||||
// 从推送接口的 requestSchema 提取字段作为数据映射模板,切换接口时全量刷新
|
||||
if (!interfaceId) return
|
||||
const iface = this.interfaceOptions.find(x => x.id === interfaceId)
|
||||
@ -654,7 +760,7 @@ export default {
|
||||
},
|
||||
addFieldMapping() { this.fieldMappingRows.push({ identifier: '', platformField: '' }) },
|
||||
addSubMapping() {
|
||||
this.subMappingRows.push({ subSerialNumber: '', subDeviceName: '', deviceRole: 'monitor', onboardConfig: '', platformDeviceId: '', platformPointId: '' })
|
||||
this.subMappingRows.push({ subSerialNumber: '', subDeviceName: '', deviceRole: 'monitor', onboardConfig: '', sensorDepth: null, holeDepth: null, holeElevation: null, platformDeviceId: '', platformPointId: '' })
|
||||
},
|
||||
onSubRoleChange(row) {
|
||||
if (row.deviceRole === 'reference') {
|
||||
@ -665,6 +771,7 @@ export default {
|
||||
})
|
||||
}
|
||||
this.refreshSubMappingOnboardConfigs()
|
||||
if (this.form.sensorType === 'deep_inclino') this.refreshOnboardConfig()
|
||||
},
|
||||
onSubSerialChange(row) {
|
||||
if (!row.subSerialNumber) return
|
||||
@ -676,6 +783,7 @@ export default {
|
||||
config.sensorId = config.sensorId || row.subSerialNumber
|
||||
row.onboardConfig = JSON.stringify(config)
|
||||
this.refreshSubMappingOnboardConfigs()
|
||||
if (this.form.sensorType === 'deep_inclino') this.refreshOnboardConfig()
|
||||
},
|
||||
findReferenceRow(rows = this.subMappingRows) {
|
||||
return (rows || []).find(r => r.deviceRole === 'reference' && r.subSerialNumber)
|
||||
@ -706,10 +814,42 @@ export default {
|
||||
row.onboardConfig = this.buildOnboardConfig(row, refRow)
|
||||
})
|
||||
},
|
||||
refreshOnboardConfig() {
|
||||
this.subMappingRows.forEach(row => {
|
||||
const existing = this.safeParseJson(row.onboardConfig, {})
|
||||
if (!existing.sensorId) existing.sensorId = row.subSerialNumber
|
||||
if (this.form.sensorType === 'deep_inclino' && row.deviceRole === 'monitor') {
|
||||
if (row.sensorDepth != null) existing.sensorDepth = row.sensorDepth
|
||||
if (row.holeDepth != null) existing.holeDepth = row.holeDepth
|
||||
if (row.holeElevation != null) existing.holeElevation = row.holeElevation
|
||||
} else {
|
||||
delete existing.sensorDepth
|
||||
delete existing.holeDepth
|
||||
delete existing.holeElevation
|
||||
}
|
||||
row.onboardConfig = JSON.stringify(existing)
|
||||
})
|
||||
},
|
||||
syncHoleDepthToAllMonitors(value) {
|
||||
this.subMappingRows.forEach(row => {
|
||||
if (row.deviceRole === 'monitor') row.holeDepth = value
|
||||
})
|
||||
this.refreshOnboardConfig()
|
||||
},
|
||||
syncHoleElevationToAllMonitors(value) {
|
||||
this.subMappingRows.forEach(row => {
|
||||
if (row.deviceRole === 'monitor') row.holeElevation = value
|
||||
})
|
||||
this.refreshOnboardConfig()
|
||||
},
|
||||
normalizeSubMappingsForSubmit() {
|
||||
const rows = (this.subMappingRows || [])
|
||||
.filter(r => r.subSerialNumber)
|
||||
.map(r => ({ ...r, deviceRole: r.deviceRole || 'monitor' }))
|
||||
if (new Set(rows.map(r => r.subSerialNumber)).size !== rows.length) {
|
||||
this.$modal.msgError('子设备编号不能重复')
|
||||
return null
|
||||
}
|
||||
const references = rows.filter(r => r.deviceRole === 'reference')
|
||||
const monitors = rows.filter(r => r.deviceRole === 'monitor')
|
||||
if (references.length > 1) {
|
||||
@ -720,11 +860,27 @@ export default {
|
||||
this.$modal.msgError('静力水准需配置 1 个基准点和至少 1 个监测点')
|
||||
return null
|
||||
}
|
||||
if (this.form.sensorType === 'deep_inclino') {
|
||||
if (monitors.length === 0) {
|
||||
this.$modal.msgError('深部位移至少配置 1 个监测点')
|
||||
return null
|
||||
}
|
||||
const invalid = monitors.find(r => r.sensorDepth == null || r.holeDepth == null || r.holeElevation == null || r.sensorDepth > r.holeDepth)
|
||||
if (invalid) {
|
||||
this.$modal.msgError(`子设备 ${invalid.subSerialNumber} 的传感器深度、孔深、孔口高程必填,且传感器深度不能大于孔深`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
const refRow = references[0] || null
|
||||
return rows.map(row => ({
|
||||
...row,
|
||||
onboardConfig: this.buildOnboardConfig(row, refRow)
|
||||
}))
|
||||
return rows.map(row => {
|
||||
const onboardConfig = this.safeParseJson(this.buildOnboardConfig(row, refRow), {})
|
||||
if (this.form.sensorType === 'deep_inclino' && row.deviceRole === 'monitor') {
|
||||
onboardConfig.sensorDepth = row.sensorDepth
|
||||
onboardConfig.holeDepth = row.holeDepth
|
||||
onboardConfig.holeElevation = row.holeElevation
|
||||
}
|
||||
return { ...row, onboardConfig: JSON.stringify(onboardConfig) }
|
||||
})
|
||||
},
|
||||
safeParseJson(value, defaultValue) {
|
||||
if (!value) return defaultValue
|
||||
@ -733,7 +889,18 @@ export default {
|
||||
cancel() { this.open = false },
|
||||
submitForm() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
if (!valid) {
|
||||
// 自动定位到第一个校验失败的字段
|
||||
this.$nextTick(() => {
|
||||
const firstError = this.$refs.form.$el.querySelector('.el-form-item.is-error')
|
||||
if (firstError) {
|
||||
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
const input = firstError.querySelector('input, .el-select .el-input__inner, textarea')
|
||||
if (input) input.focus()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
this.form.fieldMapping = this.fieldMappingRows.length > 0
|
||||
? JSON.stringify(this.fieldMappingRows) : null
|
||||
const normalizedSubRows = this.form.deviceType === 1
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="200" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-video-play" @click="handleExecute(scope.row)" v-hasPermi="['platform:flow:edit']">执行</el-button>
|
||||
<!-- <el-button size="mini" type="text" icon="el-icon-video-play" @click="handleExecute(scope.row)" v-hasPermi="['platform:flow:edit']">执行</el-button> -->
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['platform:flow:edit']">编辑</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['platform:flow:remove']">删除</el-button>
|
||||
</template>
|
||||
@ -203,6 +203,7 @@
|
||||
</el-select>
|
||||
<el-input v-else-if="scope.row.source === 'context'" v-model="scope.row.contextKey" size="mini" placeholder="选择上一步结果,如 step1.id" />
|
||||
<el-select v-else-if="scope.row.source === 'device'" v-model="scope.row.field" size="mini" style="width:100%">
|
||||
<el-option label="平台项目ID" value="platform_project_id" />
|
||||
<el-option label="平台设备ID" value="platform_device_id" />
|
||||
<el-option label="平台测点ID" value="platform_point_id" />
|
||||
<el-option label="系统设备编号" value="serial_number" />
|
||||
@ -226,17 +227,26 @@
|
||||
|
||||
<el-dialog :visible.sync="resultSaveDialogVisible" title="结果保存配置" width="760px" append-to-body :close-on-click-modal="false">
|
||||
<div class="schema-tip" style="margin-bottom:8px">
|
||||
`field` 填接口出参字段名;`saveTo` 支持 `extra_config.xxx`、`platform_instance.access_token`、`device_mapping.platform_point_id` 等目标。
|
||||
根据所选接口的出参定义,选择需要持久化的字段和保存目标。未选择保存目标的字段仅作为中间结果传递。
|
||||
</div>
|
||||
<el-table :data="stepSaveRows" size="mini" border>
|
||||
<el-table-column label="出参字段" width="180">
|
||||
<el-table-column label="出参字段" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.field" size="mini" placeholder="如 id" />
|
||||
<el-select v-model="scope.row.field" size="mini" filterable clearable placeholder="选择出参字段" style="width:100%">
|
||||
<el-option v-for="item in responseFieldOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="保存目标" min-width="360">
|
||||
<el-table-column label="保存目标" min-width="340">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-model="scope.row.saveTo" size="mini" placeholder="如 extra_config.projectId" />
|
||||
<el-select v-model="scope.row.saveTo" size="mini" filterable allow-create clearable placeholder="选择或输入保存目标" style="width:100%">
|
||||
<el-option-group label="预设目标">
|
||||
<el-option v-for="item in presetSaveToOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
<div v-if="scope.row.saveTo && scope.row.saveTo.startsWith('extra_config.')" class="schema-hint" style="margin-top:2px">
|
||||
将保存到 extra_config 字段「{{ scope.row.saveTo.replace('extra_config.', '') }}」
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="60" align="center">
|
||||
@ -284,6 +294,7 @@ export default {
|
||||
editingStepIndex: -1,
|
||||
stepParamRows: [],
|
||||
stepSaveRows: [],
|
||||
responseFieldOptions: [],
|
||||
previousResultOptions: [],
|
||||
queryParams: { pageNum: 1, pageSize: 20, platformId: null, flowType: null },
|
||||
form: { enable: '1', flowType: 1 },
|
||||
@ -303,6 +314,17 @@ export default {
|
||||
;(this.list || []).forEach(item => this.appendSensorTypeOption(optionMap, item && item.sensorType))
|
||||
this.appendSensorTypeOption(optionMap, this.form.sensorType)
|
||||
return Array.from(optionMap.values())
|
||||
},
|
||||
presetSaveToOptions() {
|
||||
return [
|
||||
// { label: '额外配置-自定义key', value: 'extra_config.' },
|
||||
{ label: '认证Token', value: 'platform_user_auth.access_token' },
|
||||
{ label: 'Token过期时间', value: 'platform_user_auth.token_expires_at' },
|
||||
{ label: '平台用户ID', value: 'platform_user_auth.platform_user_id' },
|
||||
{ label: '平台用户名', value: 'platform_user_auth.platform_user_name' },
|
||||
{ label: '设备映射-平台设备ID', value: 'device_mapping.platform_device_id' },
|
||||
{ label: '设备映射-平台测点ID', value: 'device_mapping.platform_point_id' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -399,13 +421,6 @@ export default {
|
||||
handleEnableChange(row) {
|
||||
savePlatformFlow({ flow: { id: row.id, enable: row.enable }, steps: null })
|
||||
},
|
||||
handleExecute(row) {
|
||||
this.$confirm(`确认手动执行流程【${row.name}】?`).then(() => {
|
||||
executePlatformFlow(row.id).then(() => {
|
||||
this.$modal.msgSuccess('执行完成')
|
||||
})
|
||||
})
|
||||
},
|
||||
loadInterfaces(platformId) {
|
||||
if (!platformId) {
|
||||
this.interfaceOptions = []
|
||||
@ -487,6 +502,7 @@ export default {
|
||||
if (field.source === 'onboard_config') return `入网参数 / ${this.stringifySchemaValue(field.value)}`
|
||||
if (field.source === 'device') {
|
||||
const deviceLabels = {
|
||||
platform_project_id: '平台项目ID',
|
||||
platform_device_id: '平台设备ID',
|
||||
platform_point_id: '平台测点ID',
|
||||
serial_number: '系统设备编号'
|
||||
@ -583,11 +599,26 @@ export default {
|
||||
this.paramMappingDialogVisible = false
|
||||
},
|
||||
openResultSaveDialog(stepIndex) {
|
||||
const step = this.steps[stepIndex]
|
||||
if (!step || !step.interfaceId) {
|
||||
this.$modal.msgWarning('请先选择接口')
|
||||
return
|
||||
}
|
||||
this.editingStepIndex = stepIndex
|
||||
this.stepSaveRows = this.safeParseJson(this.steps[stepIndex].resultSave, []).map(item => ({
|
||||
// 解析当前接口的出参字段,构建下拉选项
|
||||
const responseFields = this.parseResponseSchema(step.interfaceId)
|
||||
this.responseFieldOptions = (responseFields || [])
|
||||
.filter(f => f && f.name)
|
||||
.map(f => ({ label: `${f.name}${f.desc ? '(' + f.desc + ')' : ''}`, value: f.name }))
|
||||
// 解析已有保存配置
|
||||
this.stepSaveRows = this.safeParseJson(step.resultSave, []).map(item => ({
|
||||
field: item.field || '',
|
||||
saveTo: item.saveTo || ''
|
||||
}))
|
||||
if (this.stepSaveRows.length === 0 && this.responseFieldOptions.length > 0) {
|
||||
// 默认填充一个空行
|
||||
this.stepSaveRows.push({ field: '', saveTo: '' })
|
||||
}
|
||||
this.resultSaveDialogVisible = true
|
||||
},
|
||||
confirmResultSave() {
|
||||
|
||||
@ -42,12 +42,6 @@
|
||||
<el-tag size="small" v-if="scope.row.authType===3">OAuth2</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Token状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="small" type="success" v-if="tokenValid(scope.row)">有效</el-tag>
|
||||
<el-tag size="small" type="danger" v-else-if="scope.row.tokenExpiresAt">已过期</el-tag>
|
||||
<el-tag size="small" type="info" v-else>未认证</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="初始化" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
@ -63,7 +57,7 @@
|
||||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" fixed="right" width="260" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-setting" @click="handleInitialize(scope.row)" v-hasPermi="['platform:instance:edit']">初始化</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-user" @click="handleUserAuth(scope.row)" v-hasPermi="['platform:userAuth:list']">用户</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-s-operation" @click="handleInterface(scope.row)" v-hasPermi="['platform:interface:list']">接口</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['platform:instance:edit']">编辑</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['platform:instance:remove']">删除</el-button>
|
||||
@ -119,14 +113,14 @@
|
||||
</template>
|
||||
<el-divider content-position="left">平台参数</el-divider>
|
||||
<div class="schema-tip" style="margin-bottom:8px">
|
||||
这里维护平台运行所需的业务参数,例如 `projectId`、项目名称、站点编码等;初始化流程写回的值也会保存在这里,可直接前端补录或修改。
|
||||
这里维护平台运行所需的业务参数,例如站点编码等;初始化流程写回的值也会保存在这里,可直接前端补录或修改。
|
||||
</div>
|
||||
<key-value-editor
|
||||
v-model="platformConfigRows"
|
||||
add-text="添加平台参数"
|
||||
:show-enabled="false"
|
||||
:show-description="true"
|
||||
key-placeholder="参数名,如 projectId"
|
||||
key-placeholder="参数名"
|
||||
value-placeholder="参数值"
|
||||
/>
|
||||
<el-form-item label="启用">
|
||||
@ -144,11 +138,97 @@
|
||||
|
||||
<!-- 接口管理抽屉 -->
|
||||
<interface-drawer ref="interfaceDrawer" :platform-id="currentPlatformId" :platform-name="currentPlatformName" />
|
||||
|
||||
<!-- 用户管理弹窗 -->
|
||||
<el-dialog title="用户认证管理" :visible.sync="userAuthOpen" width="1000px" append-to-body :close-on-click-modal="false">
|
||||
<div style="margin-bottom:12px">
|
||||
<el-button type="primary" size="mini" icon="el-icon-plus" @click="handleAddUserAuth" v-hasPermi="['platform:userAuth:add']">新增用户</el-button>
|
||||
</div>
|
||||
<el-table v-loading="userAuthLoading" :data="userAuthList" border>
|
||||
<el-table-column label="用户ID" prop="platformUserId" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.platformUserId">{{ scope.row.platformUserId }}</span>
|
||||
<span v-else style="color:#909399">待初始化</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户名" prop="platformUserName" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.platformUserName">{{ scope.row.platformUserName }}</span>
|
||||
<span v-else style="color:#909399">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Token状态" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="small" type="success" v-if="userAuthTokenValid(scope.row)">有效</el-tag>
|
||||
<el-tag size="small" type="danger" v-else-if="scope.row.tokenExpiresAt">已过期</el-tag>
|
||||
<el-tag size="small" type="info" v-else>未认证</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="初始化" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="small" type="success" v-if="scope.row.initStatus===1">已初始化</el-tag>
|
||||
<el-tag size="small" type="info" v-else>未初始化</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="70" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.enable" active-value="1" inactive-value="0" @change="handleUserAuthEnableChange(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleInitializeUserAuth(scope.row)" v-hasPermi="['platform:userAuth:initialize']"
|
||||
:disabled="scope.row.initStatus===1">初始化</el-button>
|
||||
<el-button size="mini" type="text" @click="handleEditUserAuth(scope.row)" v-hasPermi="['platform:userAuth:edit']">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="handleDeleteUserAuth(scope.row)" v-hasPermi="['platform:userAuth:remove']">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增/编辑用户弹窗 -->
|
||||
<el-dialog :title="userFormTitle" :visible.sync="userFormOpen" width="550px" append-to-body :close-on-click-modal="false">
|
||||
<el-form ref="userForm" :model="userForm" label-width="100px">
|
||||
<!-- 编辑时显示用户ID和用户名(只读,由初始化流程填充) -->
|
||||
<template v-if="userForm.id">
|
||||
<el-form-item label="用户ID">
|
||||
<el-input v-model="userForm.platformUserId" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="userForm.platformUserName" disabled />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-divider content-position="left">认证配置</el-divider>
|
||||
<el-form-item label="用户名(认证)">
|
||||
<el-input v-model="userAuthConfigParsed.userName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码(认证)">
|
||||
<el-input v-model="userAuthConfigParsed.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="clientId">
|
||||
<el-input v-model="userAuthConfigParsed.clientId" placeholder="如:DACP_App" />
|
||||
</el-form-item>
|
||||
<el-form-item label="scope">
|
||||
<el-input v-model="userAuthConfigParsed.scope" placeholder="如:DACP" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="userForm.enable" active-value="1" inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="userForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="userFormOpen = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitUserForm" :loading="userFormLoading">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listPlatformInstance, getPlatformInstance, addPlatformInstance, updatePlatformInstance, delPlatformInstance, initializePlatform } from '@/api/platform/instance'
|
||||
import { listPlatformInstance, getPlatformInstance, addPlatformInstance, updatePlatformInstance, delPlatformInstance } from '@/api/platform/instance'
|
||||
import { listPlatformUserAuth, getPlatformUserAuth, addPlatformUserAuth, updatePlatformUserAuth, delPlatformUserAuth, initializeUserAuth } from '@/api/platform/userAuth'
|
||||
import InterfaceDrawer from '../components/InterfaceDrawer'
|
||||
import KeyValueEditor from '../components/KeyValueEditor'
|
||||
|
||||
@ -176,7 +256,16 @@ export default {
|
||||
code: [{ required: true, message: '请输入平台标识', trigger: 'blur' }],
|
||||
baseUrl: [{ required: true, message: '请输入baseURL', trigger: 'blur' }],
|
||||
authType:[{ required: true, message: '请选择认证类型', trigger: 'change' }]
|
||||
}
|
||||
},
|
||||
// 用户管理
|
||||
userAuthOpen: false,
|
||||
userAuthLoading: false,
|
||||
userAuthList: [],
|
||||
userFormOpen: false,
|
||||
userFormTitle: '',
|
||||
userFormLoading: false,
|
||||
userForm: { platformId: null, enable: '1' },
|
||||
userAuthConfigParsed: { userName: '', password: '', clientId: '', scope: '' }
|
||||
}
|
||||
},
|
||||
created() { this.getList() },
|
||||
@ -189,10 +278,6 @@ export default {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
tokenValid(row) {
|
||||
if (!row.tokenExpiresAt) return false
|
||||
return new Date(row.tokenExpiresAt).getTime() > Date.now() + 5 * 60 * 1000
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
|
||||
handleSelectionChange(sel) { this.multiple = sel.length !== 1 },
|
||||
@ -223,24 +308,82 @@ export default {
|
||||
this.$modal.msgSuccess(row.enable === '1' ? '已启用' : '已禁用')
|
||||
})
|
||||
},
|
||||
handleInitialize(row) {
|
||||
this.$confirm(`确认初始化平台【${row.name}】?将执行初始化流程获取 userId/projectId 等信息。`).then(() => {
|
||||
initializePlatform(row.id).then(() => {
|
||||
this.$modal.msgSuccess('初始化成功')
|
||||
this.getList()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleInterface(row) {
|
||||
this.currentPlatformId = row.id
|
||||
this.currentPlatformName = row.name
|
||||
this.$nextTick(() => { this.$refs.interfaceDrawer.open() })
|
||||
},
|
||||
// 用户管理
|
||||
handleUserAuth(row) {
|
||||
this.currentPlatformId = row.id
|
||||
this.currentPlatformName = row.name
|
||||
this.userAuthOpen = true
|
||||
this.loadUserAuthList()
|
||||
},
|
||||
loadUserAuthList() {
|
||||
this.userAuthLoading = true
|
||||
listPlatformUserAuth({ platformId: this.currentPlatformId }).then(res => {
|
||||
this.userAuthList = res.rows || []
|
||||
this.userAuthLoading = false
|
||||
}).catch(() => { this.userAuthLoading = false })
|
||||
},
|
||||
userAuthTokenValid(row) {
|
||||
if (!row.tokenExpiresAt) return false
|
||||
return new Date(row.tokenExpiresAt).getTime() > Date.now() + 5 * 60 * 1000
|
||||
},
|
||||
handleAddUserAuth() {
|
||||
this.userForm = { platformId: this.currentPlatformId, enable: '1' }
|
||||
this.userAuthConfigParsed = { userName: '', password: '', clientId: '', scope: '' }
|
||||
this.userFormTitle = '新增用户认证'
|
||||
this.userFormOpen = true
|
||||
},
|
||||
handleEditUserAuth(row) {
|
||||
getPlatformUserAuth(row.id).then(res => {
|
||||
this.userForm = res.data
|
||||
this.userAuthConfigParsed = this.userForm.authConfig ? JSON.parse(this.userForm.authConfig) : {}
|
||||
this.userFormTitle = '编辑用户认证'
|
||||
this.userFormOpen = true
|
||||
})
|
||||
},
|
||||
handleDeleteUserAuth(row) {
|
||||
this.$confirm('确认删除该用户认证?').then(() => {
|
||||
delPlatformUserAuth(row.id).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.loadUserAuthList()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleUserAuthEnableChange(row) {
|
||||
updatePlatformUserAuth({ id: row.id, enable: row.enable }).then(() => {
|
||||
this.$modal.msgSuccess(row.enable === '1' ? '已启用' : '已禁用')
|
||||
})
|
||||
},
|
||||
handleInitializeUserAuth(row) {
|
||||
this.$confirm(`确认初始化用户【${row.platformUserName || row.platformUserId}】?将执行初始化流程获取 userId。`).then(() => {
|
||||
initializeUserAuth(row.id).then(() => {
|
||||
this.$modal.msgSuccess('初始化成功')
|
||||
this.loadUserAuthList()
|
||||
})
|
||||
})
|
||||
},
|
||||
submitUserForm() {
|
||||
this.$refs.userForm.validate(valid => {
|
||||
if (!valid) return
|
||||
this.userForm.authConfig = JSON.stringify(this.userAuthConfigParsed)
|
||||
this.userFormLoading = true
|
||||
const api = this.userForm.id ? updatePlatformUserAuth : addPlatformUserAuth
|
||||
api(this.userForm).then(() => {
|
||||
this.$modal.msgSuccess(this.userForm.id ? '修改成功' : '新增成功')
|
||||
this.userFormOpen = false
|
||||
this.loadUserAuthList()
|
||||
}).finally(() => { this.userFormLoading = false })
|
||||
})
|
||||
},
|
||||
parseObjectRows(value) {
|
||||
if (!value) return []
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
return Object.keys(parsed).map(key => ({
|
||||
return Object.keys(parsed).filter(key => !['projectId', 'projectName'].includes(key)).map(key => ({
|
||||
key,
|
||||
value: parsed[key] == null ? '' : String(parsed[key]),
|
||||
description: ''
|
||||
@ -252,7 +395,7 @@ export default {
|
||||
buildObjectJson(rows) {
|
||||
const result = {}
|
||||
;(rows || []).forEach(item => {
|
||||
if (!item || !item.key) return
|
||||
if (!item || !item.key || ['projectId', 'projectName'].includes(item.key)) return
|
||||
result[item.key] = item.value
|
||||
})
|
||||
return Object.keys(result).length > 0 ? JSON.stringify(result) : null
|
||||
@ -261,7 +404,7 @@ export default {
|
||||
submitForm() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
this.form.authConfig = JSON.stringify(this.authConfigParsed)
|
||||
this.form.authTemplate = JSON.stringify(this.authConfigParsed)
|
||||
this.form.extraConfig = this.buildObjectJson(this.platformConfigRows)
|
||||
this.submitLoading = true
|
||||
const api = this.form.id ? updatePlatformInstance : addPlatformInstance
|
||||
|
||||
Loading…
Reference in New Issue
Block a user