Merge branch 'dev/0.8_dataBridge2' into dev/0.9_platformIntegration

# Conflicts:
#	sql/platform-integration.sql
This commit is contained in:
zhugongping 2026-07-31 15:09:36 +08:00
commit e235d0dfaf
41 changed files with 2094 additions and 933 deletions

View File

@ -8,13 +8,11 @@ TARGET_PORT = 8888 # 请根据实际情况修改
# 元数组 modbus 指令 # 元数组 modbus 指令
MODBUS_DATA_HEX = ( MODBUS_DATA_HEX = (
"7e80D9A718F9362A7e", "7e80DFEB502006887e",
"0103204135317841ab8e39bf27bccd3f1fb19ac2b231bbc0b6f82040ae3207c3f9f80efa23", "01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99"
"020320413b9fa441acd34dc0024a003fa2a59ac2af34a0c18e17ea413167dbc3f9c7fe6d40",
"0303204139565641ab21ddbff6af333fe26800c2aec626c18685244176ee82c3f9bd7568c8",
) )
# 重要-四川远德MQTT推送 # 重要-四川远德MQTT推送 (倾角计)
# "7e803042593632517e", # "7e803042593632517e",
# "01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99" # "01033041011b7b0000000041eaafb742d19bd74513d545450dd9a4450ec53d00000000420ccf64420c74a9420c74a9c2d200009d99"
# 深部位移计 # 深部位移计

View File

@ -10,11 +10,11 @@
## 1. 背景与目标 ## 1. 背景与目标
FastBee 需要将设备上报数据推送到第三方监测平台,当前已落地 DCP 结构监测 SaaS。模块目标是把“平台、接口、流程、设备绑定、字段映射、推送日志”配置化,降低后续接入新平台或调整 DCP 接口的代码改动。 FastBee 需要将设备上报数据推送到第三方监测平台,当前已落地 DCP 结构监测 SaaS。模块目标是把"平台、用户认证、接口、流程、设备绑定、字段映射、推送日志"配置化,降低后续接入新平台或调整 DCP 接口的代码改动。
当前实现采用两条主链路: 当前实现采用两条主链路:
1. 流程执行链路:用于平台初始化、Token 刷新、设备入网等多步骤调用。 1. 流程执行链路:用于用户初始化、Token 刷新、设备入网等多步骤调用。
2. 实时推送链路:设备上报后异步命中平台设备绑定,由 DCP 适配器组装请求体并推送。 2. 实时推送链路:设备上报后异步命中平台设备绑定,由 DCP 适配器组装请求体并推送。
设计边界: 设计边界:
@ -23,6 +23,7 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落
- 实时数据推送不走流程引擎,按设备绑定上的 `push_interface_id`、`field_mapping`、`extra_mapping` 直接组装请求。 - 实时数据推送不走流程引擎,按设备绑定上的 `push_interface_id`、`field_mapping`、`extra_mapping` 直接组装请求。
- 当前只有 DCP 适配器实现,平台路由按 `platform_instance.code` 分发,`code=dcp` 时进入 `DcpPlatformAdapter`。 - 当前只有 DCP 适配器实现,平台路由按 `platform_instance.code` 分发,`code=dcp` 时进入 `DcpPlatformAdapter`。
- 网关多子设备场景通过独立子表 `platform_device_sub_mapping` 管理,不再只依赖 `extra_mapping`。 - 网关多子设备场景通过独立子表 `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` | | 用户初始化 | 用户管理页点击初始化 | `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` | `access_token`、`token_expires_at` | | 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` | | 直连设备入网 | 设备绑定页点击入网 | `dcp_create_point` -> 对应传感器绑定接口 | 主表 `platform_point_id`、`platform_device_id`、`net_status` |
| 网关设备入网 | 设备绑定页点击入网 | 对每个 monitor 子设备逐个执行入网流程 | 子表 `platform_point_id`、`platform_device_id`、`net_status`;全部 monitor 成功后主表 `net_status=1` | | 网关设备入网 | 设备绑定页点击入网 | 对每个 monitor 子设备逐个执行入网流程 | 子表 `platform_point_id`、`platform_device_id`、`net_status`;全部 monitor 成功后主表 `net_status=1` |
入口代码: 入口代码:
- `PlatformInstanceServiceImpl.initialize(Long id)` - `PlatformUserAuthServiceImpl.initialize(Long userAuthId)` - 用户初始化
- `PlatformFlowServiceImpl.executeManually(Long id, Long mappingId)` - `PlatformDeviceMappingServiceImpl.onboard(Long mappingId)` - 设备入网
- `PlatformDeviceMappingServiceImpl.onboard(Long mappingId)`
- `PlatformFlowExecutor.execute(PlatformFlow flow, FlowContext context)` - `PlatformFlowExecutor.execute(PlatformFlow flow, FlowContext context)`
> **注意:** 流程手动执行端点(`POST /platform/flow/{id}/execute`)已移除,流程执行只能通过业务入口(初始化/入网/Token 刷新)触发。
### 2.2 直接调用实时推送 ### 2.2 直接调用实时推送
设备上报后由 `DeviceReportTrigger.onDeviceReport(serialNumber, payload)` 异步触发: 设备上报后由 `DeviceReportTrigger.onDeviceReport(serialNumber, payload)` 异步触发:
@ -53,9 +55,10 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落
1. `PlatformDeviceMappingMapper.selectBySerialNumber` 同时匹配直连主设备和网关子设备。 1. `PlatformDeviceMappingMapper.selectBySerialNumber` 同时匹配直连主设备和网关子设备。
2. 过滤未启用的平台和绑定关系。 2. 过滤未启用的平台和绑定关系。
3. 按平台 `code` 分发,目前 `dcp` 进入 `DcpPlatformAdapter`。 3. 按平台 `code` 分发,目前 `dcp` 进入 `DcpPlatformAdapter`。
4. 适配器按推送接口的 `request_schema` 与设备绑定字段映射组装请求体。 4. 加载 `PlatformUserAuth`,传入适配器用于 Token 获取和字段映射。
5. 检查并刷新 Token。 5. 适配器按推送接口的 `request_schema` 与设备绑定字段映射组装请求体。
6. 发送 HTTP POST,写入 `platform_push_log`。 6. 检查并刷新 Token(从 userAuth 获取)。
7. 发送 HTTP POST,写入 `platform_push_log`。
实时推送不经过 `PlatformFlowExecutor`,因此不会使用流程步骤的 `param_mapping`、`result_key`、`result_save`。 实时推送不经过 `PlatformFlowExecutor`,因此不会使用流程步骤的 `param_mapping`、`result_key`、`result_save`。
@ -71,7 +74,7 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落
调度器 `PlatformPushScheduledJob` 使用 Spring `@Scheduled(fixedDelay = 300_000)` 每 5 分钟检查一次: 调度器 `PlatformPushScheduledJob` 使用 Spring `@Scheduled(fixedDelay = 300_000)` 每 5 分钟检查一次:
1. 查询所有 `enable='1'`、`push_mode=2`、`push_interval > 0` 的绑定。 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` 推送。 3. 从 Redis 缓存读取最新数据,按直连/网关分别调用 `DcpPlatformAdapter` 推送。
4. 推送成功后更新 `last_push_time`;失败时保留不变,下次继续尝试。 4. 推送成功后更新 `last_push_time`;失败时保留不变,下次继续尝试。
@ -79,22 +82,25 @@ FastBee 需要将设备上报数据推送到第三方监测平台,当前已落
### 2.4 Token 自动刷新 ### 2.4 Token 自动刷新
流程执行器和 DCP 实时推送适配器各自调用 `ensureToken`: 流程执行器调用 `ensureToken(FlowContext ctx)`:
- 从 `FlowContext.platformUserAuth` 获取用户认证信息。
- `access_token` 为空,或 `token_expires_at` 距当前时间不足 5 分钟时视为过期。 - `access_token` 为空,或 `token_expires_at` 距当前时间不足 5 分钟时视为过期。
- 自动查找同平台 `flow_type=3` 且 `enable='1'` 的 Token 刷新流程。 - 自动查找同平台 `flow_type=3` 且 `enable='1'` 的 Token 刷新流程。
- 刷新流程本身不注入 Authorization,避免递归。 - 刷新流程本身不注入 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` 为准。 DDL 以 `sql/platform-integration.sql` 为准。
### 3.1 `platform_instance` 平台实体 ### 3.1 `platform_instance` 平台实体
保存第三方平台实例、认证配置和初始化结果。 保存第三方平台实例和认证模板。
关键字段: 关键字段:
@ -104,15 +110,47 @@ DDL 以 `sql/platform-integration.sql` 为准。
| `code` | 平台唯一标识,当前 DCP 为 `dcp`,有唯一索引 | | `code` | 平台唯一标识,当前 DCP 为 `dcp`,有唯一索引 |
| `base_url` | 平台基础 URL | | `base_url` | 平台基础 URL |
| `auth_type` | 认证类型:`1=密码Token`、`2=ApiKey`、`3=OAuth2` | | `auth_type` | 认证类型:`1=密码Token`、`2=ApiKey`、`3=OAuth2` |
| `auth_config` | 认证配置 JSON,例如 `userName`、`password`、`clientId`、`scope` | | `auth_config` | 认证配置模板 JSON,新增用户时可复制;例如 `userName`、`password`、`clientId`、`scope` |
| `extra_config` | 初始化后保存的业务上下文,例如 `userId`、`projectId`、`projectName` | | `extra_config` | 平台级业务参数,如站点编码 |
| `access_token` | 当前 Token |
| `token_expires_at` | Token 过期时间 |
| `init_status` | 初始化状态 |
| `enable` | 是否启用 | | `enable` | 是否启用 |
| `tenant_id` | 租户 ID | | `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 接口定义。 保存第三方 HTTP 接口定义。
@ -135,8 +173,8 @@ DDL 以 `sql/platform-integration.sql` 为准。
|---|---| |---|---|
| `fixed` | 固定值,取 `value` | | `fixed` | 固定值,取 `value` |
| `uuid` | 每次请求生成新的 UUID | | `uuid` | 每次请求生成新的 UUID |
| `auth` | 从 `platform_instance.auth_config` 按 `field` 取值 | | `auth` | 优先从 `platform_user_auth.auth_config` 取值,fallback 到 `platform_instance.auth_config` |
| `extra` | 从 `platform_instance.extra_config` 按 `field` 取值 | | `extra` | 优先从 `platform_user_auth.extra_config` 取值,fallback 到 `platform_instance.extra_config` |
| `context` | 从 `FlowContext` 按 `contextKey` 取值 | | `context` | 从 `FlowContext` 按 `contextKey` 取值 |
| `device` | 从 `PlatformDeviceMapping` / 当前网关子设备上下文取 `platform_device_id`、`platform_point_id`、`serial_number`、`device_name`、`gateway_serial_number`、`monitor_serial_number` | | `device` | 从 `PlatformDeviceMapping` / 当前网关子设备上下文取 `platform_device_id`、`platform_point_id`、`serial_number`、`device_name`、`gateway_serial_number`、`monitor_serial_number` |
| `thingsModel` | 从流程上下文 payload 按物模型标识取值,通常用于手动流程或扩展流程 | | `thingsModel` | 从流程上下文 payload 按物模型标识取值,通常用于手动流程或扩展流程 |
@ -167,8 +205,8 @@ DDL 以 `sql/platform-integration.sql` 为准。
|---|---| |---|---|
| `fixed` | 固定值 | | `fixed` | 固定值 |
| `device` | 从绑定关系取平台设备/测点/序列号 | | `device` | 从绑定关系取平台设备/测点/序列号 |
| `auth` | 从认证 JSON 取值 | | `auth` | 优先从 `platform_user_auth` 认证 JSON 取值 |
| `extra` | 从扩展 JSON 取值 | | `extra` | 优先从 `platform_user_auth` 扩展 JSON 取值 |
| `thingsModel` / `identifier` | 按 `field_mapping` 或 `itemFieldMappings` 从上报 payload 取物模型值 | | `thingsModel` / `identifier` | 按 `field_mapping` 或 `itemFieldMappings` 从上报 payload 取物模型值 |
| `ts` | 从物模型值的 `__ts` 派生时间,缺省为当前时间 | | `ts` | 从物模型值的 `__ts` 派生时间,缺省为当前时间 |
| `context` | 优先从 payload 的 `contextKey` 取值;`reportTime`/`epoch` 会按时间字段处理 | | `context` | 优先从 payload 的 `contextKey` 取值;`reportTime`/`epoch` 会按时间字段处理 |
@ -176,7 +214,7 @@ DDL 以 `sql/platform-integration.sql` 为准。
`response_schema.path` 支持对象路径、数组下标路径,例如 `id`、`[0].id`、`$.data.id`。 `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` | | `sensor_type` | 入网流程适用传感器类型,如 `inclino`、`deep_inclino`、`stress`、`hyeto`、`static_level` |
| `enable` | 是否启用 | | `enable` | 是否启用 |
### 3.4 `platform_flow_step` 流程步骤 ### 3.5 `platform_flow_step` 流程步骤
关键字段: 关键字段:
@ -199,17 +237,21 @@ DDL 以 `sql/platform-integration.sql` 为准。
| `interface_id` | 关联接口 | | `interface_id` | 关联接口 |
| `param_mapping` | 覆盖接口默认入参来源 | | `param_mapping` | 覆盖接口默认入参来源 |
| `result_key` | 步骤结果写入上下文的 key | | `result_key` | 步骤结果写入上下文的 key |
| `result_save` | 将响应字段持久化到平台或设备绑定 | | `result_save` | 将响应字段持久化到用户认证或设备绑定 |
`result_save` 支持: `result_save` 支持:
- `extra_config.xxx` - `platform_user_auth.access_token` - 用户级 Token
- `platform_instance.access_token` - `platform_user_auth.token_expires_at` - 值按 `expires_in` 秒数转换为 Date
- `platform_instance.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_device_id`
- `device_mapping.platform_point_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 设备与第三方平台的绑定关系。 保存 FastBee 设备与第三方平台的绑定关系。
@ -218,6 +260,9 @@ DDL 以 `sql/platform-integration.sql` 为准。
| 字段 | 说明 | | 字段 | 说明 |
|---|---| |---|---|
| `platform_id` | 所属平台 | | `platform_id` | 所属平台 |
| `platform_user_auth_id` | 关联用户认证 ID,选择已初始化的用户 |
| `platform_project_id` | 平台侧项目 ID,用户选择后存储 |
| `platform_project_name` | 平台侧项目名称 |
| `device_id` / `serial_number` | FastBee 设备 ID 与序列号 | | `device_id` / `serial_number` | FastBee 设备 ID 与序列号 |
| `device_name` | 推送给平台的设备名称,默认取平台设备名称,可手动修改 | | `device_name` | 推送给平台的设备名称,默认取平台设备名称,可手动修改 |
| `platform_device_id` | 平台侧 deviceId,直连设备入网后回填 | | `platform_device_id` | 平台侧 deviceId,直连设备入网后回填 |
@ -233,9 +278,11 @@ DDL 以 `sql/platform-integration.sql` 为准。
| `push_interval` | 定时推送间隔(分钟),`0` 表示不自动推送 | | `push_interval` | 定时推送间隔(分钟),`0` 表示不自动推送 |
| `last_push_time` | 最近推送时间,由定时调度器更新 | | `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`。 唯一约束:`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/ fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/
├── domain/ ├── domain/
│ ├── PlatformInstance │ ├── PlatformInstance
│ ├── PlatformUserAuth # 平台用户认证,Token/认证从这里获取
│ ├── PlatformInterface │ ├── PlatformInterface
│ ├── PlatformFlow │ ├── PlatformFlow
│ ├── PlatformFlowStep │ ├── PlatformFlowStep
@ -289,6 +337,7 @@ fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/
│ └── PlatformPushLog │ └── PlatformPushLog
├── mapper/ ├── mapper/
│ ├── PlatformInstanceMapper │ ├── PlatformInstanceMapper
│ ├── PlatformUserAuthMapper # 用户认证 Mapper
│ ├── PlatformInterfaceMapper │ ├── PlatformInterfaceMapper
│ ├── PlatformFlowMapper │ ├── PlatformFlowMapper
│ ├── PlatformFlowStepMapper │ ├── PlatformFlowStepMapper
@ -296,30 +345,32 @@ fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/platform/
│ ├── PlatformDeviceSubMappingMapper │ ├── PlatformDeviceSubMappingMapper
│ └── PlatformPushLogMapper │ └── PlatformPushLogMapper
├── service/ ├── service/
│ ├── IPlatformInstanceService │ ├── IPlatformInstanceService # 仅保留 selectList
│ ├── IPlatformFlowService │ ├── IPlatformUserAuthService # 用户认证(新增,接管初始化/项目/Token)
│ ├── IPlatformFlowService # 移除 executeManually
│ ├── IPlatformDeviceMappingService │ ├── IPlatformDeviceMappingService
│ ├── IPlatformDeviceSubMappingService │ ├── IPlatformDeviceSubMappingService
│ ├── IPlatformPushLogService │ ├── IPlatformPushLogService
│ └── impl/ │ └── impl/
├── flow/ ├── flow/
│ ├── PlatformFlowExecutor │ ├── PlatformFlowExecutor # Token 从 userAuth 获取
│ └── FlowContext │ └── FlowContext # platformUserAuth 为必填字段
├── adapter/ ├── adapter/
│ └── dcp/ │ └── dcp/
│ ├── DcpPlatformAdapter │ ├── DcpPlatformAdapter # 所有推送方法增加 userAuth 参数
│ └── StaticLevelBufferService │ └── StaticLevelBufferService
├── trigger/ ├── trigger/
│ └── DeviceReportTrigger │ └── DeviceReportTrigger # 加载 userAuth 传入适配器
└── job/ └── job/
└── PlatformPushScheduledJob └── PlatformPushScheduledJob # 加载 userAuth 传入适配器
``` ```
```text ```text
fastbee-open-api/src/main/java/com/fastbee/controller/platform/ 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 ├── PlatformInterfaceController # /platform/interface
├── PlatformFlowController # /platform/flow ├── PlatformFlowController # /platform/flow(移除 execute 端点)
├── PlatformDeviceMappingController # /platform/device-mapping ├── PlatformDeviceMappingController # /platform/device-mapping
└── PlatformPushLogController # /platform/push-log └── PlatformPushLogController # /platform/push-log
``` ```
@ -338,18 +389,26 @@ execute(flow, context)
对每个 step: 对每个 step:
1. 读取关联 platform_interface 1. 读取关联 platform_interface
2. 合并 request_schema 与 step.param_mapping 2. 合并 request_schema 与 step.param_mapping
3. 按 source 解析参数 3. 按 source 解析参数(优先从 userAuth 获取,fallback 到 platform.authConfig)
4. 若参数中存在 list-of-lists,展开为多批请求 4. 若参数中存在 list-of-lists,展开为多批请求
5. 每批请求刷新 uuid 参数 5. 每批请求刷新 uuid 参数
6. 替换 path 中的 {变量} 6. 替换 path 中的 {变量}
7. GET 参数放 query;form 接口编码为 x-www-form-urlencoded;其他接口发 JSON body 7. GET 参数放 query;form 接口编码为 x-www-form-urlencoded;其他接口发 JSON body
8. 非认证步骤自动注入 Authorization: Bearer {token} 8. 非认证步骤自动注入 Authorization: Bearer {token}(从 userAuth 获取)
9. 执行 HTTP 9. 执行 HTTP
10. 按 response_schema 抽取响应,写入 FlowContext 10. 按 response_schema 抽取响应,写入 FlowContext
11. 按 result_save 持久化 11. 按 result_save 持久化到 platform_user_auth 或 device_mapping
12. 异步写 platform_push_log,log_type=2 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 子设备会拆成多次绑定请求。 批量展开用于 `referenceSubDeviceSensorIdPairs` 这类参数。例如静力水准入网时,一个网关下多个 monitor 子设备会拆成多次绑定请求。
### 5.2 设备入网 ### 5.2 设备入网
@ -358,25 +417,28 @@ execute(flow, context)
直连设备: 直连设备:
1. 查找同平台、`flow_type=2`、`sensor_type` 匹配且启用的流程。 1. 根据设备绑定的 `platformUserAuthId` 查询用户认证信息。
2. 创建 `FlowContext(platform, mapping, null)`。 2. 查找同平台、`flow_type=2`、`sensor_type` 匹配且启用的流程。
3. 执行流程。 3. 创建 `FlowContext(platform, mapping, userAuth)`。
4. 若主表已有 `platform_device_id`,将主绑定 `net_status` 改为 `1`。 4. 执行流程。
5. 若主表已有 `platform_device_id`,将主绑定 `net_status` 改为 `1`。
网关设备: 网关设备:
1. 读取 `platform_device_sub_mapping`。 1. 读取 `platform_device_sub_mapping`。
2. 筛选 `device_role=monitor` 且未入网的子设备。 2. 筛选 `device_role=monitor` 且未入网的子设备。
3. 对每个 monitor 创建独立 `FlowContext`: 3. 对每个 monitor 创建独立 `FlowContext`,通过构造函数一次性传入所有上下文字段:
- `currentMonitorSerialNumber = monitor.sub_serial_number` - `platformUserAuth` = 设备绑定关联的用户认证
- `currentMonitorDeviceName = monitor.sub_device_name` - `currentMonitorSerialNumber`、`currentMonitorDeviceName`
- `currentMonitorIndex = monitor 在待入网 monitor 列表中的下标` - `currentMonitorIndex` = monitor 在待入网 monitor 列表中的下标
- `subMappingOnboardConfig = monitor.onboard_config` - `subMappingOnboardConfig` = monitor.onboard_config
4. 执行同一个入网流程。若绑定接口 schema 使用 `source=onboard_config` 且 `value` 为数组,会按 `currentMonitorIndex` 拆出当前子设备的值。 4. 执行同一个入网流程。若绑定接口 schema 使用 `source=onboard_config` 且 `value` 为数组,会按 `currentMonitorIndex` 拆出当前子设备的值。
5. 从上下文 `step1.id`、`step2.id` 取 pointId/deviceId,写回当前 monitor 子表。 5. 从上下文 `step1.id`、`step2.id` 取 pointId/deviceId,写回当前 monitor 子表。
6. 所有 monitor 都入网后,主绑定 `net_status=1`。 6. 所有 monitor 都入网后,主绑定 `net_status=1`。
7. 若部分成功部分失败,已成功子设备会保留结果,接口返回明确失败信息。 7. 若部分成功部分失败,已成功子设备会保留结果,接口返回明确失败信息。
> **注意:** 入网不再校验 `platformProjectId` 是否为空。网关子设备 `FlowContext` 现在通过构造函数一次性设置所有字段,不再使用 `setXxx` 逐个设置。
### 5.3 设备上报触发 ### 5.3 设备上报触发
`DeviceReportTrigger.onDeviceReport` 使用异步线程池 `FastBeeConstant.TASK.PUSH_TO_THIRD_PLATFORM_ASYNC`。 `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`。 - 加载 `PlatformUserAuth`,未启用或无用户认证时跳过。
- `platform.code=dcp` 且 `device_type=1`:先在子表中按上报序列号找 `PlatformDeviceSubMapping`,找到后调用 `pushGatewayData`。 - `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,不推送。 - 未知平台 code 只记录 warning,不推送。
### 5.4 定时推送调度器 ### 5.4 定时推送调度器
@ -428,10 +491,10 @@ public void execute() {
`isDue` 判断:`push_interval` 转换为毫秒后,若 `nowMs - lastPushTime.getTime() >= intervalMs` 则触发。`lastPushTime` 为空时立即触发。 `isDue` 判断:`push_interval` 转换为毫秒后,若 `nowMs - lastPushTime.getTime() >= intervalMs` 则触发。`lastPushTime` 为空时立即触发。
`pushLatestData` 从 Redis 缓存读取设备最新数据,按 `device_type` 分发: `pushLatestData` 加载 `PlatformUserAuth`,从 Redis 缓存读取设备最新数据,按 `device_type` 分发:
- 直连设备(`device_type=0`):调用 `DcpPlatformAdapter.pushData`。 - 直连设备(`device_type=0`):调用 `DcpPlatformAdapter.pushData(platform, userAuth, mapping, ...)`。
- 网关设备(`device_type=1`):遍历所有子设备,逐个从 Redis 读取并调用 `DcpPlatformAdapter.pushGatewayData`。 - 网关设备(`device_type=1`):遍历所有子设备,逐个从 Redis 读取并调用 `DcpPlatformAdapter.pushGatewayData(platform, userAuth, mapping, subMapping, ...)`。
推送完成后调用 `IPlatformDeviceMappingService.updateLastPushTime` 更新 `last_push_time`。 推送完成后调用 `IPlatformDeviceMappingService.updateLastPushTime` 更新 `last_push_time`。
@ -443,8 +506,9 @@ public void execute() {
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ PlatformPushScheduledJob(每5分钟) │ │ PlatformPushScheduledJob(每5分钟) │
│ push_mode=2 且达到时间间隔 │ │ push_mode=2 且达到时间间隔 │
│ → 加载 PlatformUserAuth │
│ → 从 Redis 读最新数据 │ │ → 从 Redis 读最新数据 │
│ → 推送到平台 │ │ → 推送到平台(传入 userAuth) │
│ → 更新 last_push_time │ │ → 更新 last_push_time │
└──────────────────────────────────────┘ └──────────────────────────────────────┘
``` ```
@ -462,12 +526,17 @@ DCP 实时推送必须在设备绑定中配置 `push_interface_id`:
- payload 支持 JSON 对象和数组两种格式。 - payload 支持 JSON 对象和数组两种格式。
- 数组格式按 `id` 或 `identifier` 转成 `{identifier: value}`,并把 `ts` 保存为 `{identifier}__ts`。 - 数组格式按 `id` 或 `identifier` 转成 `{identifier: value}`,并把 `ts` 保存为 `{identifier}__ts`。
- 有 `request_schema` 时按 schema 组装。 - 有 `request_schema` 时按 schema 组装。
- 若 schema 组装结果为空,或没有任何 payload 驱动字段,则回退到 legacy 组装: - 若 schema 组装结果为空,或没有任何 payload 驱动字段,则回退到 legacy 组装。
- `deviceId = platform_device_id`
- `epoch = 当前时间`
- 其他字段来自 `field_mapping`
- 普通推送请求体最终是数组:`[ {...} ]`。 - 普通推送请求体最终是数组:`[ {...} ]`。
所有推送方法签名增加了 `PlatformUserAuth userAuth` 参数,用于 Token 获取:
```java
// 变更后
dcpAdapter.pushData(platform, userAuth, mapping, reportSerialNumber, payload);
dcpAdapter.pushGatewayData(platform, userAuth, mapping, subMapping, reportSerialNumber, payload);
```
时间字段会统一转为带时区的 ISO-8601 毫秒格式,支持输入毫秒时间戳、秒时间戳、ISO 字符串、本地时间字符串。 时间字段会统一转为带时区的 ISO-8601 毫秒格式,支持输入毫秒时间戳、秒时间戳、ISO 字符串、本地时间字符串。
### 5.6 网关与数组字段推送 ### 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` 返回顺序为准(深部位移场景为浅到深)。 1. 查询当前绑定下所有 `device_role=monitor` 的子设备,顺序以 `platform_device_sub_mapping` 返回顺序为准(深部位移场景为浅到深)。
2. 当前上报子设备使用本次 payload,其余子设备从 Redis 读取缓存;读取 Redis 时使用子设备真实 `product_id + sub_serial_number`,避免误用网关主设备产品 ID。 2. 当前上报子设备使用本次 payload,其余子设备从 Redis 读取缓存;读取 Redis 时使用子设备真实 `product_id + sub_serial_number`,避免误用网关主设备产品 ID。
3. 每个 monitor 组装为一个请求体对象,`deviceId` 使用该 monitor 入网后回填的 `platform_device_id`,`xTotalBias`、`yTotalBias` 等主体字段使用该 monitor 自身数据。 3. 每个 monitor 组装为一个请求体对象,`deviceId` 使用该 monitor 入网后回填的 `platform_device_id`,`xTotalBias`、`yTotalBias` 等主体字段使用该 monitor 自身数据。
4. 每个对象中的 `sensorData` 不是单点,而是从该 monitor 到末端 monitor 的后缀数组;深部位移中表现为“当前深度 + 更深点位”,顺序保持从浅到深。 4. 每个对象中的 `sensorData` 不是单点,而是从该 monitor 到末端 monitor 的后缀数组;深部位移中表现为"当前深度 + 更深点位",顺序保持从浅到深。
5. 最浅节点对象包含全孔从浅到深的点位数据,最深节点对象只包含自己。 5. 最浅节点对象包含全孔从浅到深的点位数据,最深节点对象只包含自己。
6. 最终一次 HTTP 请求推送整个数组。 6. 最终一次 HTTP 请求推送整个数组。
批量推送不再从“数据推送接口”的 `request_schema` 读取入网参数 `sensorDepths`。`sensorDepths`、`holeDepth` 等仍属于深部位移入网/绑定接口配置;数据推送接口只需配置实际推送字段,例如 `deviceId`、`sensorData`、累计偏移量等。 批量推送不再从"数据推送接口"的 `request_schema` 读取入网参数 `sensorDepths`。`sensorDepths`、`holeDepth` 等仍属于深部位移入网/绑定接口配置;数据推送接口只需配置实际推送字段,例如 `deviceId`、`sensorData`、累计偏移量等。
示例请求体结构(4 个串联点,从浅到深为 A/B/C/D): 示例请求体结构(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` 当作批量推送前置条件。 - 推送接口 `request_schema` 必须包含实际数据推送字段;不要把入网接口的 `sensorDepths` 当作批量推送前置条件。
- 子设备必须已入网,monitor 子表行必须有 `platform_device_id`。 - 子设备必须已入网,monitor 子表行必须有 `platform_device_id`。
- Redis 中需要有各 monitor 子设备的最新物模型缓存;缺失数据的点位不会进入对应后缀 `sensorData`,若某 monitor 自身无数据则该 monitor 对象会被跳过。 - Redis 中需要有各 monitor 子设备的最新物模型缓存;缺失数据的点位不会进入对应后缀 `sensorData`,若某 monitor 自身无数据则该 monitor 对象会被跳过。
- 推送日志可在 `platform_push_log` 查看;成功日志会出现 `[DCP] 批量推送完成`。 - 推送日志可在 `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=2`。
- 实时数据推送日志:`log_type=1`。 - 实时数据推送日志:`log_type=1`。
@ -601,7 +691,34 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5
- 查询平台列表。 - 查询平台列表。
- 新增、编辑、删除平台。 - 新增、编辑、删除平台。
- 平台详情接口会清空 `access_token`,避免前端拿到 Token。 - 平台详情接口会清空 `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 接口定义 ### 6.2 接口定义
@ -627,9 +744,10 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5
- 按平台查询流程。 - 按平台查询流程。
- 新增、编辑流程及步骤。 - 新增、编辑流程及步骤。
- 查看流程详情时返回 `{flow, steps}`。 - 查看流程详情时返回 `{flow, steps}`。
- 手动执行:`POST /platform/flow/{id}/execute`,可选 `mappingId`。
- 步骤入参覆盖同样支持 `source=onboard_config`,语义与接口管理一致:配置 `value`,不配置子映射字段名。 - 步骤入参覆盖同样支持 `source=onboard_config`,语义与接口管理一致:配置 `value`,不配置子映射字段名。
> **注意:** 流程手动执行按钮已移除,流程只能通过业务入口触发。
### 6.4 设备绑定 ### 6.4 设备绑定
路径:`yack-web/src/views/platform/device-mapping/index.vue` 路径:`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`。 - 入网:`POST /platform/device-mapping/{id}/onboard`。
- 子设备映射: - 子设备映射:
- 查询:`GET /platform/device-mapping/{id}/sub-mappings` - 查询:`GET /platform/device-mapping/{id}/sub-mappings`
@ -646,6 +768,10 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5
网关设备必须配置子映射,monitor 子设备才会执行入网并获得平台 deviceId。 网关设备必须配置子映射,monitor 子设备才会执行入网并获得平台 deviceId。
**级联选择接口权限:** 使用 `hasAnyPermi` 支持跨页面调用:
- `/platform/user-auth/initialized/{platformId}` - 允许 `platform:userAuth:list` 或 `platform:deviceMapping:*`
- `/platform/user-auth/{id}/projects` - 同上
### 6.5 推送日志 ### 6.5 推送日志
路径:`yack-web/src/views/platform/push-log/index.vue` 路径:`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 个) ### 7.2 内置流程(6 个)
| code | flow_type | sensor_type | 步骤 | | code | flow_type | sensor_type | 步骤 |
|---|---:|---|---| |---|---|---|---:|---|
| `dcp_init` | 1 | - | `dcp_auth` -> `dcp_get_user` -> `dcp_get_projects` | | `dcp_init` | 1 | - | `dcp_auth` -> `dcp_get_user` -> `dcp_get_projects` |
| `dcp_token_refresh` | 3 | - | `dcp_auth` | | `dcp_token_refresh` | 3 | - | `dcp_auth` |
| `dcp_onboard_inclino` | 2 | `inclino` | `dcp_create_point` -> `dcp_bind_inclino` | | `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=cascadedSubDeviceSensorIds`,`sensorDepths` 使用 `source=cascadedSubDeviceDepths`。
- 静力水准绑定接口的 `sensorIds` 使用 `source=referenceSubDeviceSensorIdPairs`。 - 静力水准绑定接口的 `sensorIds` 使用 `source=referenceSubDeviceSensorIdPairs`。
- 静力水准推送接口定义了数组字段 `sensorData`,其 `itemSchema` 包含 `epoch`、`height`、`temperature`、`sensorId`。 - 静力水准推送接口定义了数组字段 `sensorData`,其 `itemSchema` 包含 `epoch`、`height`、`temperature`、`sensorId`。
- 轴力计和雨量计 DCP 文档要求累计值递增;当前代码未在适配器中实现“查询上次成功值并跳过非递增值”的通用校验,若目标平台严格校验,需要在推送前补充该逻辑或依赖平台返回失败日志。 - 轴力计和雨量计 DCP 文档要求累计值递增;当前代码未在适配器中实现"查询上次成功值并跳过非递增值"的通用校验,若目标平台严格校验,需要在推送前补充该逻辑或依赖平台返回失败日志。
### 7.4 深部位移计扩展配置 ### 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]`。 - 第 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]`。 - 第 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. 当前实现限制与注意事项 ## 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。新增平台需要新增 adapter,并在 `DeviceReportTrigger.dispatchPush` 中按平台 `code` 路由。
- 文档中的“可视化配置”已覆盖平台、接口、流程、设备绑定、子映射和日志,但平台适配器路由仍需要代码扩展。 - 文档中的"可视化配置"已覆盖平台、接口、流程、设备绑定、子映射和日志,但平台适配器路由仍需要代码扩展。
- DCP 实时推送必须配置 `push_interface_id`,未配置时不会按传感器类型自动回退路径。 - DCP 实时推送必须配置 `push_interface_id`,未配置时不会按传感器类型自动回退路径。
- 实时推送只支持 POST JSON,流程执行支持 GET、form、JSON。 - 实时推送只支持 POST JSON,流程执行支持 GET、form、JSON。
- `idempotency_key` 字段已建表,但实时推送未生成幂等键。 - `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`。 - `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,定时推送可能推送过期数据或无数据可推。 - 定时推送依赖 Redis 缓存数据,若设备数据未及时写入 Redis,定时推送可能推送过期数据或无数据可推。
- 定时推送网关设备时会遍历所有子设备,大量子设备场景需关注性能和外部接口调用频率。 - 定时推送网关设备时会遍历所有子设备,大量子设备场景需关注性能和外部接口调用频率。
- 定时推送当前使用 `@Scheduled(fixedDelay)`,不支持 Cron 表达式;如需灵活调度需引入 Quartz。 - 定时推送当前使用 `@Scheduled(fixedDelay)`,不支持 Cron 表达式;如需灵活调度需引入 Quartz。

View File

@ -71,14 +71,6 @@ public class PlatformFlowController extends BaseController {
return success(); 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 @Data
public static class FlowWithSteps { public static class FlowWithSteps {
private PlatformFlow flow; private PlatformFlow flow;

View File

@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.websocket.server.PathParam;
import java.util.List; import java.util.List;
@RestController @RestController
@ -65,19 +66,4 @@ public class PlatformInstanceController extends BaseController {
} }
return success(); return success();
} }
@PreAuthorize("@ss.hasPermi('platform:instance:edit')")
@PostMapping("/{id}/initialize")
@ApiOperation("初始化平台(执行初始化流程,获取userId)")
public AjaxResult initialize(@PathVariable Long id) {
instanceService.initialize(id);
return success("初始化成功");
}
@PreAuthorize("@ss.hasAnyPermi('platform:instance:list,platform:deviceMapping:list,platform:deviceMapping:add,platform:deviceMapping:edit')")
@GetMapping("/{id}/projects")
@ApiOperation("查询平台项目列表")
public AjaxResult getProjects(@PathVariable Long id) {
return success(instanceService.getProjectsByPlatformId(id));
}
} }

View File

@ -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));
}
}

View File

@ -96,7 +96,7 @@ public class PubMqttCallBack implements MqttCallbackExtended {
@Override @Override
public void connectComplete(boolean reconnect, String serverURI) { public void connectComplete(boolean reconnect, String serverURI) {
log.info("MQTT内部客户端已经连接!"); log.info("MQTT内部客户端已经连接!");
System.out.print( System.out.print("" +
"▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌ ▍▌▌▌▌▍ ▌▌▌ ▌▌▌▌ ▌▌▌▌▌▍ ▌▌▌▌\n" + "▌▌▌▌ ▌▌▌▌ ▌▌▌▌▌▌ ▍▌▌▌▌▍ ▌▌▌ ▌▌▌▌ ▌▌▌▌▌▍ ▌▌▌▌\n" +
" ▌▌▌▌▎▌▌▌▌ ▌▌▌ ▌▌▌ ▍▌▌▌▌▌▌▎ ▌▌▌ ▌▌▌▎▌▌ ▌▌▌▌▌▌▌ ▌▌▌▌\n" + " ▌▌▌▌▎▌▌▌▌ ▌▌▌ ▌▌▌ ▍▌▌▌▌▌▌▎ ▌▌▌ ▌▌▌▎▌▌ ▌▌▌▌▌▌▌ ▌▌▌▌\n" +
" ▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌ ▍▌▌▌▏▌▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌▌▌▌▌▌ ▌▌▌▌\n" + " ▌▌▌▌▌▌ ▌▌▌▌ ▌▌▌ ▍▌▌▌▏▌▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌ ▌▌▌▌▌▌▌▌ ▌▌▌▌\n" +

View File

@ -13,16 +13,13 @@ import com.fastbee.http.service.HttpClientFactory;
import com.fastbee.iot.cache.ITSLCache; import com.fastbee.iot.cache.ITSLCache;
import com.fastbee.iot.domain.Device; import com.fastbee.iot.domain.Device;
import com.fastbee.iot.model.ThingsModels.ValueItem; import com.fastbee.iot.model.ThingsModels.ValueItem;
import com.fastbee.iot.platform.domain.PlatformDeviceMapping; import com.fastbee.iot.platform.domain.*;
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.flow.FlowContext; 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.flow.PlatformFlowExecutor;
import com.fastbee.iot.platform.mapper.PlatformFlowMapper; import com.fastbee.iot.platform.mapper.PlatformFlowMapper;
import com.fastbee.iot.platform.mapper.PlatformInterfaceMapper; 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.IPlatformDeviceSubMappingService;
import com.fastbee.iot.platform.service.IPlatformInstanceService; import com.fastbee.iot.platform.service.IPlatformInstanceService;
import com.fastbee.iot.platform.service.IPlatformPushLogService; import com.fastbee.iot.platform.service.IPlatformPushLogService;
@ -38,7 +35,6 @@ import java.time.OffsetDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; 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_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"); private static final DateTimeFormatter DATE_TIME_SECONDS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private PlatformInterfaceMapper interfaceMapper;
@Autowired
private PlatformUserAuthMapper platformUserAuthMapper;
@Autowired @Autowired
private IPlatformInstanceService instanceService; private IPlatformInstanceService instanceService;
@Autowired @Autowired
@ -78,8 +79,6 @@ public class DcpPlatformAdapter {
@Autowired @Autowired
private StaticLevelBufferService bufferService; private StaticLevelBufferService bufferService;
@Autowired @Autowired
private PlatformInterfaceMapper interfaceMapper;
@Autowired
private IPlatformDeviceSubMappingService subMappingService; private IPlatformDeviceSubMappingService subMappingService;
@Autowired @Autowired
private ITSLCache itslCache; private ITSLCache itslCache;
@ -89,20 +88,21 @@ public class DcpPlatformAdapter {
/** /**
* 推送设备上报数据到 DCP(直连设备) * 推送设备上报数据到 DCP(直连设备)
*/ */
public void pushData(PlatformInstance platform, PlatformDeviceMapping mapping, public void pushData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
String reportSerialNumber, String payload) { String reportSerialNumber, String payload) {
if (mapping.getNetStatus() == null || mapping.getNetStatus() != 1) { if (mapping.getNetStatus() == null || mapping.getNetStatus() != 1) {
log.warn("[DCP] 设备 {} 未入网,跳过推送", mapping.getSerialNumber()); log.warn("[DCP] 设备 {} 未入网,跳过推送", mapping.getSerialNumber());
return; return;
} }
String sensorType = mapping.getSensorType(); String sensorType = mapping.getSensorType();
PlatformInterface pushInterface = loadPushInterface(mapping); PlatformInterface pushInterface = interfaceMapper.selectById(mapping.getPushInterfaceId());
JSONArray requestSchema = loadRequestSchema(pushInterface);
List<RequestSchemaField> requestSchema = pushInterface.getRequestSchemaParsed();
String interfaceCode = resolveInterfaceCode(sensorType, pushInterface); String interfaceCode = resolveInterfaceCode(sensorType, pushInterface);
try { try {
String pushPath = resolvePushPath(mapping, pushInterface); 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) { } catch (Exception e) {
String serialNumber = StringUtils.hasText(reportSerialNumber) String serialNumber = StringUtils.hasText(reportSerialNumber)
? reportSerialNumber : mapping.getSerialNumber(); ? reportSerialNumber : mapping.getSerialNumber();
@ -121,7 +121,7 @@ public class DcpPlatformAdapter {
* 数组字段由 requestSchema.arrayMode 控制组包方式: * 数组字段由 requestSchema.arrayMode 控制组包方式:
* single 只推当前子设备,reference_pair 组装 [refItem, monitorItem] 推送。 * single 只推当前子设备,reference_pair 组装 [refItem, monitorItem] 推送。
*/ */
public void pushGatewayData(PlatformInstance platform, PlatformDeviceMapping mapping, public void pushGatewayData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
PlatformDeviceSubMapping subMapping, PlatformDeviceSubMapping subMapping,
String reportSerialNumber, String payload) { String reportSerialNumber, String payload) {
if (mapping.getNetStatus() == null || mapping.getNetStatus() != 1) { if (mapping.getNetStatus() == null || mapping.getNetStatus() != 1) {
@ -129,8 +129,8 @@ public class DcpPlatformAdapter {
return; return;
} }
String sensorType = mapping.getSensorType(); String sensorType = mapping.getSensorType();
PlatformInterface pushInterface = loadPushInterface(mapping); PlatformInterface pushInterface = interfaceMapper.selectById(mapping.getPushInterfaceId());
JSONArray requestSchema = loadRequestSchema(pushInterface); List<RequestSchemaField> requestSchema = pushInterface.getRequestSchemaParsed();
String interfaceCode = resolveInterfaceCode(sensorType, pushInterface); String interfaceCode = resolveInterfaceCode(sensorType, pushInterface);
try { try {
@ -142,68 +142,61 @@ public class DcpPlatformAdapter {
// 判断是否批量推送 // 判断是否批量推送
if (pushInterface.getIsBatchPush() != null && pushInterface.getIsBatchPush() == 1) { 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; return;
} }
// 无数组字段 → 简单推送,使用子设备 platformDeviceId 和主表 fieldMapping // 无数组字段 → 简单推送,使用子设备 platformDeviceId 和主表 fieldMapping
if (!hasArrayField(requestSchema)) { if (!hasArrayField(requestSchema)) {
// 如果只有一个设备完全可以走 // 如果只有一个设备完全可以走
handleSubDeviceSimplePush(platform, mapping, subMapping, reportSerialNumber, payload, pushPath, interfaceCode, requestSchema); handleSubDeviceSimplePush(platform, userAuth, mapping, subMapping, reportSerialNumber, payload, pushPath, interfaceCode, requestSchema);
return; return;
} }
JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping()); List<FieldMappingItem> fieldMappings = mapping.getFieldMappingParsed();
String baseUrl = platform.getBaseUrl() + pushPath; String baseUrl = platform.getBaseUrl() + pushPath;
// 解析 extra_mapping 中的 itemFieldMappings 和 reportIntervalSec // 解析 extra_mapping 中的 itemFieldMappings 和 reportIntervalSec
int reportIntervalSec = 300; int reportIntervalSec = 300;
JSONArray itemFieldMappings = null; List<ExtraMappingConfig.ItemFieldMapping> itemFieldMappings = null;
String fieldName = resolveArrayFieldName(requestSchema, null); String fieldName = resolveArrayFieldName(requestSchema);
JSONObject extra = null; ExtraMappingConfig extra = mapping.getExtraMappingParsed();
if (StringUtils.hasText(mapping.getExtraMapping())) { if (extra != null) {
extra = JSON.parseObject(mapping.getExtraMapping()); reportIntervalSec = extra.getReportIntervalSec() != null ? extra.getReportIntervalSec() : 300;
reportIntervalSec = extra.getIntValue("reportIntervalSec", 300); itemFieldMappings = extra.getItemFieldMappings();
itemFieldMappings = extra.getJSONArray("itemFieldMappings"); if (StringUtils.hasText(extra.getFieldName())) {
if (StringUtils.hasText(extra.getString("fieldName"))) { fieldName = extra.getFieldName();
fieldName = extra.getString("fieldName");
} }
} }
JSONObject arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName); RequestSchemaField arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName);
String arrayMode = resolveArrayMode(arrayFieldSchema, extra); String arrayMode = resolveArrayMode(arrayFieldSchema, extra);
// 从 onboard_config 取 sensorId // 从 onboard_config 取 sensorId
String sensorId = reportSerialNumber; String sensorId = reportSerialNumber;
if (StringUtils.hasText(subMapping.getOnboardConfig())) { OnboardConfig onboardCfg = subMapping.getOnboardConfigParsed();
JSONObject config = JSON.parseObject(subMapping.getOnboardConfig()); if (onboardCfg != null && StringUtils.hasText(onboardCfg.getSensorId())) {
String cfgSensorId = config.getString("sensorId"); sensorId = onboardCfg.getSensorId();
if (StringUtils.hasText(cfgSensorId)) {
sensorId = cfgSensorId;
}
} }
// 构建当前设备的数据项 // 构建当前设备的数据项
JSONObject currentItem = buildArrayItem( JSONObject currentItem = buildArrayItem(userAuth, mapping, requestSchema, reportData, fieldName, itemFieldMappings, sensorId);
platform, mapping, requestSchema, reportData, fieldName, itemFieldMappings, sensorId);
if (currentItem == null || currentItem.isEmpty()) { if (currentItem == null || currentItem.isEmpty()) {
log.warn("[DCP] 网关子设备 {} 数据映射为空,跳过推送", reportSerialNumber); log.warn("[DCP] 网关子设备 {} 数据映射为空,跳过推送", reportSerialNumber);
return; return;
} }
JSONObject currentBodyFields = buildObjectBySchema(platform, mapping, requestSchema, reportData, fieldMappings); JSONObject currentBodyFields = buildObjectBySchema(platform, userAuth, mapping, requestSchema, reportData, fieldMappings);
// 单点数组推送:sensorData 中只包含当前上报子设备这一条数据。 // 单点数组推送:sensorData 中只包含当前上报子设备这一条数据。
if (ARRAY_MODE_SINGLE.equals(arrayMode)) { if (ARRAY_MODE_SINGLE.equals(arrayMode)) {
pushSingleArrayData(platform, mapping, subMapping, reportSerialNumber, baseUrl, pushSingleArrayData(platform, userAuth, mapping, subMapping, reportSerialNumber, baseUrl, interfaceCode, fieldName, currentItem, currentBodyFields);
interfaceCode, fieldName, currentItem, currentBodyFields);
return; return;
} }
if (subMapping.isReference()) { if (subMapping.isReference()) {
// 基准点上报:缓存当前基准点,并补推此前已缓存的监测点 // 基准点上报:缓存当前基准点,并补推此前已缓存的监测点
bufferService.saveReferenceData( bufferService.saveReferenceData(platform.getId(), fieldName, reportSerialNumber, currentItem, reportIntervalSec);
platform.getId(), fieldName, reportSerialNumber, currentItem, reportIntervalSec); int replayCount = replayPendingMonitorData(platform, userAuth, mapping, fieldName, currentItem,
int replayCount = replayPendingMonitorData(platform, mapping, fieldName, currentItem,
reportIntervalSec, baseUrl, interfaceCode); reportIntervalSec, baseUrl, interfaceCode);
log.info("[DCP] 基准点 {} 已缓存,补推监测点 {} 条", reportSerialNumber, replayCount); log.info("[DCP] 基准点 {} 已缓存,补推监测点 {} 条", reportSerialNumber, replayCount);
return; return;
@ -219,8 +212,7 @@ public class DcpPlatformAdapter {
// 获取基准点序列号,优先使用子映射中当前标记的 reference 行 // 获取基准点序列号,优先使用子映射中当前标记的 reference 行
String refSerialNumber = resolveRefSerialNumber(subMapping, mapping); String refSerialNumber = resolveRefSerialNumber(subMapping, mapping);
JSONObject refData = bufferService.getReferenceDataIfFresh( JSONObject refData = bufferService.getReferenceDataIfFresh(platform.getId(), fieldName, refSerialNumber, reportIntervalSec);
platform.getId(), fieldName, refSerialNumber, reportIntervalSec);
if (refData != null && refData.getJSONObject("itemData") != null) { if (refData != null && refData.getJSONObject("itemData") != null) {
JSONArray sensorData = new JSONArray(); JSONArray sensorData = new JSONArray();
sensorData.add(refData.getJSONObject("itemData")); sensorData.add(refData.getJSONObject("itemData"));
@ -228,12 +220,11 @@ public class DcpPlatformAdapter {
JSONArray pushBody = buildArrayPushBody(mapping, currentBodyFields, fieldName, sensorData, monitorDeviceId); JSONArray pushBody = buildArrayPushBody(mapping, currentBodyFields, fieldName, sensorData, monitorDeviceId);
PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); 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); pushLogService.asyncSave(logEntry);
log.info("[DCP] 监测点 {} 基准点就绪,已推送 deviceId={}", reportSerialNumber, monitorDeviceId); log.info("[DCP] 监测点 {} 基准点就绪,已推送 deviceId={}", reportSerialNumber, monitorDeviceId);
} else { } else {
bufferService.saveMonitorData( bufferService.saveMonitorData(platform.getId(), fieldName, reportSerialNumber, currentItem, currentBodyFields, reportIntervalSec);
platform.getId(), fieldName, reportSerialNumber, currentItem, currentBodyFields, reportIntervalSec);
log.info("[DCP] 监测点 {} 基准点未就绪,已缓存等待", reportSerialNumber); log.info("[DCP] 监测点 {} 基准点未就绪,已缓存等待", reportSerialNumber);
} }
} catch (Exception e) { } 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, PlatformDeviceSubMapping subMapping, String reportSerialNumber,
String baseUrl, String interfaceCode, String fieldName, String baseUrl, String interfaceCode, String fieldName,
JSONObject currentItem, JSONObject currentBodyFields) { JSONObject currentItem, JSONObject currentBodyFields) {
@ -259,7 +250,7 @@ public class DcpPlatformAdapter {
JSONArray pushBody = buildArrayPushBody(mapping, currentBodyFields, fieldName, sensorData, deviceId); JSONArray pushBody = buildArrayPushBody(mapping, currentBodyFields, fieldName, sensorData, deviceId);
PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); 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); pushLogService.asyncSave(logEntry);
log.info("[DCP] 子设备 {} 单点数组已推送 deviceId={}", reportSerialNumber, deviceId); log.info("[DCP] 子设备 {} 单点数组已推送 deviceId={}", reportSerialNumber, deviceId);
} }
@ -267,36 +258,32 @@ public class DcpPlatformAdapter {
/** /**
* 请求 schema 是否包含数组字段 * 请求 schema 是否包含数组字段
*/ */
private boolean hasArrayField(JSONArray requestSchema) { private boolean hasArrayField(List<RequestSchemaField> requestSchema) {
if (requestSchema == null || requestSchema.isEmpty()) { if (requestSchema == null || requestSchema.isEmpty()) {
return false; return false;
} }
for (int i = 0; i < requestSchema.size(); i++) { for (RequestSchemaField field : requestSchema) {
JSONObject field = requestSchema.getJSONObject(i); if ("array".equalsIgnoreCase(field.getType())) {
if ("array".equalsIgnoreCase(field.getString("type"))) {
return true; return true;
} }
} }
return false; return false;
} }
private String resolveArrayFieldName(JSONArray requestSchema, String fallback) { private String resolveArrayFieldName(List<RequestSchemaField> requestSchema) {
if (StringUtils.hasText(fallback)) { RequestSchemaField firstArrayField = findFirstArrayFieldSchema(requestSchema);
return fallback; if (firstArrayField != null && StringUtils.hasText(firstArrayField.getName())) {
} return firstArrayField.getName();
JSONObject firstArrayField = findFirstArrayFieldSchema(requestSchema);
if (firstArrayField != null && StringUtils.hasText(firstArrayField.getString("name"))) {
return firstArrayField.getString("name");
} }
return "sensorData"; return "sensorData";
} }
private String resolveArrayMode(JSONObject arrayFieldSchema, JSONObject extra) { private String resolveArrayMode(RequestSchemaField arrayFieldSchema, ExtraMappingConfig extra) {
String mode = null; String mode = null;
if (extra != null && StringUtils.hasText(extra.getString("arrayMode"))) { if (extra != null && StringUtils.hasText(extra.getArrayMode())) {
mode = extra.getString("arrayMode"); mode = extra.getArrayMode();
} else if (arrayFieldSchema != null && StringUtils.hasText(arrayFieldSchema.getString("arrayMode"))) { } else if (arrayFieldSchema != null && StringUtils.hasText(arrayFieldSchema.getArrayMode())) {
mode = arrayFieldSchema.getString("arrayMode"); mode = arrayFieldSchema.getArrayMode();
} }
if (!StringUtils.hasText(mode)) { if (!StringUtils.hasText(mode)) {
return ARRAY_MODE_REFERENCE_PAIR; return ARRAY_MODE_REFERENCE_PAIR;
@ -319,14 +306,9 @@ public class DcpPlatformAdapter {
} }
// 兼容历史配置:从 onboard_config 取基准点原始序列号 // 兼容历史配置:从 onboard_config 取基准点原始序列号
if (StringUtils.hasText(subMapping.getOnboardConfig())) { OnboardConfig config = subMapping.getOnboardConfigParsed();
try { if (config != null && StringUtils.hasText(config.getRefSerialNumber())) {
JSONObject config = JSON.parseObject(subMapping.getOnboardConfig()); return config.getRefSerialNumber();
String refSensorId = config.getString("refSerialNumber");
if (StringUtils.hasText(refSensorId)) {
return refSensorId;
}
} catch (Exception ignored) {}
} }
// 回退:用网关本身的序列号 // 回退:用网关本身的序列号
return mapping.getSerialNumber(); 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, String fieldName, JSONObject referenceItem,
int reportIntervalSec, String baseUrl, String interfaceCode) { int reportIntervalSec, String baseUrl, String interfaceCode) {
List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId()); List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId());
@ -375,7 +357,7 @@ public class DcpPlatformAdapter {
sensorData, monitor.getPlatformDeviceId()); sensorData, monitor.getPlatformDeviceId());
PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, monitor.getSubSerialNumber()); 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); pushLogService.asyncSave(logEntry);
if (logEntry.getStatus() != null && logEntry.getStatus() == 1) { if (logEntry.getStatus() != null && logEntry.getStatus() == 1) {
bufferService.removeMonitorData(platform.getId(), fieldName, monitor.getSubSerialNumber()); 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 reportSerialNumber, String payload, String pushPath,
String interfaceCode, JSONArray requestSchema) { String interfaceCode, List<RequestSchemaField> requestSchema) {
Object body = buildPushBody(platform, mapping, payload, requestSchema); Object body = buildPushBody(platform, userAuth, mapping, payload, requestSchema);
if (body == null) { if (body == null) {
log.warn("[DCP] 设备 {} 字段映射结果为空,跳过推送", mapping.getSerialNumber()); log.warn("[DCP] 设备 {} 字段映射结果为空,跳过推送", mapping.getSerialNumber());
return; return;
} }
PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); 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); pushLogService.asyncSave(logEntry);
} }
private Object buildPushBody(PlatformInstance platform, PlatformDeviceMapping mapping, private Object buildPushBody(PlatformInstance platform, PlatformUserAuth userAuth,PlatformDeviceMapping mapping,
String payload, JSONArray requestSchema) { String payload, List<RequestSchemaField> requestSchema) {
if (!StringUtils.hasText(payload)) { if (!StringUtils.hasText(payload)) {
return null; return null;
} }
@ -415,12 +397,12 @@ public class DcpPlatformAdapter {
return null; return null;
} }
JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping()); List<FieldMappingItem> fieldMappings = mapping.getFieldMappingParsed();
if (requestSchema == null || requestSchema.isEmpty()) { if (requestSchema == null || requestSchema.isEmpty()) {
return buildLegacyPushBody(mapping.getPlatformDeviceId(), fieldMappings, reportData); 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)) { if (dcpData.isEmpty() || hasPayloadDrivenValue(dcpData, requestSchema)) {
return buildLegacyPushBody(mapping.getPlatformDeviceId(), fieldMappings, reportData); return buildLegacyPushBody(mapping.getPlatformDeviceId(), fieldMappings, reportData);
} }
@ -430,7 +412,7 @@ public class DcpPlatformAdapter {
return arr; 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()) { if (fieldMappings == null || fieldMappings.isEmpty()) {
return null; return null;
} }
@ -439,10 +421,9 @@ public class DcpPlatformAdapter {
dcpData.put("deviceId", platformDeviceId); dcpData.put("deviceId", platformDeviceId);
dcpData.put("epoch", nowIsoTimestamp()); dcpData.put("epoch", nowIsoTimestamp());
for (int i = 0; i < fieldMappings.size(); i++) { for (FieldMappingItem fm : fieldMappings) {
JSONObject fm = fieldMappings.getJSONObject(i); String identifier = fm.getIdentifier();
String identifier = fm.getString("identifier"); String platformField = fm.getPlatformField();
String platformField = fm.getString("platformField");
if (!StringUtils.hasText(identifier) || !StringUtils.hasText(platformField)) { if (!StringUtils.hasText(identifier) || !StringUtils.hasText(platformField)) {
continue; continue;
} }
@ -466,18 +447,16 @@ public class DcpPlatformAdapter {
* <p> * <p>
* 使用子设备 platformDeviceId 和主表 fieldMapping 组装请求体 * 使用子设备 platformDeviceId 和主表 fieldMapping 组装请求体
*/ */
private void handleSubDeviceSimplePush(PlatformInstance platform, PlatformDeviceMapping mapping, private void handleSubDeviceSimplePush(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
PlatformDeviceSubMapping subMapping, PlatformDeviceSubMapping subMapping, String reportSerialNumber, String payload,
String reportSerialNumber, String payload, String pushPath, String interfaceCode, List<RequestSchemaField> requestSchema) {
String pushPath, String interfaceCode, Object body = buildSubDevicePushBody(platform, userAuth, mapping, subMapping, payload, requestSchema);
JSONArray requestSchema) {
Object body = buildSubDevicePushBody(platform, mapping, subMapping, payload, requestSchema);
if (body == null) { if (body == null) {
log.warn("[DCP] 网关子设备 {} 字段映射结果为空,跳过推送", reportSerialNumber); log.warn("[DCP] 网关子设备 {} 字段映射结果为空,跳过推送", reportSerialNumber);
return; return;
} }
PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); 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); pushLogService.asyncSave(logEntry);
} }
@ -486,9 +465,9 @@ public class DcpPlatformAdapter {
* <p> * <p>
* 复用主表 fieldMapping 进行物模型字段映射,deviceId 取自子映射行 * 复用主表 fieldMapping 进行物模型字段映射,deviceId 取自子映射行
*/ */
private Object buildSubDevicePushBody(PlatformInstance platform, PlatformDeviceMapping mapping, private Object buildSubDevicePushBody(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
PlatformDeviceSubMapping subMapping, String payload, PlatformDeviceSubMapping subMapping, String payload,
JSONArray requestSchema) { List<RequestSchemaField> requestSchema) {
JSONObject reportData = parsePayloadToMap(payload); JSONObject reportData = parsePayloadToMap(payload);
if (reportData == null || reportData.isEmpty()) { if (reportData == null || reportData.isEmpty()) {
return null; return null;
@ -498,12 +477,12 @@ public class DcpPlatformAdapter {
String platformDeviceId = StringUtils.hasText(subMapping.getPlatformDeviceId()) String platformDeviceId = StringUtils.hasText(subMapping.getPlatformDeviceId())
? subMapping.getPlatformDeviceId() : mapping.getPlatformDeviceId(); ? subMapping.getPlatformDeviceId() : mapping.getPlatformDeviceId();
JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping()); List<FieldMappingItem> fieldMappings = mapping.getFieldMappingParsed();
if (requestSchema == null || requestSchema.isEmpty()) { if (requestSchema == null || requestSchema.isEmpty()) {
return buildLegacyPushBody(platformDeviceId, fieldMappings, reportData); 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)) { if (dcpData.isEmpty() || hasPayloadDrivenValue(dcpData, requestSchema)) {
return buildLegacyPushBody(platformDeviceId, fieldMappings, reportData); return buildLegacyPushBody(platformDeviceId, fieldMappings, reportData);
@ -538,23 +517,22 @@ public class DcpPlatformAdapter {
return arr; return arr;
} }
private JSONObject buildObjectBySchema(PlatformInstance platform, PlatformDeviceMapping mapping, private JSONObject buildObjectBySchema(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
JSONArray requestSchema, JSONObject reportData, List<RequestSchemaField> requestSchema, JSONObject reportData,
JSONArray fieldMappings) { List<FieldMappingItem> fieldMappings) {
JSONObject body = new JSONObject(); JSONObject body = new JSONObject();
if (requestSchema == null || requestSchema.isEmpty()) { if (requestSchema == null || requestSchema.isEmpty()) {
return body; return body;
} }
for (int i = 0; i < requestSchema.size(); i++) { for (RequestSchemaField schemaField : requestSchema) {
JSONObject schemaField = requestSchema.getJSONObject(i); if ("array".equalsIgnoreCase(schemaField.getType())) {
if ("array".equalsIgnoreCase(schemaField.getString("type"))) {
continue; continue;
} }
String fieldName = schemaField.getString("name"); String fieldName = schemaField.getName();
if (!StringUtils.hasText(fieldName)) { if (!StringUtils.hasText(fieldName)) {
continue; continue;
} }
Object value = resolveSchemaFieldValue(platform, mapping, schemaField, reportData, fieldMappings); Object value = resolveSchemaFieldValue(platform, userAuth, mapping, schemaField, reportData, fieldMappings);
if (value != null) { if (value != null) {
body.put(fieldName, value); body.put(fieldName, value);
} }
@ -562,25 +540,25 @@ public class DcpPlatformAdapter {
return body; return body;
} }
private Object resolveSchemaFieldValue(PlatformInstance platform, PlatformDeviceMapping mapping, private Object resolveSchemaFieldValue(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
JSONObject schemaField, JSONObject reportData, RequestSchemaField schemaField, JSONObject reportData,
JSONArray fieldMappings) { List<FieldMappingItem> fieldMappings) {
String source = schemaField.getString("source"); String source = schemaField.getSource();
String fieldName = schemaField.getString("name"); String fieldName = schemaField.getName();
String configField = schemaField.getString("field"); String configField = schemaField.getField();
String contextKey = schemaField.getString("contextKey"); String contextKey = schemaField.getContextKey();
if ("fixed".equals(source)) { if ("fixed".equals(source)) {
return schemaField.get("value"); return schemaField.getValue();
} }
if ("device".equals(source)) { if ("device".equals(source)) {
return resolveDeviceField(mapping, configField); return resolveDeviceField(mapping, configField);
} }
if ("auth".equals(source)) { if ("auth".equals(source)) {
return resolveJsonField(platform.getAuthConfig(), configField); return resolveJsonField(userAuth.getAuthConfig(), configField);
} }
if ("extra".equals(source)) { if ("extra".equals(source)) {
return resolveJsonField(platform.getExtraConfig(), configField); return resolveJsonField(userAuth.getExtraConfig(), configField);
} }
if (isThingsModelSource(source)) { if (isThingsModelSource(source)) {
return resolveThingsModelValue(reportData, fieldMappings, fieldName, contextKey); return resolveThingsModelValue(reportData, fieldMappings, fieldName, contextKey);
@ -601,11 +579,11 @@ public class DcpPlatformAdapter {
return null; return null;
} }
private JSONObject buildArrayItem(PlatformInstance platform, PlatformDeviceMapping mapping, private JSONObject buildArrayItem(PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
JSONArray requestSchema, JSONObject reportData, String fieldName, List<RequestSchemaField> requestSchema, JSONObject reportData, String fieldName,
JSONArray itemFieldMappings, String subDeviceId) { List<ExtraMappingConfig.ItemFieldMapping> itemFieldMappings, String subDeviceId) {
JSONObject arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName); RequestSchemaField arrayFieldSchema = findArrayFieldSchema(requestSchema, fieldName);
JSONArray itemSchema = arrayFieldSchema == null ? null : arrayFieldSchema.getJSONArray("itemSchema"); List<RequestSchemaField> itemSchema = arrayFieldSchema == null ? null : arrayFieldSchema.getItemSchema();
if (itemSchema == null || itemSchema.isEmpty()) { if (itemSchema == null || itemSchema.isEmpty()) {
return buildLegacyArrayItem(reportData, itemFieldMappings, subDeviceId); return buildLegacyArrayItem(reportData, itemFieldMappings, subDeviceId);
} }
@ -613,20 +591,18 @@ public class DcpPlatformAdapter {
JSONObject item = new JSONObject(); JSONObject item = new JSONObject();
boolean hasThingsModelField = false; boolean hasThingsModelField = false;
boolean hasThingsModelValue = false; boolean hasThingsModelValue = false;
for (int i = 0; i < itemSchema.size(); i++) { for (RequestSchemaField schemaField : itemSchema) {
JSONObject schemaField = itemSchema.getJSONObject(i); String itemFieldName = schemaField.getName();
String itemFieldName = schemaField.getString("name");
if (!StringUtils.hasText(itemFieldName)) { if (!StringUtils.hasText(itemFieldName)) {
continue; continue;
} }
if (isThingsModelSource(schemaField.getString("source"))) { if (isThingsModelSource(schemaField.getSource())) {
hasThingsModelField = true; hasThingsModelField = true;
} }
Object value = resolveArrayItemFieldValue( Object value = resolveArrayItemFieldValue(userAuth, mapping, schemaField, reportData, itemFieldMappings, subDeviceId);
platform, mapping, schemaField, reportData, itemFieldMappings, subDeviceId);
if (value != null) { if (value != null) {
item.put(itemFieldName, value); item.put(itemFieldName, value);
if (isThingsModelSource(schemaField.getString("source"))) { if (isThingsModelSource(schemaField.getSource())) {
hasThingsModelValue = true; hasThingsModelValue = true;
} }
} }
@ -637,51 +613,51 @@ public class DcpPlatformAdapter {
return item; 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); Map<String, Object> itemValues = extractItemValues(reportData, itemFieldMappings);
Object height = itemValues.get("height"); Object height = itemValues.get("height");
Object temperature = itemValues.get("temperature"); Object temperature = itemValues.get("temperature");
if (height == null && temperature == null) { if (height == null && temperature == null) {
return null; return null;
} }
String heightIdentifier = findIdentifier(itemFieldMappings, "height"); String heightIdentifier = findIdentifierFromItem(itemFieldMappings, "height");
String epochIdentifier = StringUtils.hasText(heightIdentifier) String epochIdentifier = StringUtils.hasText(heightIdentifier)
? heightIdentifier : findFirstIdentifier(itemFieldMappings); ? heightIdentifier : findFirstIdentifierFromItem(itemFieldMappings);
Object epoch = resolveEpoch(reportData, epochIdentifier); Object epoch = resolveEpoch(reportData, epochIdentifier);
return buildSensorDataItem(epoch, height, temperature, subDeviceId); return buildSensorDataItem(epoch, height, temperature, subDeviceId);
} }
private Object resolveArrayItemFieldValue(PlatformInstance platform, PlatformDeviceMapping mapping, private Object resolveArrayItemFieldValue(PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
JSONObject schemaField, JSONObject reportData, RequestSchemaField schemaField, JSONObject reportData,
JSONArray itemFieldMappings, String subDeviceId) { List<ExtraMappingConfig.ItemFieldMapping> itemFieldMappings, String subDeviceId) {
String source = schemaField.getString("source"); String source = schemaField.getSource();
String fieldName = schemaField.getString("name"); String fieldName = schemaField.getName();
if ("fixed".equals(source)) { if ("fixed".equals(source)) {
return schemaField.get("value"); return schemaField.getValue();
} }
if ("subDeviceId".equals(source)) { if ("subDeviceId".equals(source)) {
return subDeviceId; return subDeviceId;
} }
if ("ts".equals(source)) { if ("ts".equals(source)) {
return resolveTimestampValue(reportData, itemFieldMappings, fieldName, schemaField.getString("contextKey")); return resolveTimestampValueFromItem(reportData, itemFieldMappings, fieldName, schemaField.getContextKey());
} }
if ("device".equals(source)) { if ("device".equals(source)) {
return resolveDeviceField(mapping, schemaField.getString("field")); return resolveDeviceField(mapping, schemaField.getField());
} }
if ("auth".equals(source)) { if ("auth".equals(source)) {
return resolveJsonField(platform.getAuthConfig(), schemaField.getString("field")); return resolveJsonField(userAuth.getAuthConfig(), schemaField.getField());
} }
if ("extra".equals(source)) { if ("extra".equals(source)) {
return resolveJsonField(platform.getExtraConfig(), schemaField.getString("field")); return resolveJsonField(userAuth.getExtraConfig(), schemaField.getField());
} }
if (isThingsModelSource(source)) { if (isThingsModelSource(source)) {
String identifier = findIdentifier(itemFieldMappings, fieldName); String identifier = findIdentifierFromItem(itemFieldMappings, fieldName);
return StringUtils.hasText(identifier) ? reportData.get(identifier) : null; return StringUtils.hasText(identifier) ? reportData.get(identifier) : null;
} }
if ("context".equals(source)) { if ("context".equals(source)) {
String contextKey = schemaField.getString("contextKey"); String contextKey = schemaField.getContextKey();
if ("reportTime".equals(contextKey) || "epoch".equals(fieldName)) { if ("reportTime".equals(contextKey) || "epoch".equals(fieldName)) {
return resolveTimestampValue(reportData, itemFieldMappings, fieldName, contextKey); return resolveTimestampValueFromItem(reportData, itemFieldMappings, fieldName, contextKey);
} }
Object contextValue = reportData.get(contextKey); Object contextValue = reportData.get(contextKey);
if (contextValue != null) { if (contextValue != null) {
@ -691,33 +667,51 @@ public class DcpPlatformAdapter {
return null; 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)) { if (requestSchema == null || requestSchema.isEmpty() || !StringUtils.hasText(fieldName)) {
return null; return null;
} }
for (int i = 0; i < requestSchema.size(); i++) { for (RequestSchemaField schemaField : requestSchema) {
JSONObject schemaField = requestSchema.getJSONObject(i); if ("array".equalsIgnoreCase(schemaField.getType())
if ("array".equalsIgnoreCase(schemaField.getString("type")) && fieldName.equals(schemaField.getName())) {
&& fieldName.equals(schemaField.getString("name"))) {
return schemaField; return schemaField;
} }
} }
return null; return null;
} }
private JSONObject findFirstArrayFieldSchema(JSONArray requestSchema) { private RequestSchemaField findFirstArrayFieldSchema(List<RequestSchemaField> requestSchema) {
if (requestSchema == null || requestSchema.isEmpty()) { if (requestSchema == null || requestSchema.isEmpty()) {
return null; return null;
} }
for (int i = 0; i < requestSchema.size(); i++) { for (RequestSchemaField schemaField : requestSchema) {
JSONObject schemaField = requestSchema.getJSONObject(i); if ("array".equalsIgnoreCase(schemaField.getType())) {
if ("array".equalsIgnoreCase(schemaField.getString("type"))) {
return schemaField; return schemaField;
} }
} }
return null; 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) { private JSONObject copyJsonObject(JSONObject source) {
if (source == null || source.isEmpty()) { if (source == null || source.isEmpty()) {
return new JSONObject(); return new JSONObject();
@ -725,59 +719,13 @@ public class DcpPlatformAdapter {
return JSON.parseObject(source.toJSONString()); 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) { private String resolvePushPath(PlatformDeviceMapping mapping, PlatformInterface pushInterface) {
if (pushInterface != null && StringUtils.hasText(pushInterface.getPath())) { if (pushInterface != null && StringUtils.hasText(pushInterface.getPath())) {
return pushInterface.getPath(); return pushInterface.getPath();
} }
String sensorType = mapping == null ? null : mapping.getSensorType(); String sensorType = mapping == null ? null : mapping.getSensorType();
String serialNumber = mapping == null ? null : mapping.getSerialNumber(); String serialNumber = mapping == null ? null : mapping.getSerialNumber();
throw new RuntimeException("未配置推送接口,请在设备绑定中选择推送接口" throw new RuntimeException("未配置推送接口,请在设备绑定中选择推送接口" + ",设备=" + serialNumber + ",传感器类型=" + sensorType);
+ ",设备=" + serialNumber + ",传感器类型=" + sensorType);
} }
private String resolveInterfaceCode(String sensorType, PlatformInterface pushInterface) { private String resolveInterfaceCode(String sensorType, PlatformInterface pushInterface) {
@ -787,19 +735,7 @@ public class DcpPlatformAdapter {
return "dcp_push_" + sensorType; return "dcp_push_" + sensorType;
} }
private JSONArray parseJsonArray(String json) { private Object resolveThingsModelValue(JSONObject reportData, List<FieldMappingItem> fieldMappings,
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,
String platformField, String fallbackKey) { String platformField, String fallbackKey) {
String identifier = findIdentifier(fieldMappings, platformField); String identifier = findIdentifier(fieldMappings, platformField);
if (StringUtils.hasText(identifier)) { if (StringUtils.hasText(identifier)) {
@ -811,7 +747,7 @@ public class DcpPlatformAdapter {
return null; return null;
} }
private Object resolveTimestampValue(JSONObject reportData, JSONArray fieldMappings, private Object resolveTimestampValue(JSONObject reportData, List<FieldMappingItem> fieldMappings,
String platformField, String fallbackKey) { String platformField, String fallbackKey) {
String identifier = findIdentifier(fieldMappings, platformField); String identifier = findIdentifier(fieldMappings, platformField);
if (StringUtils.hasText(identifier)) { if (StringUtils.hasText(identifier)) {
@ -827,6 +763,22 @@ public class DcpPlatformAdapter {
return nowIsoTimestamp(); 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) { private Object resolveDeviceField(PlatformDeviceMapping mapping, String field) {
if (mapping == null || !StringUtils.hasText(field)) { if (mapping == null || !StringUtils.hasText(field)) {
return null; return null;
@ -875,41 +827,61 @@ public class DcpPlatformAdapter {
return item; 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<>(); Map<String, Object> values = new HashMap<>();
if (itemFieldMappings == null) return values; if (itemFieldMappings == null) return values;
for (int i = 0; i < itemFieldMappings.size(); i++) { for (ExtraMappingConfig.ItemFieldMapping im : itemFieldMappings) {
JSONObject im = itemFieldMappings.getJSONObject(i); if (StringUtils.hasText(im.getIdentifier()) && StringUtils.hasText(im.getPlatformField())) {
String platformField = im.getString("platformField"); values.put(im.getPlatformField(), reportData.get(im.getIdentifier()));
String identifier = im.getString("identifier");
if (StringUtils.hasText(identifier) && StringUtils.hasText(platformField)) {
values.put(platformField, reportData.get(identifier));
} }
} }
return values; return values;
} }
private String findIdentifier(JSONArray itemFieldMappings, String platformField) { private String findIdentifier(List<FieldMappingItem> itemFieldMappings, String platformField) {
if (itemFieldMappings == null || !StringUtils.hasText(platformField)) { if (itemFieldMappings == null || !StringUtils.hasText(platformField)) {
return null; return null;
} }
for (int i = 0; i < itemFieldMappings.size(); i++) { for (FieldMappingItem im : itemFieldMappings) {
JSONObject im = itemFieldMappings.getJSONObject(i); if (platformField.equals(im.getPlatformField())) {
if (platformField.equals(im.getString("platformField"))) { return im.getIdentifier();
return im.getString("identifier");
} }
} }
return null; 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()) { if (itemFieldMappings == null || itemFieldMappings.isEmpty()) {
return null; return null;
} }
for (int i = 0; i < itemFieldMappings.size(); i++) { for (FieldMappingItem im : itemFieldMappings) {
String identifier = itemFieldMappings.getJSONObject(i).getString("identifier"); if (StringUtils.hasText(im.getIdentifier())) {
if (StringUtils.hasText(identifier)) { return im.getIdentifier();
return identifier; }
}
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; return null;
@ -917,13 +889,13 @@ public class DcpPlatformAdapter {
// ==================== HTTP 执行 ==================== // ==================== 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(); long startMs = System.currentTimeMillis();
logEntry.setRequestUrl(url); logEntry.setRequestUrl(url);
logEntry.setRequestBody(bodyStr); logEntry.setRequestBody(bodyStr);
try { try {
String token = ensureToken(platform); String token = ensureToken(platform, userAuth, mapping);
Map<String, Object> headers = new HashMap<>(); Map<String, Object> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + token); headers.put("Authorization", "Bearer " + token);
headers.put("Content-Type", "application/json"); headers.put("Content-Type", "application/json");
@ -1080,10 +1052,10 @@ public class DcpPlatformAdapter {
* 当前上报的子设备使用传入的 payload(最新数据),其余子设备从 Redis 缓存读取。 * 当前上报的子设备使用传入的 payload(最新数据),其余子设备从 Redis 缓存读取。
* 推送顺序:从浅到深(与绑定时从深到浅相反)。 * 推送顺序:从浅到深(与绑定时从深到浅相反)。
*/ */
private void pushBatchSubDevicesData(PlatformInstance platform, PlatformDeviceMapping mapping, private void pushBatchSubDevicesData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
PlatformDeviceSubMapping currentSubMapping, String reportSerialNumber, PlatformDeviceSubMapping currentSubMapping, String reportSerialNumber,
String payload, String pushPath, String interfaceCode, String payload, String pushPath, String interfaceCode,
JSONArray requestSchema) { List<RequestSchemaField> requestSchema) {
try { try {
// 1. 查询所有子设备。批量推送只依赖子映射顺序和推送接口 schema, // 1. 查询所有子设备。批量推送只依赖子映射顺序和推送接口 schema,
// 不再从推送接口 request_schema 读取入网用的 sensorDepths。 // 不再从推送接口 request_schema 读取入网用的 sensorDepths。
@ -1116,16 +1088,16 @@ public class DcpPlatformAdapter {
} }
// 3. 准备公共参数(复用已有 fieldMapping + requestSchema) // 3. 准备公共参数(复用已有 fieldMapping + requestSchema)
JSONArray fieldMappings = parseJsonArray(mapping.getFieldMapping()); List<FieldMappingItem> fieldMappings = mapping.getFieldMappingParsed();
String fieldName = resolveArrayFieldName(requestSchema, null); String fieldName = resolveArrayFieldName(requestSchema);
JSONObject extra = null; List<ExtraMappingConfig.ItemFieldMapping> itemFieldMappings = null;
if (StringUtils.hasText(mapping.getExtraMapping())) { ExtraMappingConfig extra = mapping.getExtraMappingParsed();
extra = JSON.parseObject(mapping.getExtraMapping()); if (extra != null) {
if (StringUtils.hasText(extra.getString("fieldName"))) { if (StringUtils.hasText(extra.getFieldName())) {
fieldName = extra.getString("fieldName"); fieldName = extra.getFieldName();
} }
itemFieldMappings = extra.getItemFieldMappings();
} }
JSONArray itemFieldMappings = extra != null ? extra.getJSONArray("itemFieldMappings") : null;
// 解析当前上报子设备的 payload // 解析当前上报子设备的 payload
JSONObject currentReportData = parsePayloadToMap(payload); JSONObject currentReportData = parsePayloadToMap(payload);
@ -1149,7 +1121,7 @@ public class DcpPlatformAdapter {
} }
String sensorId = resolveSubDeviceSensorId(sub); 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()) { if (sensorDataItem == null || sensorDataItem.isEmpty()) {
log.warn("[DCP] 子设备 {} 数据映射为空,跳过其 sensorData", subSn); log.warn("[DCP] 子设备 {} 数据映射为空,跳过其 sensorData", subSn);
continue; continue;
@ -1190,7 +1162,7 @@ public class DcpPlatformAdapter {
continue; continue;
} }
JSONObject bodyFields = buildObjectBySchema(platform, mapping, requestSchema, reportData, fieldMappings); JSONObject bodyFields = buildObjectBySchema(platform, userAuth, mapping, requestSchema, reportData, fieldMappings);
JSONArray deviceBodyArr = buildArrayPushBody(mapping, bodyFields, fieldName, JSONArray deviceBodyArr = buildArrayPushBody(mapping, bodyFields, fieldName,
sensorDataArr, sub.getPlatformDeviceId()); sensorDataArr, sub.getPlatformDeviceId());
if (!deviceBodyArr.isEmpty()) { if (!deviceBodyArr.isEmpty()) {
@ -1207,7 +1179,7 @@ public class DcpPlatformAdapter {
// 7. 一次 HTTP 请求推送 // 7. 一次 HTTP 请求推送
String baseUrl = platform.getBaseUrl() + pushPath; String baseUrl = platform.getBaseUrl() + pushPath;
PlatformPushLog logEntry = createLogEntry(platform, mapping, interfaceCode, reportSerialNumber); 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); pushLogService.asyncSave(logEntry);
log.info("[DCP] 批量推送完成 mappingId={} 当前子设备={} 推送设备对象数={}/{}", log.info("[DCP] 批量推送完成 mappingId={} 当前子设备={} 推送设备对象数={}/{}",
@ -1265,49 +1237,39 @@ public class DcpPlatformAdapter {
* 优先从 onboard_config 中取 sensorId,否则使用子设备序列号。 * 优先从 onboard_config 中取 sensorId,否则使用子设备序列号。
*/ */
private String resolveSubDeviceSensorId(PlatformDeviceSubMapping subMapping) { private String resolveSubDeviceSensorId(PlatformDeviceSubMapping subMapping) {
if (StringUtils.hasText(subMapping.getOnboardConfig())) { OnboardConfig config = subMapping.getOnboardConfigParsed();
try { if (config != null && StringUtils.hasText(config.getSensorId())) {
JSONObject config = JSON.parseObject(subMapping.getOnboardConfig()); return config.getSensorId();
String sensorId = config.getString("sensorId");
if (StringUtils.hasText(sensorId)) {
return sensorId;
}
} catch (Exception e) {
log.debug("[DCP] 解析 onboardConfig 失败 subSn={}", subMapping.getSubSerialNumber());
}
} }
return subMapping.getSubSerialNumber(); return subMapping.getSubSerialNumber();
} }
// ==================== Token 管理 ==================== // ==================== Token 管理 ====================
private String ensureToken(PlatformInstance platform) { private String ensureToken(PlatformInstance platform, PlatformUserAuth platformUserAuth, PlatformDeviceMapping mapping) {
// 校验是否过期
Date now = new Date(); Date now = new Date();
boolean expired = platform.getTokenExpiresAt() == null boolean expired = platformUserAuth.getTokenExpiresAt() == null
|| platform.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L; || platformUserAuth.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L;
if (!StringUtils.hasText(platform.getAccessToken()) || expired) { if (!StringUtils.hasText(platformUserAuth.getAccessToken()) || expired) {
PlatformInstance fresh = instanceService.getById(platform.getId()); // 执行刷新token流程
boolean stillExpired = fresh.getTokenExpiresAt() == null PlatformFlowMapper flowMapper = SpringUtils.getBean(PlatformFlowMapper.class);
|| fresh.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L; PlatformFlow tokenFlow = flowMapper.selectOne(
if (!StringUtils.hasText(fresh.getAccessToken()) || stillExpired) { Wrappers.<PlatformFlow>lambdaQuery()
PlatformFlowMapper flowMapper = SpringUtils.getBean(PlatformFlowMapper.class); .eq(PlatformFlow::getPlatformId, platform.getId())
PlatformFlow tokenFlow = flowMapper.selectOne( .eq(PlatformFlow::getFlowType, 3)
Wrappers.<PlatformFlow>lambdaQuery() .eq(PlatformFlow::getEnable, "1")
.eq(PlatformFlow::getPlatformId, fresh.getId()) .last("LIMIT 1"));
.eq(PlatformFlow::getFlowType, 3) if (tokenFlow != null) {
.eq(PlatformFlow::getEnable, "1") FlowContext ctx = new FlowContext(platform, mapping, platformUserAuth);
.last("LIMIT 1")); flowExecutor.execute(tokenFlow, ctx);
if (tokenFlow != null) { platformUserAuth = platformUserAuthMapper.selectById(mapping.getPlatformUserAuthId());
FlowContext ctx = new FlowContext(fresh); } else {
flowExecutor.execute(tokenFlow, ctx); log.warn("[DCP] 平台 {} 未配置 Token 刷新流程(flow_type=3)", platform.getId());
fresh = instanceService.getById(platform.getId());
} else {
log.warn("[DCP] 平台 {} 未配置 Token 刷新流程(flow_type=3)", fresh.getId());
}
} }
platform.setAccessToken(fresh.getAccessToken());
platform.setTokenExpiresAt(fresh.getTokenExpiresAt());
} }
return platform.getAccessToken();
return platformUserAuth.getAccessToken();
} }
} }

View File

@ -1,16 +1,22 @@
package com.fastbee.iot.platform.domain; package com.fastbee.iot.platform.domain;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; 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.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.springframework.util.StringUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* 平台设备绑定关系 * 平台设备绑定关系
@ -28,6 +34,9 @@ public class PlatformDeviceMapping implements Serializable {
@ApiModelProperty("所属平台ID") @ApiModelProperty("所属平台ID")
private Long platformId; private Long platformId;
@ApiModelProperty("关联用户认证ID")
private Long platformUserAuthId;
@ApiModelProperty("DCP项目ID,绑定页面用户自己选择") @ApiModelProperty("DCP项目ID,绑定页面用户自己选择")
private String platformProjectId; private String platformProjectId;
@ -68,6 +77,40 @@ public class PlatformDeviceMapping implements Serializable {
@ApiModelProperty("扩展映射配置,如静力水准子设备编号与DCP sensorId映射") @ApiModelProperty("扩展映射配置,如静力水准子设备编号与DCP sensorId映射")
private String extraMapping; 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=已入网)") @ApiModelProperty("入网状态(0=未入网,1=已入网)")
private Integer netStatus; private Integer netStatus;

View File

@ -1,12 +1,17 @@
package com.fastbee.iot.platform.domain; package com.fastbee.iot.platform.domain;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; 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.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.springframework.util.StringUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
@ -45,6 +50,23 @@ public class PlatformDeviceSubMapping implements Serializable {
@ApiModelProperty("入网时传给平台的标识参数(平台相关)") @ApiModelProperty("入网时传给平台的标识参数(平台相关)")
private String onboardConfig; 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=已入网)") @ApiModelProperty("入网状态(0=未入网,1=已入网)")
private Integer netStatus; private Integer netStatus;

View File

@ -1,13 +1,22 @@
package com.fastbee.iot.platform.domain; 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.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; 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.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.springframework.util.StringUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
import java.util.Map;
/** /**
* 流程步骤 * 流程步骤
@ -40,6 +49,41 @@ public class PlatformFlowStep implements Serializable {
@ApiModelProperty("需持久化的出参字段 [{field,saveTo}]") @ApiModelProperty("需持久化的出参字段 [{field,saveTo}]")
private String resultSave; 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("备注") @ApiModelProperty("备注")
private String remark; private String remark;
} }

View File

@ -1,12 +1,17 @@
package com.fastbee.iot.platform.domain; 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.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.springframework.util.StringUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
@ -36,10 +41,11 @@ public class PlatformInstance implements Serializable {
@ApiModelProperty("认证类型(1=密码Token,2=ApiKey,3=OAuth2)") @ApiModelProperty("认证类型(1=密码Token,2=ApiKey,3=OAuth2)")
private Integer authType; private Integer authType;
@ApiModelProperty("认证凭证JSON {userName,password,clientId,scope}") // ========================= 已弃用 =========================
@ApiModelProperty("认证配置模板(新增用户时复制){userName,password,clientId,scope}")
private String authConfig; private String authConfig;
@ApiModelProperty("初始化后保存的平台数据 {userId,projectId,...}") @ApiModelProperty("平台级配置(站点编码等)")
private String extraConfig; private String extraConfig;
@ApiModelProperty("当前有效Token") @ApiModelProperty("当前有效Token")
@ -51,6 +57,7 @@ public class PlatformInstance implements Serializable {
@ApiModelProperty("初始化状态(0=未初始化,1=已初始化)") @ApiModelProperty("初始化状态(0=未初始化,1=已初始化)")
private Integer initStatus; private Integer initStatus;
// ========================= 已弃用 =========================
@ApiModelProperty("是否启用(0=禁用,1=启用)") @ApiModelProperty("是否启用(0=禁用,1=启用)")
private String enable; private String enable;

View File

@ -1,15 +1,22 @@
package com.fastbee.iot.platform.domain; package com.fastbee.iot.platform.domain;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; 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.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.springframework.util.StringUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* 平台接口定义 * 平台接口定义
@ -48,6 +55,40 @@ public class PlatformInterface implements Serializable {
@ApiModelProperty("出参字段定义 JSON") @ApiModelProperty("出参字段定义 JSON")
private String responseSchema; 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("排序") @ApiModelProperty("排序")
private Integer sortOrder; private Integer sortOrder;

View File

@ -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;
}

View File

@ -3,6 +3,7 @@ package com.fastbee.iot.platform.flow;
import com.fastbee.iot.platform.domain.PlatformDeviceMapping; import com.fastbee.iot.platform.domain.PlatformDeviceMapping;
import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping;
import com.fastbee.iot.platform.domain.PlatformInstance; import com.fastbee.iot.platform.domain.PlatformInstance;
import com.fastbee.iot.platform.domain.PlatformUserAuth;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -17,17 +18,24 @@ public class FlowContext {
* 平台实体 * 平台实体
*/ */
private final PlatformInstance platform; private final PlatformInstance platform;
/**
* 用户认证(用户级流程时使用)
*/
private final PlatformUserAuth platformUserAuth;
/** /**
* 绑定推送关系 * 绑定推送关系
*/ */
private final PlatformDeviceMapping deviceMapping; private PlatformDeviceMapping deviceMapping;
// 绑定推送子关系 // 绑定推送子关系
private List<PlatformDeviceSubMapping> monitorSubMappings; private List<PlatformDeviceSubMapping> monitorSubMappings;
/** /**
* 设备上报的原始 payload(实时推送时有值) * 设备上报的原始 payload(实时推送时有值)
*/ */
private final String payload; private String payload;
/** /**
* 步骤出参存储,key = step.resultKey,value = 解析后的 JSON Map * 步骤出参存储,key = step.resultKey,value = 解析后的 JSON Map
*/ */
@ -62,20 +70,55 @@ public class FlowContext {
*/ */
private String referenceSubMappingOnboardConfig; 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.platform = platform;
this.deviceMapping = deviceMapping; this.deviceMapping = deviceMapping;
this.platformUserAuth = platformUserAuth;
this.payload = payload; this.payload = payload;
} }
public FlowContext(PlatformInstance platform) { public FlowContext(PlatformInstance platform, PlatformDeviceMapping deviceMapping, PlatformUserAuth platformUserAuth) {
this(platform, null, null); 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() { public PlatformInstance getPlatform() {
return platform; return platform;
} }
public PlatformUserAuth getPlatformUserAuth() {
return platformUserAuth;
}
public PlatformDeviceMapping getDeviceMapping() { public PlatformDeviceMapping getDeviceMapping() {
return deviceMapping; return deviceMapping;
} }

View File

@ -13,11 +13,8 @@ import com.fastbee.iot.domain.Device;
import com.fastbee.iot.mapper.DeviceMapper; import com.fastbee.iot.mapper.DeviceMapper;
import com.fastbee.iot.mapper.SubGatewayMapper; import com.fastbee.iot.mapper.SubGatewayMapper;
import com.fastbee.iot.platform.domain.*; import com.fastbee.iot.platform.domain.*;
import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper; import com.fastbee.iot.platform.mapper.*;
import com.fastbee.iot.platform.mapper.PlatformDeviceSubMappingMapper; import com.fastbee.iot.platform.model.json.*;
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.service.IPlatformPushLogService; import com.fastbee.iot.platform.service.IPlatformPushLogService;
import com.dtflys.forest.http.ForestRequestType; import com.dtflys.forest.http.ForestRequestType;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -231,67 +228,91 @@ public class PlatformFlowExecutor {
* 解析步骤入参 * 解析步骤入参
* source 枚举: fixed / auth / extra / context / device / thingsModel / ts * source 枚举: fixed / auth / extra / context / device / thingsModel / ts
*/ */
@SuppressWarnings("unchecked")
private Map<String, Object> resolveParams(PlatformInterface iface, PlatformFlowStep step, FlowContext ctx) { private Map<String, Object> resolveParams(PlatformInterface iface, PlatformFlowStep step, FlowContext ctx) {
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();
if (!StringUtils.hasText(iface.getRequestSchema())) { List<RequestSchemaField> schema = iface.getRequestSchemaParsed();
if (schema == null || schema.isEmpty()) {
return result; return result;
} }
// 合并接口默认 schema 和步骤覆盖 mapping // 合并接口默认 schema 和步骤覆盖 mapping
JSONArray schema = JSON.parseArray(iface.getRequestSchema()); Map<String, ParamOverrideConfig> overrides = step.getParamMappingParsed();
Map<String, Object> overrides = StringUtils.hasText(step.getParamMapping()) if (overrides == null) {
? JSON.parseObject(step.getParamMapping(), Map.class) overrides = Collections.emptyMap();
: Collections.emptyMap(); }
JSONObject authConfig = StringUtils.hasText(ctx.getPlatform().getAuthConfig()) // 获取 userAuth authConfig
? JSON.parseObject(ctx.getPlatform().getAuthConfig()) : new JSONObject(); JSONObject authConfig = new JSONObject();
JSONObject extraConfig = StringUtils.hasText(ctx.getPlatform().getExtraConfig()) if (ctx.getPlatformUserAuth() != null) {
? JSON.parseObject(ctx.getPlatform().getExtraConfig()) : new JSONObject(); JSONObject parsed = ctx.getPlatformUserAuth().getAuthConfigParsed();
if (parsed != null) {
authConfig = parsed;
}
}
for (int i = 0; i < schema.size(); i++) { // 获取 userAuth extraConfig
JSONObject field = schema.getJSONObject(i); JSONObject extraConfig = new JSONObject();
String name = field.getString("name"); 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)) { if (overrides.containsKey(name)) {
Object override = overrides.get(name); ParamOverrideConfig override = overrides.get(name);
if (override instanceof Map) { if (override != null) {
Map<String, Object> ov = (Map<String, Object>) override; String src = override.getSource() != null ? override.getSource() : field.getSource();
String src = (String) ov.get("source"); String type = override.getType() != null ? override.getType() : field.getType();
String type = ov.get("type") != null ? String.valueOf(ov.get("type")) : field.getString("type"); result.put(name, normalizeValueByType(type,
result.put(name, normalizeValueByType(type, resolveValue(src, ov, authConfig, extraConfig, ctx))); resolveValue(src, mergeOverride(field, override), authConfig, extraConfig, ctx)));
} else { } else {
result.put(name, normalizeValueByType(field.getString("type"), override)); result.put(name, normalizeValueByType(field.getType(), null));
} }
continue; continue;
} }
String source = field.getString("source"); result.put(name, normalizeValueByType(field.getType(),
result.put(name, normalizeValueByType(field.getString("type"), resolveValue(source, field, authConfig, extraConfig, ctx))); resolveValue(field.getSource(), field, authConfig, extraConfig, ctx)));
} }
return result; 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) { JSONObject extraConfig, FlowContext ctx) {
if (source == null) return null; if (source == null) return null;
JSONObject fd = fieldDef instanceof JSONObject ? (JSONObject) fieldDef
: JSON.parseObject(JSON.toJSONString(fieldDef));
switch (source) { switch (source) {
case "fixed": case "fixed":
return fd.get("value"); return fieldDef.getValue();
case "uuid": case "uuid":
return newGeneratedId(); return newGeneratedId();
case "auth": case "auth":
return authConfig.get(fd.getString("field")); return authConfig.get(fieldDef.getField());
case "extra": case "extra":
return extraConfig.get(fd.getString("field")); return extraConfig.get(fieldDef.getField());
case "context": case "context":
return ctx.getFromContext(fd.getString("contextKey")); return ctx.getFromContext(fieldDef.getContextKey());
case "device": case "device":
if (ctx.getDeviceMapping() == null) return null; 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_device_id".equals(df)) return ctx.getDeviceMapping().getPlatformDeviceId();
if ("platform_point_id".equals(df)) return ctx.getDeviceMapping().getPlatformPointId(); if ("platform_point_id".equals(df)) return ctx.getDeviceMapping().getPlatformPointId();
// 支持从设备绑定表读取项目信息 // 支持从设备绑定表读取项目信息
@ -303,7 +324,7 @@ public class PlatformFlowExecutor {
if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber())) { if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber())) {
return 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)) { if ("device_name".equals(df)) {
return resolveDeviceName(ctx); return resolveDeviceName(ctx);
@ -316,11 +337,11 @@ public class PlatformFlowExecutor {
} }
return null; return null;
case "thingsModel": case "thingsModel":
return resolveThingsModelValue(ctx, fd); return resolveThingsModelValue(ctx, fieldDef);
case "ts": case "ts":
return resolveThingsModelTs(ctx, fd); return resolveThingsModelTs(ctx, fieldDef);
case "onboard_config": case "onboard_config":
return resolveOnboardConfigValue(ctx, fd); return resolveOnboardConfigValue(ctx, fieldDef);
case "subDeviceSensorIds": case "subDeviceSensorIds":
return collectSubDeviceSensorIds(ctx); return collectSubDeviceSensorIds(ctx);
case "cascadedSubDeviceSensorIds": case "cascadedSubDeviceSensorIds":
@ -334,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()); JSONObject payload = parsePayloadToObject(ctx.getPayload());
if (payload == null) { if (payload == null) {
return null; return null;
@ -343,7 +364,7 @@ public class PlatformFlowExecutor {
return StringUtils.hasText(identifier) ? payload.get(identifier) : null; 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()); JSONObject payload = parsePayloadToObject(ctx.getPayload());
if (payload == null) { if (payload == null) {
return null; return null;
@ -359,13 +380,13 @@ public class PlatformFlowExecutor {
return ts != null ? ts : payload.get("timestamp"); return ts != null ? ts : payload.get("timestamp");
} }
private String resolveThingsModelIdentifier(JSONObject fieldDef) { private String resolveThingsModelIdentifier(RequestSchemaField fieldDef) {
String identifier = fieldDef.getString("identifier"); String identifier = fieldDef.getIdentifier();
if (StringUtils.hasText(identifier)) { if (StringUtils.hasText(identifier)) {
return identifier; return identifier;
} }
String field = fieldDef.getString("field"); String field = fieldDef.getField();
return StringUtils.hasText(field) ? field : fieldDef.getString("contextKey"); return StringUtils.hasText(field) ? field : fieldDef.getContextKey();
} }
private JSONObject parsePayloadToObject(String payload) { private JSONObject parsePayloadToObject(String payload) {
@ -406,15 +427,18 @@ public class PlatformFlowExecutor {
return null; return null;
} }
private Object resolveOnboardConfigValue(FlowContext ctx, JSONObject fieldDef) { private Object resolveOnboardConfigValue(FlowContext ctx, RequestSchemaField fieldDef) {
String name = fieldDef.getString("name"); String name = fieldDef.getName();
List<PlatformDeviceSubMapping> monitorSubMappings = ctx.getMonitorSubMappings(); List<PlatformDeviceSubMapping> monitorSubMappings = ctx.getMonitorSubMappings();
if (ctx.getCurrentMonitorIndex() != null && CollectionUtils.isNotEmpty(monitorSubMappings)) { if (ctx.getCurrentMonitorIndex() != null && CollectionUtils.isNotEmpty(monitorSubMappings)) {
int index = ctx.getCurrentMonitorIndex(); int index = ctx.getCurrentMonitorIndex();
PlatformDeviceSubMapping platformDeviceSubMapping = monitorSubMappings.get(index); PlatformDeviceSubMapping platformDeviceSubMapping = monitorSubMappings.get(index);
JSONObject authConfig = StringUtils.hasText(platformDeviceSubMapping.getOnboardConfig()) OnboardConfig config = platformDeviceSubMapping.getOnboardConfigParsed();
? JSON.parseObject(platformDeviceSubMapping.getOnboardConfig()) : new JSONObject(); if (config == null) {
String value = authConfig.getString(name); throw new RuntimeException("入网失败,子设备" + platformDeviceSubMapping.getSubDeviceName() + "未配置值:" + name);
}
// 从 OnboardConfig 按字段名反射获取值(支持 sensorId/sensorDepth/refSerialNumber/refSensorId)
String value = resolveOnboardConfigField(config, name);
if (value == null) { if (value == null) {
throw new RuntimeException("入网失败,子设备" + platformDeviceSubMapping.getSubDeviceName() + "未配置值:" + name); throw new RuntimeException("入网失败,子设备" + platformDeviceSubMapping.getSubDeviceName() + "未配置值:" + name);
} }
@ -493,21 +517,24 @@ public class PlatformFlowExecutor {
private Set<String> resolveGeneratedFieldNames(PlatformInterface iface, PlatformFlowStep step) { private Set<String> resolveGeneratedFieldNames(PlatformInterface iface, PlatformFlowStep step) {
LinkedHashMap<String, String> sources = new LinkedHashMap<>(); LinkedHashMap<String, String> sources = new LinkedHashMap<>();
try { try {
if (iface != null && StringUtils.hasText(iface.getRequestSchema())) { if (iface != null) {
JSONArray schema = JSON.parseArray(iface.getRequestSchema()); List<RequestSchemaField> schema = iface.getRequestSchemaParsed();
for (int i = 0; i < schema.size(); i++) { if (schema != null) {
JSONObject field = schema.getJSONObject(i); for (RequestSchemaField field : schema) {
if (field != null && StringUtils.hasText(field.getString("name"))) { if (field != null && StringUtils.hasText(field.getName())) {
sources.put(field.getString("name"), field.getString("source")); sources.put(field.getName(), field.getSource());
}
} }
} }
} }
if (step != null && StringUtils.hasText(step.getParamMapping())) { if (step != null) {
JSONObject overrides = JSON.parseObject(step.getParamMapping()); Map<String, ParamOverrideConfig> overrides = step.getParamMappingParsed();
for (String fieldName : overrides.keySet()) { if (overrides != null) {
JSONObject override = overrides.getJSONObject(fieldName); for (Map.Entry<String, ParamOverrideConfig> entry : overrides.entrySet()) {
if (override != null && StringUtils.hasText(override.getString("source"))) { ParamOverrideConfig override = entry.getValue();
sources.put(fieldName, override.getString("source")); if (override != null && StringUtils.hasText(override.getSource())) {
sources.put(entry.getKey(), override.getSource());
}
} }
} }
} }
@ -524,10 +551,8 @@ public class PlatformFlowExecutor {
private List<String> collectSubDeviceSensorIds(FlowContext ctx) { private List<String> collectSubDeviceSensorIds(FlowContext ctx) {
if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber()) if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber())
&& StringUtils.hasText(ctx.getCurrentReferenceSerialNumber())) { && StringUtils.hasText(ctx.getCurrentReferenceSerialNumber())) {
String refSensorId = resolveOnboardSensorId( String refSensorId = resolveOnboardSensorIdByCtx(ctx, true);
ctx.getReferenceSubMappingOnboardConfig(), ctx.getCurrentReferenceSerialNumber()); String monitorSensorId = resolveOnboardSensorIdByCtx(ctx, false);
String monitorSensorId = resolveOnboardSensorId(
ctx.getSubMappingOnboardConfig(), ctx.getCurrentMonitorSerialNumber());
List<String> ids = new ArrayList<>(); List<String> ids = new ArrayList<>();
if (StringUtils.hasText(refSensorId)) ids.add(refSensorId); if (StringUtils.hasText(refSensorId)) ids.add(refSensorId);
if (StringUtils.hasText(monitorSensorId)) ids.add(monitorSensorId); if (StringUtils.hasText(monitorSensorId)) ids.add(monitorSensorId);
@ -537,16 +562,14 @@ public class PlatformFlowExecutor {
} }
// 网关入网时,从子表 onboard_config 获取 sensorId // 网关入网时,从子表 onboard_config 获取 sensorId
if (StringUtils.hasText(ctx.getSubMappingOnboardConfig())) { OnboardConfig currentConfig = resolveCurrentOnboardConfig(ctx);
try { if (currentConfig != null) {
JSONObject config = JSON.parseObject(ctx.getSubMappingOnboardConfig()); List<String> ids = new ArrayList<>();
List<String> ids = new ArrayList<>(); String sensorId = currentConfig.getSensorId();
String sensorId = config.getString("sensorId"); String refSensorId = currentConfig.getRefSensorId();
String refSensorId = config.getString("refSensorId"); if (StringUtils.hasText(refSensorId)) ids.add(refSensorId);
if (StringUtils.hasText(refSensorId)) ids.add(refSensorId); if (StringUtils.hasText(sensorId)) ids.add(sensorId);
if (StringUtils.hasText(sensorId)) ids.add(sensorId); if (!ids.isEmpty()) return ids;
if (!ids.isEmpty()) return ids;
} catch (Exception ignored) {}
} }
// 回退到从 extra_mapping 解析(直连设备等场景) // 回退到从 extra_mapping 解析(直连设备等场景)
return collectSubDeviceSensorBindings(ctx.getDeviceMapping()).stream() return collectSubDeviceSensorBindings(ctx.getDeviceMapping()).stream()
@ -560,7 +583,13 @@ public class PlatformFlowExecutor {
List<PlatformDeviceSubMapping> suffix = collectCascadedSubDeviceSuffix(ctx); List<PlatformDeviceSubMapping> suffix = collectCascadedSubDeviceSuffix(ctx);
Collections.reverse(suffix); Collections.reverse(suffix);
List<String> sensorIds = suffix.stream() List<String> sensorIds = suffix.stream()
.map(sub -> resolveOnboardSensorId(sub.getOnboardConfig(), sub.getSubSerialNumber())) .map(sub -> {
OnboardConfig cfg = sub.getOnboardConfigParsed();
if (cfg != null && StringUtils.hasText(cfg.getSensorId())) {
return cfg.getSensorId();
}
return normalizeText(sub.getSubSerialNumber());
})
.collect(Collectors.toList()); .collect(Collectors.toList());
if (sensorIds.stream().anyMatch(id -> !StringUtils.hasText(id))) { if (sensorIds.stream().anyMatch(id -> !StringUtils.hasText(id))) {
throw new RuntimeException("深部位移子设备缺少 sensorId"); throw new RuntimeException("深部位移子设备缺少 sensorId");
@ -627,14 +656,14 @@ public class PlatformFlowExecutor {
} }
private Object readSensorDepth(PlatformDeviceSubMapping mapping) { private Object readSensorDepth(PlatformDeviceSubMapping mapping) {
if (mapping == null || !StringUtils.hasText(mapping.getOnboardConfig())) { if (mapping == null) {
return null; return null;
} }
try { OnboardConfig config = mapping.getOnboardConfigParsed();
return JSON.parseObject(mapping.getOnboardConfig()).get("sensorDepth"); if (config == null) {
} catch (Exception e) { return null;
throw new RuntimeException("子设备 " + mapping.getSubSerialNumber() + " 入网参数不是有效JSON");
} }
return config.getSensorDepth();
} }
/** /**
@ -643,12 +672,13 @@ public class PlatformFlowExecutor {
*/ */
private List<List<String>> collectReferenceSubDeviceSensorIdPairs(FlowContext ctx) { private List<List<String>> collectReferenceSubDeviceSensorIdPairs(FlowContext ctx) {
if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber())) { if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber())) {
String sensorId = resolveOnboardSensorId( String sensorId = resolveOnboardSensorIdByCtx(ctx, false);
ctx.getSubMappingOnboardConfig(), ctx.getCurrentMonitorSerialNumber()); String refSensorId = resolveOnboardSensorIdByCtx(ctx, true);
String refSensorId = resolveOnboardSensorId(
ctx.getReferenceSubMappingOnboardConfig(), ctx.getCurrentReferenceSerialNumber());
if (!StringUtils.hasText(refSensorId)) { 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 (StringUtils.hasText(sensorId) && StringUtils.hasText(refSensorId)) {
if (sameText(refSensorId, sensorId)) { if (sameText(refSensorId, sensorId)) {
@ -665,17 +695,15 @@ public class PlatformFlowExecutor {
} }
// 网关入网:onboardConfig 已指定当前监测点 // 网关入网:onboardConfig 已指定当前监测点
if (StringUtils.hasText(ctx.getSubMappingOnboardConfig())) { OnboardConfig currentConfig = resolveCurrentOnboardConfig(ctx);
try { if (currentConfig != null) {
JSONObject config = JSON.parseObject(ctx.getSubMappingOnboardConfig()); String sensorId = currentConfig.getSensorId();
String sensorId = config.getString("sensorId"); String refSensorId = currentConfig.getRefSensorId();
String refSensorId = config.getString("refSensorId"); if (StringUtils.hasText(sensorId) && StringUtils.hasText(refSensorId)) {
if (StringUtils.hasText(sensorId) && StringUtils.hasText(refSensorId)) { List<List<String>> pairs = new ArrayList<>();
List<List<String>> pairs = new ArrayList<>(); pairs.add(Arrays.asList(refSensorId, sensorId));
pairs.add(Arrays.asList(refSensorId, sensorId)); return pairs;
return pairs; }
}
} catch (Exception ignored) {}
} }
// 回退:从 extra_mapping 解析(兼容直连设备场景) // 回退:从 extra_mapping 解析(兼容直连设备场景)
PlatformDeviceMapping mapping = ctx.getDeviceMapping(); PlatformDeviceMapping mapping = ctx.getDeviceMapping();
@ -704,20 +732,52 @@ public class PlatformFlowExecutor {
return pairs; return pairs;
} }
private String resolveOnboardSensorId(String onboardConfig, String fallbackSerialNumber) { /** 获取当前监测点子设备的 OnboardConfig POJO */
String sensorId = resolveOnboardConfigText(onboardConfig, "sensorId"); private OnboardConfig resolveCurrentOnboardConfig(FlowContext ctx) {
return StringUtils.hasText(sensorId) ? sensorId : normalizeText(fallbackSerialNumber); 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)) { * 解析 sensorId:优先从子映射 OnboardConfig 获取,否则返回序列号
return null; * @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 { return normalizeText(fallbackSn);
JSONObject config = JSON.parseObject(onboardConfig); }
return normalizeText(config.getString(field));
} catch (Exception ignored) { /** 从 OnboardConfig POJO 按字段名取值 */
return null; 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;
} }
} }
@ -744,28 +804,35 @@ public class PlatformFlowExecutor {
private String appendConfiguredSubDeviceBindings(LinkedHashMap<String, SubDeviceSensorBinding> bindings, private String appendConfiguredSubDeviceBindings(LinkedHashMap<String, SubDeviceSensorBinding> bindings,
PlatformDeviceMapping mapping) { PlatformDeviceMapping mapping) {
if (!StringUtils.hasText(mapping.getExtraMapping())) { ExtraMappingConfig extra = mapping.getExtraMappingParsed();
if (extra == null) {
return null; return null;
} }
try { try {
JSONObject extra = JSON.parseObject(mapping.getExtraMapping());
String referenceSerialNumber = null; String referenceSerialNumber = null;
JSONArray arrayFields = extra.getJSONArray("arrayFields"); List<ExtraMappingConfig.ArrayFieldConfig> arrayFields = extra.getArrayFields();
if (arrayFields != null) { if (arrayFields != null) {
for (int i = 0; i < arrayFields.size(); i++) { for (ExtraMappingConfig.ArrayFieldConfig arrayField : arrayFields) {
JSONObject arrayField = arrayFields.getJSONObject(i);
referenceSerialNumber = appendConfiguredMappingRows(bindings, referenceSerialNumber = appendConfiguredMappingRows(bindings,
arrayField == null ? null : arrayField.getJSONArray("subDeviceMappings"), arrayField == null ? null : arrayField.getSubDeviceMappings(),
referenceSerialNumber); referenceSerialNumber);
} }
} }
JSONObject staticLevel = extra.getJSONObject("staticLevel"); ExtraMappingConfig.StaticLevelConfig staticLevel = extra.getStaticLevel();
if (staticLevel != null) { if (staticLevel != null) {
referenceSerialNumber = normalizeText(staticLevel.getString("referenceSerialNumber")); referenceSerialNumber = normalizeText(staticLevel.getReferenceSerialNumber());
referenceSerialNumber = appendConfiguredMappingRows( List<ExtraMappingConfig.SensorIdMapping> sensorIdMappings = staticLevel.getSensorIdMappings();
bindings, staticLevel.getJSONArray("sensorIdMappings"), referenceSerialNumber); 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; return referenceSerialNumber;
} catch (Exception e) { } catch (Exception e) {
@ -776,24 +843,20 @@ public class PlatformFlowExecutor {
} }
private String appendConfiguredMappingRows(LinkedHashMap<String, SubDeviceSensorBinding> bindings, private String appendConfiguredMappingRows(LinkedHashMap<String, SubDeviceSensorBinding> bindings,
JSONArray mappings, String referenceSerialNumber) { List<ExtraMappingConfig.SubDeviceMapping> mappings,
String referenceSerialNumber) {
if (mappings == null || mappings.isEmpty()) { if (mappings == null || mappings.isEmpty()) {
return referenceSerialNumber; return referenceSerialNumber;
} }
String ref = referenceSerialNumber; String ref = referenceSerialNumber;
for (int i = 0; i < mappings.size(); i++) { for (ExtraMappingConfig.SubDeviceMapping item : mappings) {
JSONObject item = mappings.getJSONObject(i); if (item == null) continue;
if (item == null) { String serialNumber = normalizeText(item.getSerialNumber());
continue; if (!StringUtils.hasText(serialNumber)) continue;
} String sensorId = normalizeText(item.getSensorId());
String serialNumber = normalizeText(item.getString("serialNumber"));
if (!StringUtils.hasText(serialNumber)) {
continue;
}
String sensorId = normalizeText(item.getString("sensorId"));
upsertSubDeviceBinding(bindings, serialNumber, upsertSubDeviceBinding(bindings, serialNumber,
StringUtils.hasText(sensorId) ? sensorId : serialNumber); StringUtils.hasText(sensorId) ? sensorId : serialNumber);
if (Boolean.TRUE.equals(item.getBoolean("isReference"))) { if (Boolean.TRUE.equals(item.getIsReference())) {
ref = serialNumber; ref = serialNumber;
} }
} }
@ -918,13 +981,7 @@ public class PlatformFlowExecutor {
/** 移除已替换到路径中的变量,避免重复放入 body */ /** 移除已替换到路径中的变量,避免重复放入 body */
private void removePathVars(Map<String, Object> params, String path) { private void removePathVars(Map<String, Object> params, String path) {
Iterator<Map.Entry<String, Object>> it = params.entrySet().iterator(); params.entrySet().removeIf(e -> path.contains("{" + e.getKey() + "}"));
while (it.hasNext()) {
Map.Entry<String, Object> e = it.next();
if (path.contains("{" + e.getKey() + "}")) {
it.remove();
}
}
} }
private Object buildResponseData(PlatformInterface iface, String responseBody) { private Object buildResponseData(PlatformInterface iface, String responseBody) {
@ -966,19 +1023,18 @@ public class PlatformFlowExecutor {
private JSONObject extractResponseBySchema(PlatformInterface iface, Object parsedResponse) { private JSONObject extractResponseBySchema(PlatformInterface iface, Object parsedResponse) {
JSONObject extracted = new JSONObject(); 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; return extracted;
} }
try { try {
JSONArray schema = JSON.parseArray(iface.getResponseSchema()); for (ResponseSchemaField field : schema) {
for (int i = 0; i < schema.size(); i++) { if (field == null || !StringUtils.hasText(field.getName())) {
JSONObject field = schema.getJSONObject(i);
if (field == null || !StringUtils.hasText(field.getString("name"))) {
continue; continue;
} }
Object value = readPathValue(parsedResponse, field.getString("path")); Object value = readPathValue(parsedResponse, field.getPath());
if (value != null) { if (value != null) {
extracted.put(field.getString("name"), value); extracted.put(field.getName(), value);
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -1086,84 +1142,99 @@ public class PlatformFlowExecutor {
return value; 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) { private void persistResultSave(PlatformFlowStep step, Object responseData, FlowContext context) {
try { try {
JSONArray saves = JSON.parseArray(step.getResultSave()); List<ResultSaveField> saves = step.getResultSaveParsed();
if (saves == null || saves.isEmpty()) return;
JSONObject response = toJsonObject(responseData); JSONObject response = toJsonObject(responseData);
if (response == null) return; if (response == null) return;
PlatformInstance platform = context.getPlatform();
JSONObject extra = StringUtils.hasText(platform.getExtraConfig()) PlatformUserAuth userAuth = context.getPlatformUserAuth();
? JSON.parseObject(platform.getExtraConfig()) : new JSONObject();
JSONObject extra = userAuth.getExtraConfigParsed();
if (extra == null) extra = new JSONObject();
boolean extraChanged = false; boolean extraChanged = false;
boolean platformFieldChanged = false; boolean userAuthFieldChanged = false;
// 平台用户授权信息(token、userId)
PlatformUserAuth userAuthUpdate = new PlatformUserAuth();
PlatformDeviceMapping mapping = context.getDeviceMapping(); PlatformDeviceMapping mapping = context.getDeviceMapping();
boolean mappingChanged = false; boolean mappingChanged = false;
// 保存平台映射关系(测点ID、设备ID)
PlatformDeviceMapping mappingUpdate = new PlatformDeviceMapping();
for (int i = 0; i < saves.size(); i++) { for (ResultSaveField save : saves) {
JSONObject save = saves.getJSONObject(i); String field = save.getField();
String field = save.getString("field"); String saveTo = save.getSaveTo();
String saveTo = save.getString("saveTo");
Object value = response.get(field); Object value = response.get(field);
if (value == null || saveTo == null) continue; if (value == null || saveTo == null) continue;
if (saveTo.startsWith("extra_config.")) { if (saveTo.startsWith("platform_user_auth.")) {
String key = saveTo.substring("extra_config.".length()); // 新增:保存到用户认证表
extra.put(key, value); String col = saveTo.substring("platform_user_auth.".length());
extraChanged = true; switch (col) {
} else if (saveTo.startsWith("platform_instance.")) { case "access_token":
String col = saveTo.substring("platform_instance.".length()); userAuthUpdate.setAccessToken(value.toString());
if ("access_token".equals(col)) { userAuthFieldChanged = true;
platform.setAccessToken(value.toString()); break;
platformFieldChanged = true; case "token_expires_at":
} else if ("token_expires_at".equals(col)) { try {
try { long seconds = Long.parseLong(value.toString());
long seconds = Long.parseLong(value.toString()); userAuthUpdate.setTokenExpiresAt(new Date(System.currentTimeMillis() + seconds * 1000));
platform.setTokenExpiresAt(new Date(System.currentTimeMillis() + seconds * 1000)); userAuthFieldChanged = true;
platformFieldChanged = true; } catch (Exception ignored) {
} 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) { } else if (saveTo.startsWith("device_mapping.") && mapping != null) {
String col = saveTo.substring("device_mapping.".length()); String col = saveTo.substring("device_mapping.".length());
if ("platform_device_id".equals(col)) { if ("platform_device_id".equals(col)) {
mapping.setPlatformDeviceId(value.toString()); mappingUpdate.setPlatformDeviceId(value.toString());
mappingChanged = true; mappingChanged = true;
} else if ("platform_point_id".equals(col)) { } else if ("platform_point_id".equals(col)) {
mapping.setPlatformPointId(value.toString()); mappingUpdate.setPlatformPointId(value.toString());
mappingChanged = true; 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 字段,不覆盖其他字段 if (userAuthFieldChanged || extraChanged) {
platform.setExtraConfig(extra.toJSONString()); userAuthUpdate.setId(userAuth.getId());
PlatformInstance update = new PlatformInstance(); PlatformUserAuthMapper userAuthMapper = SpringUtils.getBean(PlatformUserAuthMapper.class);
update.setId(platform.getId()); userAuthMapper.updateById(userAuthUpdate);
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 (mappingChanged) { if (mappingChanged) {
PlatformDeviceMapping update = new PlatformDeviceMapping(); mappingUpdate.setId(mapping.getId());
update.setId(mapping.getId());
update.setPlatformDeviceId(mapping.getPlatformDeviceId());
update.setPlatformPointId(mapping.getPlatformPointId());
// 直接用 SpringUtils 获取 mapper,避免循环依赖注入 // 直接用 SpringUtils 获取 mapper,避免循环依赖注入
PlatformDeviceMappingMapper mappingMapper = SpringUtils.getBean(PlatformDeviceMappingMapper.class); PlatformDeviceMappingMapper mappingMapper = SpringUtils.getBean(PlatformDeviceMappingMapper.class);
mappingMapper.updateById(update); mappingMapper.updateById(mappingUpdate);
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[PlatformFlow] persistResultSave 失败: {}", e.getMessage()); log.warn("[PlatformFlow] persistResultSave 失败: {}", e.getMessage());
throw new RuntimeException("执行流程❌,保存数据失败: " + e.getMessage());
} }
} }
@ -1196,7 +1267,7 @@ public class PlatformFlowExecutor {
* 解析当前步骤应使用的 Token: * 解析当前步骤应使用的 Token:
* - flow_type=3(Token刷新):不注入,避免循环 * - flow_type=3(Token刷新):不注入,避免循环
* - flow_type=1(平台初始化)step1:认证步骤本身不注入 * - flow_type=1(平台初始化)step1:认证步骤本身不注入
* - 其他情况:从 platform_instance 取(step1 执行后已通过 persistResultSave 写入),过期则自动刷新 * - 其他情况:从 userAuth 获取,过期则自动刷新
*/ */
private String resolveToken(PlatformFlow flow, PlatformFlowStep step, FlowContext ctx) { private String resolveToken(PlatformFlow flow, PlatformFlowStep step, FlowContext ctx) {
if (flow.getFlowType() == 3) { if (flow.getFlowType() == 3) {
@ -1206,42 +1277,50 @@ public class PlatformFlowExecutor {
// 初始化流程第1步是认证接口本身,不注入 token // 初始化流程第1步是认证接口本身,不注入 token
return null; return null;
} }
// 其余情况(含初始化流程 step2+):从 platform 对象取 return ensureToken(ctx);
// step1 执行后 persistResultSave 已将 token 写入 platform 内存对象和 DB
return ensureToken(ctx.getPlatform());
} }
/** 确保 Token 有效,过期则刷新 */ /**
public 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(); Date now = new Date();
// 提前 5 分钟视为过期 // 提前 5 分钟视为过期
boolean expired = platform.getTokenExpiresAt() == null boolean expired = userAuth.getTokenExpiresAt() == null
|| platform.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L; || userAuth.getTokenExpiresAt().getTime() - now.getTime() < 5 * 60 * 1000L;
if (!StringUtils.hasText(platform.getAccessToken()) || expired) { if (!StringUtils.hasText(userAuth.getAccessToken()) || expired) {
refreshToken(platform); refreshToken(ctx);
} }
return platform.getAccessToken(); return userAuth.getAccessToken();
} }
/** 执行 flow_type=3 的 Token 刷新流程 */ /**
private void refreshToken(PlatformInstance platform) { * 执行 flow_type=3 的 Token 刷新流程
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PlatformFlow> wrapper = */
Wrappers.<PlatformFlow>lambdaQuery() private void refreshToken(FlowContext ctx) {
.eq(PlatformFlow::getPlatformId, platform.getId()) PlatformUserAuth userAuth = ctx.getPlatformUserAuth();
.eq(PlatformFlow::getFlowType, 3) PlatformFlowMapper flowMapper = SpringUtils.getBean(PlatformFlowMapper.class);
.eq(PlatformFlow::getEnable, "1"); PlatformFlow tokenFlow = flowMapper.selectOne(Wrappers.<PlatformFlow>lambdaQuery()
// 通过 ApplicationContext 获取 PlatformFlowMapper,避免循环依赖 .eq(PlatformFlow::getPlatformId, userAuth.getPlatformId())
// 此处直接用 instanceMapper 所在的 Spring 容器获取 .eq(PlatformFlow::getFlowType, 3)
// 实际注入见 PlatformFlowExecutor 字段 .eq(PlatformFlow::getEnable, "1")
com.fastbee.iot.platform.mapper.PlatformFlowMapper flowMapper = .last("LIMIT 1"));
com.fastbee.common.utils.spring.SpringUtils.getBean(
com.fastbee.iot.platform.mapper.PlatformFlowMapper.class);
PlatformFlow tokenFlow = flowMapper.selectOne(wrapper);
if (tokenFlow == null) { if (tokenFlow == null) {
log.warn("[PlatformFlow] 平台 {} 未配置 Token 刷新流程(flow_type=3)", platform.getId()); log.warn("[PlatformFlow] 平台 {} 未配置 Token 刷新流程(flow_type=3)", userAuth.getPlatformId());
return; return;
} }
FlowContext ctx = new FlowContext(platform);
// 获取平台实例
PlatformInstance platform = ctx.getPlatform();
if (platform == null) {
log.warn("[PlatformFlow] 平台不存在: {}", userAuth.getPlatformId());
return;
}
execute(tokenFlow, ctx); execute(tokenFlow, ctx);
// 从上下文中提取 token(约定 Token 刷新流程最后一步 resultKey="tokenStep") // 从上下文中提取 token(约定 Token 刷新流程最后一步 resultKey="tokenStep")
@ -1252,12 +1331,13 @@ public class PlatformFlowExecutor {
String accessToken = (String) tokenResp.get("access_token"); String accessToken = (String) tokenResp.get("access_token");
Object expiresIn = tokenResp.get("expires_in"); Object expiresIn = tokenResp.get("expires_in");
if (StringUtils.hasText(accessToken)) { if (StringUtils.hasText(accessToken)) {
platform.setAccessToken(accessToken); userAuth.setAccessToken(accessToken);
if (expiresIn != null) { if (expiresIn != null) {
long seconds = Long.parseLong(expiresIn.toString()); 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);
} }
} }
} }

View File

@ -8,14 +8,12 @@ import com.fastbee.iot.cache.ITSLCache;
import com.fastbee.iot.domain.Device; import com.fastbee.iot.domain.Device;
import com.fastbee.iot.model.ThingsModels.ValueItem; import com.fastbee.iot.model.ThingsModels.ValueItem;
import com.fastbee.iot.platform.adapter.dcp.DcpPlatformAdapter; import com.fastbee.iot.platform.adapter.dcp.DcpPlatformAdapter;
import com.fastbee.iot.platform.domain.PlatformDeviceMapping; import com.fastbee.iot.platform.domain.*;
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.mapper.PlatformInterfaceMapper; import com.fastbee.iot.platform.mapper.PlatformInterfaceMapper;
import com.fastbee.iot.platform.service.IPlatformDeviceMappingService; import com.fastbee.iot.platform.service.IPlatformDeviceMappingService;
import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService;
import com.fastbee.iot.platform.service.IPlatformInstanceService; import com.fastbee.iot.platform.service.IPlatformInstanceService;
import com.fastbee.iot.platform.service.IPlatformUserAuthService;
import com.fastbee.iot.service.IDeviceService; import com.fastbee.iot.service.IDeviceService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
@ -59,6 +57,8 @@ public class PlatformPushScheduledJob {
private IDeviceService deviceService; private IDeviceService deviceService;
@Autowired @Autowired
private PlatformInterfaceMapper interfaceMapper; private PlatformInterfaceMapper interfaceMapper;
@Autowired
private IPlatformUserAuthService platformUserAuthService;
/** /**
* 每5分钟执行一次,检查并触发到期的推送任务 * 每5分钟执行一次,检查并触发到期的推送任务
@ -128,6 +128,12 @@ public class PlatformPushScheduledJob {
return; return;
} }
PlatformUserAuth platformUserAuth = platformUserAuthService.getById(mapping.getPlatformUserAuthId());
if (platformUserAuth == null || !"1".equals(platformUserAuth.getEnable())) {
log.warn("[PlatformPushJob] 平台已授权用户不存在或已禁用 platformUserAuthId={}", mapping.getPlatformUserAuthId());
return;
}
// 数据库 last_push_time 为 DATETIME(秒精度),此处截断毫秒, // 数据库 last_push_time 为 DATETIME(秒精度),此处截断毫秒,
// 避免 Redis 中带毫秒的时间与库中已截断的时间比较时误判为"数据变化"而重复推送 // 避免 Redis 中带毫秒的时间与库中已截断的时间比较时误判为"数据变化"而重复推送
Date dataTime = truncateToSeconds(resolveLatestDataTime(mapping)); Date dataTime = truncateToSeconds(resolveLatestDataTime(mapping));
@ -145,10 +151,10 @@ public class PlatformPushScheduledJob {
PlatformInterface pushInterface = loadPushInterface(mapping); PlatformInterface pushInterface = loadPushInterface(mapping);
if (pushInterface != null && pushInterface.getIsBatchPush() != null && pushInterface.getIsBatchPush() == 1) { if (pushInterface != null && pushInterface.getIsBatchPush() != null && pushInterface.getIsBatchPush() == 1) {
// 批量推送 // 批量推送
pushBatchGatewayData(platform, mapping); pushBatchGatewayData(platform, platformUserAuth, mapping);
} else { } else {
// 非批量:逐个子设备推送 // 非批量:逐个子设备推送
pushGatewayLatestData(platform, mapping); pushGatewayLatestData(platform, platformUserAuth, mapping);
} }
} else { } else {
// 直连设备 // 直连设备
@ -159,7 +165,7 @@ public class PlatformPushScheduledJob {
return; return;
} }
String payload = buildPayloadFromCache(cacheMap); String payload = buildPayloadFromCache(cacheMap);
dcpAdapter.pushData(platform, mapping, serialNumber, payload); dcpAdapter.pushData(platform, platformUserAuth, mapping, serialNumber, payload);
} }
// lastPushTime 记录最近已推送的数据时间,用于幂等判断 // lastPushTime 记录最近已推送的数据时间,用于幂等判断
@ -172,7 +178,7 @@ public class PlatformPushScheduledJob {
* 取最深层(最后一个)监测点作为触发点,构造模拟 payload, * 取最深层(最后一个)监测点作为触发点,构造模拟 payload,
* 调用 pushGatewayData 进入批量推送路径,推送从浅到深的全量子设备数据。 * 调用 pushGatewayData 进入批量推送路径,推送从浅到深的全量子设备数据。
*/ */
private void pushBatchGatewayData(PlatformInstance platform, PlatformDeviceMapping mapping) { private void pushBatchGatewayData(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping) {
List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId()); List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId());
if (CollectionUtils.isEmpty(subMappings)) { if (CollectionUtils.isEmpty(subMappings)) {
log.warn("[PlatformPushJob] 网关设备 {} 无子设备映射", mapping.getSerialNumber()); log.warn("[PlatformPushJob] 网关设备 {} 无子设备映射", mapping.getSerialNumber());
@ -202,14 +208,14 @@ public class PlatformPushScheduledJob {
} }
String payload = buildPayloadFromCache(cacheMap); 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()); 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()); List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId());
if (CollectionUtils.isEmpty(subMappings)) { if (CollectionUtils.isEmpty(subMappings)) {
log.warn("[PlatformPushJob] 网关设备 {} 无子设备映射", mapping.getSerialNumber()); log.warn("[PlatformPushJob] 网关设备 {} 无子设备映射", mapping.getSerialNumber());
@ -230,7 +236,7 @@ public class PlatformPushScheduledJob {
} }
String payload = buildPayloadFromCache(subCacheMap); String payload = buildPayloadFromCache(subCacheMap);
dcpAdapter.pushGatewayData(platform, mapping, subMapping, subSerialNumber, payload); dcpAdapter.pushGatewayData(platform, userAuth, mapping, subMapping, subSerialNumber, payload);
} catch (Exception e) { } catch (Exception e) {
log.error("[PlatformPushJob] 网关子设备推送失败 subSn={}", subMapping.getSubSerialNumber(), e); log.error("[PlatformPushJob] 网关子设备推送失败 subSn={}", subMapping.getSubSerialNumber(), e);
} }

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -10,21 +10,9 @@ public interface IPlatformFlowService extends IService<PlatformFlow> {
List<PlatformFlow> selectList(PlatformFlow query); List<PlatformFlow> selectList(PlatformFlow query);
/** 查询某平台指定类型的流程 */
PlatformFlow selectByPlatformAndType(Long platformId, Integer flowType);
/** 查询某平台指定类型+传感器类型的流程(设备入网用) */
PlatformFlow selectByPlatformTypeAndSensor(Long platformId, Integer flowType, String sensorType);
/** 查询流程的有序步骤列表 */ /** 查询流程的有序步骤列表 */
List<PlatformFlowStep> selectSteps(Long flowId); List<PlatformFlowStep> selectSteps(Long flowId);
/** 手动触发流程执行 */
void executeManually(Long flowId);
/** 手动触发流程执行(设备入网流程需传 mappingId) */
void executeManually(Long flowId, Long mappingId);
/** 保存流程及步骤(含步骤的新增/更新/删除) */ /** 保存流程及步骤(含步骤的新增/更新/删除) */
void saveWithSteps(PlatformFlow flow, List<PlatformFlowStep> steps); void saveWithSteps(PlatformFlow flow, List<PlatformFlowStep> steps);
} }

View File

@ -3,25 +3,9 @@ package com.fastbee.iot.platform.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.fastbee.iot.platform.domain.PlatformInstance; import com.fastbee.iot.platform.domain.PlatformInstance;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
public interface IPlatformInstanceService extends IService<PlatformInstance> { public interface IPlatformInstanceService extends IService<PlatformInstance> {
List<PlatformInstance> selectList(PlatformInstance query); 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);
/**
* 根据平台ID查询项目列表
*/
List<Map<String, Object>> getProjectsByPlatformId(Long platformId);
} }

View File

@ -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);
}

View File

@ -3,17 +3,16 @@ package com.fastbee.iot.platform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fastbee.iot.platform.domain.PlatformDeviceMapping; import com.fastbee.iot.platform.domain.*;
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.flow.FlowContext; import com.fastbee.iot.platform.flow.FlowContext;
import com.fastbee.iot.platform.flow.PlatformFlowExecutor; import com.fastbee.iot.platform.flow.PlatformFlowExecutor;
import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper; import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper;
import com.fastbee.iot.platform.mapper.PlatformFlowMapper; import com.fastbee.iot.platform.mapper.PlatformFlowMapper;
import com.fastbee.iot.platform.mapper.PlatformInstanceMapper; 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.IPlatformDeviceMappingService;
import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService;
import com.fastbee.iot.platform.service.IPlatformUserAuthService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -40,6 +39,8 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
private PlatformFlowExecutor flowExecutor; private PlatformFlowExecutor flowExecutor;
@Autowired @Autowired
private IPlatformDeviceSubMappingService subMappingService; private IPlatformDeviceSubMappingService subMappingService;
@Autowired
private PlatformUserAuthMapper platformUserAuthMapper;
@Override @Override
public List<PlatformDeviceMapping> selectList(PlatformDeviceMapping query) { public List<PlatformDeviceMapping> selectList(PlatformDeviceMapping query) {
@ -72,15 +73,16 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
throw new RuntimeException("设备绑定关系不存在: " + mappingId); throw new RuntimeException("设备绑定关系不存在: " + mappingId);
} }
if (!StringUtils.hasText(mapping.getPlatformProjectId())) {
throw new RuntimeException("请选择所属项目后再执行入网");
}
PlatformInstance platform = instanceMapper.selectById(mapping.getPlatformId()); PlatformInstance platform = instanceMapper.selectById(mapping.getPlatformId());
if (platform == null) { if (platform == null) {
throw new RuntimeException("平台不存在: " + mapping.getPlatformId()); 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() PlatformFlow onboardFlow = flowMapper.selectOne(Wrappers.<PlatformFlow>lambdaQuery()
.eq(PlatformFlow::getPlatformId, mapping.getPlatformId()) .eq(PlatformFlow::getPlatformId, mapping.getPlatformId())
.eq(PlatformFlow::getFlowType, 2) .eq(PlatformFlow::getFlowType, 2)
@ -92,17 +94,17 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
} }
if (mapping.getDeviceType() != null && mapping.getDeviceType() == 1) { if (mapping.getDeviceType() != null && mapping.getDeviceType() == 1) {
onboardGateway(mapping, platform, onboardFlow); onboardGateway(mapping, platform, platformUserAuth, onboardFlow);
} else { } else {
onboardDirect(mapping, platform, onboardFlow); onboardDirect(mapping, platform, platformUserAuth, onboardFlow);
} }
} }
/** /**
* 直连设备入网:结果写主表 platform_device_id / platform_point_id * 直连设备入网:结果写主表 platform_device_id / platform_point_id
*/ */
private void onboardDirect(PlatformDeviceMapping mapping, PlatformInstance platform, PlatformFlow onboardFlow) { private void onboardDirect(PlatformDeviceMapping mapping, PlatformInstance platform, PlatformUserAuth platformUserAuth, PlatformFlow onboardFlow) {
FlowContext ctx = new FlowContext(platform, mapping, null); FlowContext ctx = new FlowContext(platform, mapping, platformUserAuth);
flowExecutor.execute(onboardFlow, ctx); flowExecutor.execute(onboardFlow, ctx);
// persistResultSave 已在流程执行中回填了主表字段,重新读取 // persistResultSave 已在流程执行中回填了主表字段,重新读取
@ -116,7 +118,7 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
/** /**
* 网关设备入网:遍历子表中 device_role=monitor 且未入网的行,逐行执行入网流程 * 网关设备入网:遍历子表中 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()); List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId());
PlatformDeviceSubMapping reference = resolveGatewayReference(mapping, subMappings); PlatformDeviceSubMapping reference = resolveGatewayReference(mapping, subMappings);
List<PlatformDeviceSubMapping> monitors = subMappings.stream() List<PlatformDeviceSubMapping> monitors = subMappings.stream()
@ -132,12 +134,8 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
for (int i = 0; i < monitors.size(); i++) { for (int i = 0; i < monitors.size(); i++) {
PlatformDeviceSubMapping monitor = monitors.get(i); PlatformDeviceSubMapping monitor = monitors.get(i);
FlowContext ctx = new FlowContext(platform, mapping, null); FlowContext ctx = new FlowContext(platform, platformUserAuth, mapping, monitors, monitor.getSubSerialNumber(),
ctx.setCurrentMonitorSerialNumber(monitor.getSubSerialNumber()); monitor.getSubDeviceName(), i, monitor.getOnboardConfig());
ctx.setCurrentMonitorDeviceName(monitor.getSubDeviceName());
ctx.setCurrentMonitorIndex(i);
ctx.setMonitorSubMappings(monitors);
ctx.setSubMappingOnboardConfig(monitor.getOnboardConfig());
if (reference != null) { if (reference != null) {
ctx.setCurrentReferenceSerialNumber(reference.getSubSerialNumber()); ctx.setCurrentReferenceSerialNumber(reference.getSubSerialNumber());
ctx.setCurrentReferenceDeviceName(reference.getSubDeviceName()); ctx.setCurrentReferenceDeviceName(reference.getSubDeviceName());

View File

@ -3,17 +3,8 @@ package com.fastbee.iot.platform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fastbee.common.utils.spring.SpringUtils; import com.fastbee.iot.platform.domain.*;
import com.fastbee.iot.platform.domain.PlatformDeviceMapping; import com.fastbee.iot.platform.mapper.*;
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.service.IPlatformFlowService; import com.fastbee.iot.platform.service.IPlatformFlowService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -28,10 +19,6 @@ public class PlatformFlowServiceImpl extends ServiceImpl<PlatformFlowMapper, Pla
@Autowired @Autowired
private PlatformFlowStepMapper stepMapper; private PlatformFlowStepMapper stepMapper;
@Autowired
private PlatformInstanceMapper instanceMapper;
@Autowired
private PlatformFlowExecutor flowExecutor;
@Override @Override
public List<PlatformFlow> selectList(PlatformFlow query) { public List<PlatformFlow> selectList(PlatformFlow query) {
@ -43,58 +30,11 @@ public class PlatformFlowServiceImpl extends ServiceImpl<PlatformFlowMapper, Pla
return this.list(wrapper); 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 @Override
public List<PlatformFlowStep> selectSteps(Long flowId) { public List<PlatformFlowStep> selectSteps(Long flowId) {
return stepMapper.selectByFlowId(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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void saveWithSteps(PlatformFlow flow, List<PlatformFlowStep> steps) { public void saveWithSteps(PlatformFlow flow, List<PlatformFlowStep> steps) {

View File

@ -1,41 +1,25 @@
package com.fastbee.iot.platform.service.impl; 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.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dtflys.forest.http.ForestRequest; import com.fastbee.common.enums.StatusEnum;
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.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.PlatformInstanceMapper;
import com.fastbee.iot.platform.mapper.PlatformUserAuthMapper;
import com.fastbee.iot.platform.service.IPlatformInstanceService; import com.fastbee.iot.platform.service.IPlatformInstanceService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service @Service
public class PlatformInstanceServiceImpl extends ServiceImpl<PlatformInstanceMapper, PlatformInstance> public class PlatformInstanceServiceImpl extends ServiceImpl<PlatformInstanceMapper, PlatformInstance>
implements IPlatformInstanceService { implements IPlatformInstanceService {
@Autowired @Autowired
private PlatformFlowMapper flowMapper; private PlatformUserAuthMapper platformUserAuthMapper;
@Autowired
private PlatformFlowExecutor flowExecutor;
@Autowired
private RedisCache redisCache;
@Override @Override
public List<PlatformInstance> selectList(PlatformInstance query) { public List<PlatformInstance> selectList(PlatformInstance query) {
@ -43,114 +27,12 @@ public class PlatformInstanceServiceImpl extends ServiceImpl<PlatformInstanceMap
.eq(StringUtils.hasText(query.getEnable()), PlatformInstance::getEnable, query.getEnable()) .eq(StringUtils.hasText(query.getEnable()), PlatformInstance::getEnable, query.getEnable())
.like(StringUtils.hasText(query.getName()), PlatformInstance::getName, query.getName()) .like(StringUtils.hasText(query.getName()), PlatformInstance::getName, query.getName())
.orderByAsc(PlatformInstance::getId); .orderByAsc(PlatformInstance::getId);
return this.list(wrapper); List<PlatformInstance> resultList = this.list(wrapper);
} if (resultList != null) {
resultList.forEach(instance ->
@Override instance.setInitStatus(platformUserAuthMapper.countEffectivePlatformUser(instance.getId()) > 0 ?
public void initialize(Long platformId) { StatusEnum.SUCCESS.getStatus() : StatusEnum.FAIL.getStatus()));
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);
}
@Override
public List<Map<String, Object>> getProjectsByPlatformId(Long platformId) {
PlatformInstance platform = this.getById(platformId);
if (platform == null) {
throw new RuntimeException("平台不存在: " + platformId);
} }
return resultList;
// 从extra_config读取userId
JSONObject extraConfig = JSON.parseObject(platform.getExtraConfig());
String userId = extraConfig == null ? null : extraConfig.getString("userId");
if (!StringUtils.hasText(userId)) {
throw new RuntimeException("平台未完成初始化,缺少userId。请先执行平台初始化流程。");
}
String key = RedisKeyBuilder.buildDCPProjectInfoKey(userId);
Object cacheObject = redisCache.getCacheObject(key);
if (cacheObject != null) {
return (List<Map<String, Object>>) cacheObject;
}
// 确保 token 有效(过期自动刷新)
String token = flowExecutor.ensureToken(platform);
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());
} }
} }

View File

@ -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());
}
}

View File

@ -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.PlatformDeviceMapping;
import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping; import com.fastbee.iot.platform.domain.PlatformDeviceSubMapping;
import com.fastbee.iot.platform.domain.PlatformInstance; 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.IPlatformDeviceMappingService;
import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService; import com.fastbee.iot.platform.service.IPlatformDeviceSubMappingService;
import com.fastbee.iot.platform.service.IPlatformInstanceService; import com.fastbee.iot.platform.service.IPlatformInstanceService;
import com.fastbee.iot.platform.service.IPlatformUserAuthService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@ -30,6 +32,8 @@ public class DeviceReportTrigger {
@Autowired @Autowired
private IPlatformInstanceService instanceService; private IPlatformInstanceService instanceService;
@Autowired @Autowired
private IPlatformUserAuthService platformUserAuthService;
@Autowired
private DcpPlatformAdapter dcpAdapter; private DcpPlatformAdapter dcpAdapter;
/** /**
@ -58,7 +62,9 @@ public class DeviceReportTrigger {
PlatformInstance platform = instanceService.getById(mapping.getPlatformId()); PlatformInstance platform = instanceService.getById(mapping.getPlatformId());
if (platform == null || !"1".equals(platform.getEnable())) continue; 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) { } catch (Exception e) {
log.error("[DeviceReportTrigger] 推送异常 serialNumber={}", serialNumber, e); log.error("[DeviceReportTrigger] 推送异常 serialNumber={}", serialNumber, e);
@ -66,16 +72,16 @@ public class DeviceReportTrigger {
} }
/** 根据平台 code 路由到对应 Adapter */ /** 根据平台 code 路由到对应 Adapter */
private void dispatchPush(PlatformInstance platform, PlatformDeviceMapping mapping, private void dispatchPush(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
String reportSerialNumber, String payload) { String reportSerialNumber, String payload) {
switch (platform.getCode()) { switch (platform.getCode()) {
case "dcp": case "dcp":
if (mapping.getDeviceType() != null && mapping.getDeviceType() == 1) { if (mapping.getDeviceType() != null && mapping.getDeviceType() == 1) {
// 网关设备:查子表确定该 serialNumber 的角色和 platformDeviceId // 网关设备:查子表确定该 serialNumber 的角色和 platformDeviceId
dispatchGatewayPush(platform, mapping, reportSerialNumber, payload); dispatchGatewayPush(platform, userAuth, mapping, reportSerialNumber, payload);
} else { } else {
// 直连设备 // 直连设备
dcpAdapter.pushData(platform, mapping, reportSerialNumber, payload); dcpAdapter.pushData(platform, userAuth, mapping, reportSerialNumber, payload);
} }
break; break;
default: default:
@ -89,7 +95,7 @@ public class DeviceReportTrigger {
* - 上报者是子表中的 reference → adapter 仅缓存基准数据 * - 上报者是子表中的 reference → adapter 仅缓存基准数据
* - 上报者是网关本身(serial_number 匹配主表) → 判断是否在子表中标记为 reference * - 上报者是网关本身(serial_number 匹配主表) → 判断是否在子表中标记为 reference
*/ */
private void dispatchGatewayPush(PlatformInstance platform, PlatformDeviceMapping mapping, private void dispatchGatewayPush(PlatformInstance platform, PlatformUserAuth userAuth, PlatformDeviceMapping mapping,
String reportSerialNumber, String payload) { String reportSerialNumber, String payload) {
// 查找该序列号在子表中的记录 // 查找该序列号在子表中的记录
List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId()); List<PlatformDeviceSubMapping> subMappings = subMappingService.selectByParentId(mapping.getId());
@ -104,6 +110,6 @@ public class DeviceReportTrigger {
return; return;
} }
dcpAdapter.pushGatewayData(platform, mapping, matchedSub, reportSerialNumber, payload); dcpAdapter.pushGatewayData(platform, userAuth, mapping, matchedSub, reportSerialNumber, payload);
} }
} }

View File

@ -5,6 +5,7 @@
<resultMap type="com.fastbee.iot.platform.domain.PlatformDeviceMapping" id="PlatformDeviceMappingResult"> <resultMap type="com.fastbee.iot.platform.domain.PlatformDeviceMapping" id="PlatformDeviceMappingResult">
<result property="id" column="id"/> <result property="id" column="id"/>
<result property="platformId" column="platform_id"/> <result property="platformId" column="platform_id"/>
<result property="platformUserAuthId" column="platform_user_auth_id"/>
<result property="platformProjectId" column="platform_project_id"/> <result property="platformProjectId" column="platform_project_id"/>
<result property="platformProjectName" column="platform_project_name"/> <result property="platformProjectName" column="platform_project_name"/>
<result property="deviceId" column="device_id"/> <result property="deviceId" column="device_id"/>

View File

@ -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>

View File

@ -120,6 +120,7 @@ CREATE TABLE IF NOT EXISTS `platform_flow_step` (
CREATE TABLE IF NOT EXISTS `platform_device_mapping` ( CREATE TABLE IF NOT EXISTS `platform_device_mapping` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`platform_id` bigint(20) NOT NULL 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_id` varchar(100) NOT NULL COMMENT 'DCP项目ID',
`platform_project_name` varchar(255) DEFAULT NULL COMMENT 'DCP项目名称', `platform_project_name` varchar(255) DEFAULT NULL COMMENT 'DCP项目名称',
`device_id` bigint(20) NOT NULL COMMENT 'FastBee设备ID', `device_id` bigint(20) NOT NULL COMMENT 'FastBee设备ID',
@ -144,7 +145,8 @@ CREATE TABLE IF NOT EXISTS `platform_device_mapping` (
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_serial` (`serial_number`), 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='平台设备绑定关系'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台设备绑定关系';
-- ---------------------------- -- ----------------------------
@ -238,6 +240,31 @@ CREATE TABLE IF NOT EXISTS `platform_push_log` (
-- 注意:platform_id 需在创建 DCP 平台实例后更新,此处用占位符 0 -- 注意:platform_id 需在创建 DCP 平台实例后更新,此处用占位符 0
-- 实际由 DcpPlatformAdapter 在初始化时写入 -- 实际由 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='平台用户认证';
-- 平台对接另外一套方案 -- 平台对接另外一套方案
-- fastbee.iot_platform_api definition -- fastbee.iot_platform_api definition
@ -504,6 +531,15 @@ CREATE TABLE `iot_platform_resource_mapping` (
KEY `idx_external_id` (`tenant_channel_id`,`resource_type`,`external_id`) KEY `idx_external_id` (`tenant_channel_id`,`resource_type`,`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='平台对接-资源映射'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 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`);
-- fastbee.iot_platform_task_binding definition -- fastbee.iot_platform_task_binding definition

View File

@ -15,7 +15,3 @@ export function savePlatformFlow(data) {
export function delPlatformFlow(id) { export function delPlatformFlow(id) {
return request({ url: `/platform/flow/${id}`, method: 'delete' }) return request({ url: `/platform/flow/${id}`, method: 'delete' })
} }
export function executePlatformFlow(id) {
return request({ url: `/platform/flow/${id}/execute`, method: 'post' })
}

View File

@ -19,11 +19,3 @@ export function updatePlatformInstance(data) {
export function delPlatformInstance(ids) { export function delPlatformInstance(ids) {
return request({ url: `/platform/instance/${ids}`, method: 'delete' }) return request({ url: `/platform/instance/${ids}`, method: 'delete' })
} }
export function initializePlatform(id) {
return request({ url: `/platform/instance/${id}/initialize`, method: 'post' })
}
export function getProjectsByPlatformId(id) {
return request({ url: `/platform/instance/${id}/projects`, method: 'get' })
}

View 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' })
}

View File

@ -134,8 +134,13 @@
<el-option v-for="p in platformList" :key="p.id" :label="p.name" :value="p.id" /> <el-option v-for="p in platformList" :key="p.id" :label="p.name" :value="p.id" />
</el-select> </el-select>
</el-form-item> </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-form-item label="所属项目" prop="platformProjectId">
<el-select v-model="form.platformProjectId" placeholder="请选择项目" style="width:100%" @change="onProjectChange" :disabled="!form.platformId || form.netStatus === 1" clearable> <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-option v-for="proj in projectOptions" :key="proj.id" :label="proj.name" :value="String(proj.id)" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@ -331,7 +336,8 @@
<script> <script>
import { listPlatformDeviceMapping, addPlatformDeviceMapping, updatePlatformDeviceMapping, delPlatformDeviceMapping, onboardDevice, listSubMappings, syncSubMappings } from '@/api/platform/deviceMapping' import { listPlatformDeviceMapping, addPlatformDeviceMapping, updatePlatformDeviceMapping, delPlatformDeviceMapping, onboardDevice, listSubMappings, syncSubMappings } from '@/api/platform/deviceMapping'
import { listPlatformInstance, getProjectsByPlatformId } from '@/api/platform/instance' import { listPlatformInstance } from '@/api/platform/instance'
import { getInitializedUsers, getProjectsByUserAuthId } from '@/api/platform/userAuth'
import { listPlatformInterface } from '@/api/platform/interface' import { listPlatformInterface } from '@/api/platform/interface'
import { listPlatformFlow, getPlatformFlow } from '@/api/platform/flow' import { listPlatformFlow, getPlatformFlow } from '@/api/platform/flow'
import { listDevice, listThingsModel, getDevice, listDeviceShort } from '@/api/iot/device' import { listDevice, listThingsModel, getDevice, listDeviceShort } from '@/api/iot/device'
@ -350,6 +356,7 @@ export default {
deviceSearchLoading: false, thingsModelOptions: [], deviceSearchLoading: false, thingsModelOptions: [],
interfaceOptions: [], interfaceOptions: [],
projectOptions: [], projectOptions: [],
initializedUserList: [],
open: false, title: '', open: false, title: '',
fieldMappingRows: [], fieldMappingRows: [],
// 网关子设备映射行 // 网关子设备映射行
@ -362,12 +369,13 @@ export default {
queryParams: { pageNum: 1, pageSize: 20, platformId: null, serialNumber: '', sensorType: '', netStatus: null }, queryParams: { pageNum: 1, pageSize: 20, platformId: null, serialNumber: '', sensorType: '', netStatus: null },
form: { form: {
enable: '1', deviceType: 0, reportIntervalSec: 300, deviceName: '', enable: '1', deviceType: 0, reportIntervalSec: 300, deviceName: '',
platformId: null, platformProjectId: null, platformProjectName: '', platformId: null, platformUserAuthId: null, platformProjectId: null, platformProjectName: '',
pushInterfaceId: null, pushMode: 1, pushInterval: 5, pushInterfaceId: null, pushMode: 1, pushInterval: 5,
deviceId: null, serialNumber: '', sensorType: '', remark: '' deviceId: null, serialNumber: '', sensorType: '', remark: ''
}, },
rules: { rules: {
platformId: [{ required: true, message: '请选择平台', trigger: 'change' }], platformId: [{ required: true, message: '请选择平台', trigger: 'change' }],
platformUserAuthId: [{ required: true, message: '请选择用户', trigger: 'change' }],
platformProjectId: [{ required: true, message: '请选择所属项目', trigger: 'change' }], platformProjectId: [{ required: true, message: '请选择所属项目', trigger: 'change' }],
deviceId: [{ required: true, message: '请选择设备', trigger: 'change' }], deviceId: [{ required: true, message: '请选择设备', trigger: 'change' }],
sensorType: [{ required: true, message: '请选择或输入设备类型', trigger: 'change' }], sensorType: [{ required: true, message: '请选择或输入设备类型', trigger: 'change' }],
@ -468,7 +476,7 @@ export default {
handleAdd() { handleAdd() {
this.form = { this.form = {
enable: '1', deviceType: 0, pushInterfaceId: null, pushMode: 1, pushInterval: 5, reportIntervalSec: 300, deviceName: '', enable: '1', deviceType: 0, pushInterfaceId: null, pushMode: 1, pushInterval: 5, reportIntervalSec: 300, deviceName: '',
platformId: null, platformProjectId: null, platformProjectName: '', platformId: null, platformUserAuthId: null, platformProjectId: null, platformProjectName: '',
deviceId: null, serialNumber: '', sensorType: '', remark: '' deviceId: null, serialNumber: '', sensorType: '', remark: ''
} }
this.fieldMappingRows = [] this.fieldMappingRows = []
@ -479,6 +487,7 @@ export default {
this.thingsModelOptions = [] this.thingsModelOptions = []
this.interfaceOptions = [] this.interfaceOptions = []
this.projectOptions = [] this.projectOptions = []
this.initializedUserList = []
this.title = '新增设备绑定'; this.open = true this.title = '新增设备绑定'; this.open = true
}, },
handleUpdate(row) { handleUpdate(row) {
@ -494,10 +503,14 @@ export default {
this.itemFieldMappingRows = extra.itemFieldMappings || [] this.itemFieldMappingRows = extra.itemFieldMappings || []
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
} }
// 加载该平台的接口列表 // 加载该平台的接口列表和用户列表
if (row.platformId) { if (row.platformId) {
this.loadInterfaces(row.platformId) this.loadInterfaces(row.platformId)
this.loadProjects(row.platformId) this.loadInitializedUsers(row.platformId)
}
// 加载用户的项目列表
if (row.platformUserAuthId) {
this.loadProjectsByUserAuth(row.platformUserAuthId)
} }
// 加载子设备映射(网关设备) // 加载子设备映射(网关设备)
if (row.deviceType === 1 && row.id) { if (row.deviceType === 1 && row.id) {
@ -533,17 +546,33 @@ export default {
onPlatformChange(platformId) { onPlatformChange(platformId) {
this.interfaceOptions = [] this.interfaceOptions = []
this.form.pushInterfaceId = null this.form.pushInterfaceId = null
this.initializedUserList = []
this.form.platformUserAuthId = null
this.projectOptions = [] this.projectOptions = []
this.form.platformProjectId = null this.form.platformProjectId = null
this.form.platformProjectName = '' this.form.platformProjectName = ''
if (platformId) { if (platformId) {
this.loadInterfaces(platformId) this.loadInterfaces(platformId)
this.loadProjects(platformId) this.loadInitializedUsers(platformId)
} }
}, },
loadProjects(platformId) { loadInitializedUsers(platformId) {
if (!platformId) { this.projectOptions = []; return } if (!platformId) { this.initializedUserList = []; return }
getProjectsByPlatformId(platformId).then(res => { 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 || [] this.projectOptions = res.data || []
}).catch(() => { this.projectOptions = [] }) }).catch(() => { this.projectOptions = [] })
}, },

View File

@ -55,7 +55,7 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" fixed="right" width="200" align="center"> <el-table-column label="操作" fixed="right" width="200" align="center">
<template slot-scope="scope"> <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-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> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['platform:flow:remove']">删除</el-button>
</template> </template>
@ -227,17 +227,26 @@
<el-dialog :visible.sync="resultSaveDialogVisible" title="结果保存配置" width="760px" append-to-body :close-on-click-modal="false"> <el-dialog :visible.sync="resultSaveDialogVisible" title="结果保存配置" width="760px" append-to-body :close-on-click-modal="false">
<div class="schema-tip" style="margin-bottom:8px"> <div class="schema-tip" style="margin-bottom:8px">
`field` 填接口出参字段名;`saveTo` 支持 `extra_config.xxx`、`platform_instance.access_token`、`device_mapping.platform_point_id` 等目标。 根据所选接口的出参定义,选择需要持久化的字段和保存目标。未选择保存目标的字段仅作为中间结果传递。
</div> </div>
<el-table :data="stepSaveRows" size="mini" border> <el-table :data="stepSaveRows" size="mini" border>
<el-table-column label="出参字段" width="180"> <el-table-column label="出参字段" width="200">
<template slot-scope="scope"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column label="保存目标" min-width="360"> <el-table-column label="保存目标" min-width="340">
<template slot-scope="scope"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="60" align="center"> <el-table-column label="操作" width="60" align="center">
@ -285,6 +294,7 @@ export default {
editingStepIndex: -1, editingStepIndex: -1,
stepParamRows: [], stepParamRows: [],
stepSaveRows: [], stepSaveRows: [],
responseFieldOptions: [],
previousResultOptions: [], previousResultOptions: [],
queryParams: { pageNum: 1, pageSize: 20, platformId: null, flowType: null }, queryParams: { pageNum: 1, pageSize: 20, platformId: null, flowType: null },
form: { enable: '1', flowType: 1 }, form: { enable: '1', flowType: 1 },
@ -304,6 +314,17 @@ export default {
;(this.list || []).forEach(item => this.appendSensorTypeOption(optionMap, item && item.sensorType)) ;(this.list || []).forEach(item => this.appendSensorTypeOption(optionMap, item && item.sensorType))
this.appendSensorTypeOption(optionMap, this.form.sensorType) this.appendSensorTypeOption(optionMap, this.form.sensorType)
return Array.from(optionMap.values()) 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() { created() {
@ -400,13 +421,6 @@ export default {
handleEnableChange(row) { handleEnableChange(row) {
savePlatformFlow({ flow: { id: row.id, enable: row.enable }, steps: null }) 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) { loadInterfaces(platformId) {
if (!platformId) { if (!platformId) {
this.interfaceOptions = [] this.interfaceOptions = []
@ -585,11 +599,26 @@ export default {
this.paramMappingDialogVisible = false this.paramMappingDialogVisible = false
}, },
openResultSaveDialog(stepIndex) { openResultSaveDialog(stepIndex) {
const step = this.steps[stepIndex]
if (!step || !step.interfaceId) {
this.$modal.msgWarning('请先选择接口')
return
}
this.editingStepIndex = stepIndex 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 || '', field: item.field || '',
saveTo: item.saveTo || '' saveTo: item.saveTo || ''
})) }))
if (this.stepSaveRows.length === 0 && this.responseFieldOptions.length > 0) {
// 默认填充一个空行
this.stepSaveRows.push({ field: '', saveTo: '' })
}
this.resultSaveDialogVisible = true this.resultSaveDialogVisible = true
}, },
confirmResultSave() { confirmResultSave() {

View File

@ -42,12 +42,6 @@
<el-tag size="small" v-if="scope.row.authType===3">OAuth2</el-tag> <el-tag size="small" v-if="scope.row.authType===3">OAuth2</el-tag>
</template> </template>
</el-table-column> </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>
<el-table-column label="初始化" width="90" align="center"> <el-table-column label="初始化" width="90" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
@ -63,7 +57,7 @@
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip /> <el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" width="260" align="center"> <el-table-column label="操作" fixed="right" width="260" align="center">
<template slot-scope="scope"> <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-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-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> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['platform:instance:remove']">删除</el-button>
@ -144,11 +138,97 @@
<!-- 接口管理抽屉 --> <!-- 接口管理抽屉 -->
<interface-drawer ref="interfaceDrawer" :platform-id="currentPlatformId" :platform-name="currentPlatformName" /> <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> </div>
</template> </template>
<script> <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 InterfaceDrawer from '../components/InterfaceDrawer'
import KeyValueEditor from '../components/KeyValueEditor' import KeyValueEditor from '../components/KeyValueEditor'
@ -176,7 +256,16 @@ export default {
code: [{ required: true, message: '请输入平台标识', trigger: 'blur' }], code: [{ required: true, message: '请输入平台标识', trigger: 'blur' }],
baseUrl: [{ required: true, message: '请输入baseURL', trigger: 'blur' }], baseUrl: [{ required: true, message: '请输入baseURL', trigger: 'blur' }],
authType:[{ required: true, message: '请选择认证类型', trigger: 'change' }] 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() }, created() { this.getList() },
@ -189,10 +278,6 @@ export default {
this.loading = false 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() }, handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() }, resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
handleSelectionChange(sel) { this.multiple = sel.length !== 1 }, handleSelectionChange(sel) { this.multiple = sel.length !== 1 },
@ -223,19 +308,77 @@ export default {
this.$modal.msgSuccess(row.enable === '1' ? '已启用' : '已禁用') this.$modal.msgSuccess(row.enable === '1' ? '已启用' : '已禁用')
}) })
}, },
handleInitialize(row) {
this.$confirm(`确认初始化平台【${row.name}】?将执行初始化流程获取 userId。`).then(() => {
initializePlatform(row.id).then(() => {
this.$modal.msgSuccess('初始化成功')
this.getList()
})
})
},
handleInterface(row) { handleInterface(row) {
this.currentPlatformId = row.id this.currentPlatformId = row.id
this.currentPlatformName = row.name this.currentPlatformName = row.name
this.$nextTick(() => { this.$refs.interfaceDrawer.open() }) 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) { parseObjectRows(value) {
if (!value) return [] if (!value) return []
try { try {
@ -261,7 +404,7 @@ export default {
submitForm() { submitForm() {
this.$refs.form.validate(valid => { this.$refs.form.validate(valid => {
if (!valid) return 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.form.extraConfig = this.buildObjectJson(this.platformConfigRows)
this.submitLoading = true this.submitLoading = true
const api = this.form.id ? updatePlatformInstance : addPlatformInstance const api = this.form.id ? updatePlatformInstance : addPlatformInstance