20260704:1、加入设备名称推送;2、明确入网参数绑定数组数据(孔深、高程);3、清空推送日志,按设备类型查询推送日志

This commit is contained in:
zhugongping 2026-07-05 08:48:47 +08:00
parent 820bd95632
commit 355e0eaa27
19 changed files with 457 additions and 56 deletions

View File

@ -120,11 +120,27 @@ DDL 以 `sql/platform-integration.sql` 为准。
| `auth` | 从 `platform_instance.auth_config` 按 `field` 取值 |
| `extra` | 从 `platform_instance.extra_config` 按 `field` 取值 |
| `context` | 从 `FlowContext` 按 `contextKey` 取值 |
| `device` | 从 `PlatformDeviceMapping` 取 `platform_device_id`、`platform_point_id`、`serial_number`、`gateway_serial_number`、`monitor_serial_number` |
| `onboard_config` | 从当前子映射 `onboard_config` 取值 |
| `device` | 从 `PlatformDeviceMapping` / 当前网关子设备上下文取 `platform_device_id`、`platform_point_id`、`serial_number`、`device_name`、`gateway_serial_number`、`monitor_serial_number` |
| `thingsModel` | 从流程上下文 payload 按物模型标识取值,通常用于手动流程或扩展流程 |
| `ts` | 从流程上下文 payload 取时间戳 |
| `onboard_config` | 入网参数,直接取接口 `request_schema` 或步骤覆盖配置中的 `value`,不再读取子映射表 `onboard_config` |
| `subDeviceSensorIds` | 收集子设备 sensorId 列表 |
| `referenceSubDeviceSensorIdPairs` | 收集 `[基准点sensorId, 监测点sensorId]` 配对列表,可能展开为多次请求 |
`onboard_config` 是设备入网专用的可配置入参。接口管理页或流程步骤覆盖页直接维护 `value`:
```json
{"name":"sensorDepths","type":"array","source":"onboard_config","value":[1.7,3.4,5.1,6.8]}
{"name":"holeDepth","type":"number","source":"onboard_config","value":[1.7,3.4,5.1,6.8]}
```
网关多 monitor 入网时,`PlatformDeviceMappingServiceImpl` 按 `PlatformDeviceSubMappingMapper.selectByParentId` 返回顺序筛选 monitor,并把当前下标写入 `FlowContext.currentMonitorIndex`。流程执行器解析 `onboard_config` 时:
- `value` 不是数组:每个子设备使用同一个值。
- `value` 是数组:按当前 monitor 下标取对应元素。
- schema `type=array` 且取到单值:最终请求体会包装为单元素数组,例如 `1.7` -> `[1.7]`。
- 数组长度小于待入网 monitor 数量:直接抛错,避免错绑。
实时推送适配器中支持的 `source`:
| source | 含义 |
@ -149,7 +165,7 @@ DDL 以 `sql/platform-integration.sql` 为准。
| `platform_id` | 所属平台 |
| `name` / `code` | 流程名称与标识 |
| `flow_type` | `1=平台初始化`、`2=设备入网`、`3=Token刷新` |
| `sensor_type` | 入网流程适用传感器类型,如 `inclino`、`stress`、`hyeto`、`static_level` |
| `sensor_type` | 入网流程适用传感器类型,如 `inclino`、`deep_inclino`、`stress`、`hyeto`、`static_level` |
| `enable` | 是否启用 |
### 3.4 `platform_flow_step` 流程步骤
@ -185,7 +201,7 @@ DDL 以 `sql/platform-integration.sql` 为准。
| `device_id` / `serial_number` | FastBee 设备 ID 与序列号 |
| `platform_device_id` | 平台侧 deviceId,直连设备入网后回填 |
| `platform_point_id` | 平台侧 pointId,直连设备入网后回填 |
| `sensor_type` | `inclino`、`stress`、`hyeto`、`static_level` |
| `sensor_type` | 传感器类型,来自 `platform_sensor_type` 字典;当前包含 `inclino`、`deep_inclino`、`stress`、`hyeto`、`static_level` |
| `device_type` | `0=直连设备`、`1=网关设备` |
| `push_interface_id` | 推送接口 ID,用于动态字段映射和推送路径 |
| `field_mapping` | 标量字段映射,格式如 `[{identifier, platformField, transform}]` |
@ -206,7 +222,7 @@ DDL 以 `sql/platform-integration.sql` 为准。
| `device_role` | `reference=基准点`、`monitor=监测点` |
| `platform_device_id` | 平台分配给该监测点的 deviceId |
| `platform_point_id` | 平台分配给该监测点的 pointId |
| `onboard_config` | 入网参数 JSON,例如 `sensorId`、`refSensorId`、`refSerialNumber` |
| `onboard_config` | 子设备平台标识 JSON,例如 `sensorId`、`refSensorId`、`refSerialNumber`;普通绑定接口参数不放这里 |
| `net_status` | 子设备入网状态 |
唯一约束:`parent_mapping_id + sub_serial_number`。
@ -326,8 +342,10 @@ execute(flow, context)
2. 筛选 `device_role=monitor` 且未入网的子设备。
3. 对每个 monitor 创建独立 `FlowContext`:
- `currentMonitorSerialNumber = monitor.sub_serial_number`
- `currentMonitorDeviceName = monitor.sub_device_name`
- `currentMonitorIndex = monitor 在待入网 monitor 列表中的下标`
- `subMappingOnboardConfig = monitor.onboard_config`
4. 执行同一个入网流程。
4. 执行同一个入网流程。若绑定接口 schema 使用 `source=onboard_config` 且 `value` 为数组,会按 `currentMonitorIndex` 拆出当前子设备的值。
5. 从上下文 `step1.id`、`step2.id` 取 pointId/deviceId,写回当前 monitor 子表。
6. 所有 monitor 都入网后,主绑定 `net_status=1`。
7. 若部分成功部分失败,已成功子设备会保留结果,接口返回明确失败信息。
@ -355,19 +373,11 @@ WHERE s.sub_serial_number = #{serialNumber} AND m.enable = '1'
### 5.4 DCP 普通推送
DCP 推送接口优先使用设备绑定的 `push_interface_id`:
DCP 实时推送必须在设备绑定中配置 `push_interface_id`:
- 有推送接口:使用接口 `path`、`code`、`request_schema`。
- 未配置或解析失败:按 `sensor_type` 回退到内置路径。
内置路径:
| sensor_type | 路径 |
|---|---|
| `inclino` | `/api/app/inclino-data/many` |
| `stress` | `/api/app/stress-data/many` |
| `hyeto` | `/api/app/hyeto-data/many` |
| `static_level` | `/api/app/static-level-data/many` |
- 使用推送接口的 `path`、`code`、`request_schema` 组装并发送请求。
- 未配置推送接口时,`DcpPlatformAdapter.resolvePushPath` 直接抛错,不再按 `sensor_type` 回退到内置路径。
- `resolveInterfaceCode` 仍会在接口 `code` 为空时使用 `dcp_push_{sensorType}` 作为日志标识兜底。
请求体构建规则:
@ -468,6 +478,7 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5
- 按平台维护接口。
- 配置 method、path、content-type、请求 schema、响应 schema。
- 请求 schema 用于流程执行和设备绑定页动态字段映射。
- `source=onboard_config` 的取值配置编辑 `value`,支持填写 JSON 数组或数字/字符串。保存时前端会把 `[1.7,3.4]` 转成数组,把 `6.8` 转成数字。
### 6.3 流程编排
@ -481,6 +492,7 @@ Redis TTL 为 `reportIntervalSec * 2`,读取时超过 `reportIntervalSec * 1.5
- 新增、编辑流程及步骤。
- 查看流程详情时返回 `{flow, steps}`。
- 手动执行:`POST /platform/flow/{id}/execute`,可选 `mappingId`。
- 步骤入参覆盖同样支持 `source=onboard_config`,语义与接口管理一致:配置 `value`,不配置子映射字段名。
### 6.4 设备绑定
@ -557,6 +569,47 @@ SET @dcp_platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMI
- 静力水准推送接口定义了数组字段 `sensorData`,其 `itemSchema` 包含 `epoch`、`height`、`temperature`、`sensorId`。
- 轴力计和雨量计 DCP 文档要求累计值递增;当前代码未在适配器中实现“查询上次成功值并跳过非递增值”的通用校验,若目标平台严格校验,需要在推送前补充该逻辑或依赖平台返回失败日志。
### 7.4 深部位移计扩展配置
`sql/platform-integration.sql` 已在 `platform_sensor_type` 字典中维护 `deep_inclino`,前端传感器类型下拉也支持从字典读取和手动输入。`sql/platform-dcp-init-data.sql` 当前未内置深部位移计绑定接口和入网流程,需要在接口管理和流程编排中配置。
DCP 深部位移绑定接口:
| 项 | 值 |
|---|---|
| code 建议 | `dcp_bind_deep_inclino` |
| method | `POST` |
| path | `/api/app/deep-inclino-device` |
| content-type | `application/json` |
典型 `request_schema`:
```json
[
{"name":"projectId","type":"string","source":"extra","field":"projectId","required":true},
{"name":"pointId","type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"name","type":"string","source":"device","field":"serial_number","required":true},
{"name":"moduleId","type":"string","source":"uuid","required":true},
{"name":"virtualId","type":"string","source":"uuid","required":true},
{"name":"sensorDepths","type":"array","source":"onboard_config","value":[1.7,3.4,5.1,6.8],"required":true},
{"name":"holeDepth","type":"number","source":"onboard_config","value":[1.7,3.4,5.1,6.8],"required":true},
{"name":"sensorIds","type":"array","source":"subDeviceSensorIds","required":true},
{"name":"axisDirection","type":"string","source":"onboard_config","value":"X","required":false},
{"name":"holeElevation","type":"number","source":"onboard_config","value":0,"required":false},
{"name":"zipDuring","type":"number","source":"onboard_config","value":0,"required":false},
{"name":"frequency","type":"number","source":"onboard_config","value":2147483647,"required":false},
{"name":"remark","type":"string","source":"onboard_config","value":"","required":false}
]
```
网关多子设备绑定深部位移计时,`sensorDepths` 与 `holeDepth` 这类数组值按待入网 monitor 子设备顺序逐个拆分:
- 第 1 个 monitor:`sensorDepths` 取 `1.7`,因字段类型为 `array`,最终请求为 `"sensorDepths":[1.7]`;`holeDepth` 为 `1.7`。
- 第 2 个 monitor:`sensorDepths` 为 `[3.4]`,`holeDepth` 为 `3.4`。
- 依此类推。
深度类字段按工程习惯使用正数,表示距孔口向下的距离;若需要平台侧高程,按 `holeElevation - sensorDepth` 计算,不通过负深度表达方向。
---
## 8. 关键复用点
@ -577,6 +630,7 @@ SET @dcp_platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMI
- 当前适配器只有 DCP。新增平台需要新增 adapter,并在 `DeviceReportTrigger.dispatchPush` 中按平台 `code` 路由。
- 文档中的“可视化配置”已覆盖平台、接口、流程、设备绑定、子映射和日志,但平台适配器路由仍需要代码扩展。
- DCP 实时推送必须配置 `push_interface_id`,未配置时不会按传感器类型自动回退路径。
- 实时推送只支持 POST JSON,流程执行支持 GET、form、JSON。
- `idempotency_key` 字段已建表,但实时推送未生成幂等键。
- `PlatformInstanceController.getInfo` 只清空 `access_token`,注释提到密码脱敏,但当前代码未改写 `auth_config` 内密码。

View File

@ -50,4 +50,12 @@ public class PlatformPushLogController extends BaseController {
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(pushLogService.removeByIds(Arrays.asList(ids)));
}
@PreAuthorize("@ss.hasPermi('platform:pushLog:remove')")
@DeleteMapping("/clean")
@ApiOperation("批量删除推送日志")
public AjaxResult clean(@PathVariable Long[] ids) {
pushLogService.cleanPushLog();
return success();
}
}

View File

@ -816,7 +816,7 @@ public class DcpPlatformAdapter {
}
private Object resolveDeviceField(PlatformDeviceMapping mapping, String field) {
if (!StringUtils.hasText(field)) {
if (mapping == null || !StringUtils.hasText(field)) {
return null;
}
switch (field) {
@ -826,11 +826,22 @@ public class DcpPlatformAdapter {
return mapping.getPlatformPointId();
case "serial_number":
return mapping.getSerialNumber();
case "device_name":
return resolveDeviceName(mapping);
default:
return null;
}
}
private String resolveDeviceName(PlatformDeviceMapping mapping) {
if (mapping == null) {
return null;
}
return StringUtils.hasText(mapping.getDeviceName())
? mapping.getDeviceName()
: mapping.getSerialNumber();
}
private Object resolveJsonField(String json, String field) {
if (!StringUtils.hasText(json) || !StringUtils.hasText(field)) {
return null;

View File

@ -33,6 +33,9 @@ public class PlatformDeviceMapping implements Serializable {
@ApiModelProperty("FastBee设备序列号")
private String serialNumber;
@ApiModelProperty("FastBee设备名称")
private String deviceName;
@ApiModelProperty("DCP侧deviceId,入网后自动回填")
private String platformDeviceId;

View File

@ -30,6 +30,9 @@ public class PlatformDeviceSubMapping implements Serializable {
@ApiModelProperty("子设备序列号")
private String subSerialNumber;
@ApiModelProperty("子设备名称")
private String subDeviceName;
@ApiModelProperty("角色: reference=基准 monitor=监测")
private String deviceRole;

View File

@ -11,19 +11,50 @@ import java.util.Map;
*/
public class FlowContext {
/**
* 平台实体
*/
private final PlatformInstance platform;
/**
* 绑定推送关系
*/
private final PlatformDeviceMapping deviceMapping;
/** 设备上报的原始 payload(实时推送时有值) */
/**
* 设备上报的原始 payload(实时推送时有值)
*/
private final String payload;
/** 步骤出参存储,key = step.resultKey,value = 解析后的 JSON Map */
/**
* 步骤出参存储,key = step.resultKey,value = 解析后的 JSON Map
*/
private final Map<String, Object> stepResults = new HashMap<>();
/** 网关入网时,指定当前处理的监测点序列号 */
/**
* 网关入网时,指定当前处理的监测点序列号
*/
private String currentMonitorSerialNumber;
/** 网关子设备入网时,当前子映射行的 onboard_config JSON(平台相关参数) */
/**
* 网关入网时,指定当前处理的监测点名称
*/
private String currentMonitorDeviceName;
/**
* 网关入网时,当前监测点在 PlatformDeviceSubMapping 监测点列表中的顺序
*/
private Integer currentMonitorIndex;
/**
* 网关子设备入网时,当前子映射行的 onboard_config JSON(平台相关参数)
*/
private String subMappingOnboardConfig;
/** 网关子设备入网时,当前基准点序列号 */
/**
* 网关子设备入网时,当前基准点序列号
*/
private String currentReferenceSerialNumber;
/** 网关子设备入网时,基准点子映射行的 onboard_config JSON */
/**
* 网关子设备入网时,当前基准点名称
*/
private String currentReferenceDeviceName;
/**
* 网关子设备入网时,基准点子映射行的 onboard_config JSON
*/
private String referenceSubMappingOnboardConfig;
public FlowContext(PlatformInstance platform, PlatformDeviceMapping deviceMapping, String payload) {
@ -60,6 +91,22 @@ public class FlowContext {
this.currentMonitorSerialNumber = currentMonitorSerialNumber;
}
public String getCurrentMonitorDeviceName() {
return currentMonitorDeviceName;
}
public void setCurrentMonitorDeviceName(String currentMonitorDeviceName) {
this.currentMonitorDeviceName = currentMonitorDeviceName;
}
public Integer getCurrentMonitorIndex() {
return currentMonitorIndex;
}
public void setCurrentMonitorIndex(Integer currentMonitorIndex) {
this.currentMonitorIndex = currentMonitorIndex;
}
public String getSubMappingOnboardConfig() {
return subMappingOnboardConfig;
}
@ -76,6 +123,14 @@ public class FlowContext {
this.currentReferenceSerialNumber = currentReferenceSerialNumber;
}
public String getCurrentReferenceDeviceName() {
return currentReferenceDeviceName;
}
public void setCurrentReferenceDeviceName(String currentReferenceDeviceName) {
this.currentReferenceDeviceName = currentReferenceDeviceName;
}
public String getReferenceSubMappingOnboardConfig() {
return referenceSubMappingOnboardConfig;
}

View File

@ -225,7 +225,7 @@ public class PlatformFlowExecutor {
/**
* 解析步骤入参
* source 枚举: fixed / auth / extra / context / device
* source 枚举: fixed / auth / extra / context / device / thingsModel / ts
*/
@SuppressWarnings("unchecked")
private Map<String, Object> resolveParams(PlatformInterface iface, PlatformFlowStep step, FlowContext ctx) {
@ -297,6 +297,9 @@ public class PlatformFlowExecutor {
}
return normalizeValueByType(fd.getString("type"), resolveGatewaySerialNumber(ctx.getDeviceMapping()));
}
if ("device_name".equals(df)) {
return resolveDeviceName(ctx);
}
if ("gateway_serial_number".equals(df)) {
return resolveGatewaySerialNumber(ctx.getDeviceMapping());
}
@ -304,9 +307,12 @@ public class PlatformFlowExecutor {
return ctx.getCurrentMonitorSerialNumber();
}
return null;
case "thingsModel":
return resolveThingsModelValue(ctx, fd);
case "ts":
return resolveThingsModelTs(ctx, fd);
case "onboard_config":
// 从子映射的 onboard_config JSON 中取指定字段
return resolveOnboardConfigField(ctx, fd.getString("field"));
return resolveOnboardConfigValue(ctx, fd);
case "subDeviceSensorIds":
return collectSubDeviceSensorIds(ctx);
case "referenceSubDeviceSensorIdPairs":
@ -316,19 +322,94 @@ public class PlatformFlowExecutor {
}
}
/**
* 从子映射 onboard_config 中取字段值
*/
private Object resolveOnboardConfigField(FlowContext ctx, String field) {
if (!StringUtils.hasText(field) || !StringUtils.hasText(ctx.getSubMappingOnboardConfig())) {
private Object resolveThingsModelValue(FlowContext ctx, JSONObject fieldDef) {
JSONObject payload = parsePayloadToObject(ctx.getPayload());
if (payload == null) {
return null;
}
String identifier = resolveThingsModelIdentifier(fieldDef);
return StringUtils.hasText(identifier) ? payload.get(identifier) : null;
}
private Object resolveThingsModelTs(FlowContext ctx, JSONObject fieldDef) {
JSONObject payload = parsePayloadToObject(ctx.getPayload());
if (payload == null) {
return null;
}
String identifier = resolveThingsModelIdentifier(fieldDef);
if (StringUtils.hasText(identifier)) {
Object value = payload.get(identifier + "__ts");
if (value != null) {
return value;
}
}
Object ts = payload.get("ts");
return ts != null ? ts : payload.get("timestamp");
}
private String resolveThingsModelIdentifier(JSONObject fieldDef) {
String identifier = fieldDef.getString("identifier");
if (StringUtils.hasText(identifier)) {
return identifier;
}
String field = fieldDef.getString("field");
return StringUtils.hasText(field) ? field : fieldDef.getString("contextKey");
}
private JSONObject parsePayloadToObject(String payload) {
if (!StringUtils.hasText(payload)) {
return null;
}
try {
JSONObject config = JSON.parseObject(ctx.getSubMappingOnboardConfig());
return config.get(field);
String trimmed = payload.trim();
if (trimmed.startsWith("{")) {
return JSON.parseObject(trimmed);
}
if (trimmed.startsWith("[")) {
JSONObject result = new JSONObject();
JSONArray arr = JSON.parseArray(trimmed);
for (int i = 0; i < arr.size(); i++) {
JSONObject item = arr.getJSONObject(i);
if (item == null) {
continue;
}
String id = item.getString("identifier");
if (!StringUtils.hasText(id)) {
id = item.getString("id");
}
if (!StringUtils.hasText(id)) {
continue;
}
result.put(id, item.get("value"));
Object ts = item.get("ts");
if (ts != null) {
result.put(id + "__ts", ts);
}
}
return result;
}
} catch (Exception e) {
return null;
log.warn("[PlatformFlow] 解析设备上报 payload 失败: {}", e.getMessage());
}
return null;
}
private Object resolveOnboardConfigValue(FlowContext ctx, JSONObject fieldDef) {
String name = fieldDef.getString("name");
if (!fieldDef.containsKey("value")) {
throw new RuntimeException("入网参数 " + name + " 未配置值");
}
Object value = fieldDef.get("value");
if (value instanceof JSONArray && ctx.getCurrentMonitorIndex() != null) {
JSONArray values = (JSONArray) value;
int index = ctx.getCurrentMonitorIndex();
if (index >= values.size()) {
throw new RuntimeException("入网参数 " + name + " 数组长度不足,当前子设备序号="
+ (index + 1) + ",数组长度=" + values.size());
}
return values.get(index);
}
return value;
}
private String newGeneratedId() {
@ -679,6 +760,9 @@ public class PlatformFlowExecutor {
}
private String resolveGatewaySerialNumber(PlatformDeviceMapping mapping) {
if (mapping == null) {
return null;
}
String serialNumber = normalizeText(mapping.getSerialNumber());
if (StringUtils.hasText(serialNumber)) {
return serialNumber;
@ -696,6 +780,21 @@ public class PlatformFlowExecutor {
}
}
private String resolveDeviceName(FlowContext ctx) {
if (ctx == null || ctx.getDeviceMapping() == null) {
return null;
}
if (StringUtils.hasText(ctx.getCurrentMonitorSerialNumber())) {
return StringUtils.hasText(ctx.getCurrentMonitorDeviceName())
? ctx.getCurrentMonitorDeviceName()
: ctx.getCurrentMonitorSerialNumber();
}
PlatformDeviceMapping mapping = ctx.getDeviceMapping();
return StringUtils.hasText(mapping.getDeviceName())
? mapping.getDeviceName()
: mapping.getSerialNumber();
}
private boolean sameText(String left, String right) {
return Objects.equals(normalizeText(left), normalizeText(right));
}
@ -1093,7 +1192,9 @@ public class PlatformFlowExecutor {
return value;
}
if (!(value instanceof String)) {
return value;
JSONArray array = new JSONArray();
array.add(value);
return array;
}
String text = ((String) value).trim();
if (!StringUtils.hasText(text)) {

View File

@ -11,4 +11,7 @@ public interface PlatformPushLogMapper extends BaseMapperX<PlatformPushLog> {
*/
String selectLastSuccessResponseBody(@Param("deviceId") Long deviceId,
@Param("interfaceCode") String interfaceCode);
void cleanPushLog();
}

View File

@ -17,4 +17,10 @@ public interface IPlatformPushLogService extends IService<PlatformPushLog> {
/** 手动重推:重新发送已记录的请求,更新日志状态 */
void retry(Long id);
/**
* 情况日志
*/
void cleanPushLog();
}

View File

@ -125,12 +125,16 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
int successCount = 0;
List<String> failedMessages = new ArrayList<>();
for (PlatformDeviceSubMapping monitor : monitors) {
for (int i = 0; i < monitors.size(); i++) {
PlatformDeviceSubMapping monitor = monitors.get(i);
FlowContext ctx = new FlowContext(platform, mapping, null);
ctx.setCurrentMonitorSerialNumber(monitor.getSubSerialNumber());
ctx.setCurrentMonitorDeviceName(monitor.getSubDeviceName());
ctx.setCurrentMonitorIndex(i);
ctx.setSubMappingOnboardConfig(monitor.getOnboardConfig());
if (reference != null) {
ctx.setCurrentReferenceSerialNumber(reference.getSubSerialNumber());
ctx.setCurrentReferenceDeviceName(reference.getSubDeviceName());
ctx.setReferenceSubMappingOnboardConfig(reference.getOnboardConfig());
}
@ -173,6 +177,12 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
}
}
/**
* 解析基准点,默认都是监测点
* @param mapping 平台设备绑定关系
* @param subMappings 平台子设备绑定关系
* @return 基准点
*/
private PlatformDeviceSubMapping resolveGatewayReference(PlatformDeviceMapping mapping,
List<PlatformDeviceSubMapping> subMappings) {
List<PlatformDeviceSubMapping> references = subMappings.stream()

View File

@ -38,6 +38,7 @@ public class PlatformPushLogServiceImpl extends ServiceImpl<PlatformPushLogMappe
.eq(query.getStatus() != null, PlatformPushLog::getStatus, query.getStatus())
.eq(query.getLogType() != null, PlatformPushLog::getLogType, query.getLogType())
.eq(StringUtils.hasText(query.getSerialNumber()), PlatformPushLog::getSerialNumber, query.getSerialNumber())
.eq(StringUtils.hasText(query.getInterfaceCode()), PlatformPushLog::getInterfaceCode, query.getInterfaceCode())
.orderByDesc(PlatformPushLog::getCreateTime);
return this.list(wrapper);
}
@ -114,4 +115,10 @@ public class PlatformPushLogServiceImpl extends ServiceImpl<PlatformPushLogMappe
throw new RuntimeException("重推失败: " + e.getMessage(), e);
}
}
@Override
public void cleanPushLog() {
pushLogMapper.cleanPushLog();
}
}

View File

@ -7,6 +7,7 @@
<result property="platformId" column="platform_id"/>
<result property="deviceId" column="device_id"/>
<result property="serialNumber" column="serial_number"/>
<result property="deviceName" column="device_name"/>
<result property="platformDeviceId" column="platform_device_id"/>
<result property="platformPointId" column="platform_point_id"/>
<result property="sensorType" column="sensor_type"/>

View File

@ -32,4 +32,7 @@
LIMIT 1
</select>
<update id="cleanPushLog">
truncate table platform_push_log
</update>
</mapper>

View File

@ -119,6 +119,7 @@ CREATE TABLE IF NOT EXISTS `platform_device_mapping` (
`platform_id` bigint(20) NOT NULL COMMENT '所属平台ID',
`device_id` bigint(20) NOT NULL COMMENT 'FastBee设备ID',
`serial_number` varchar(64) NOT NULL COMMENT 'FastBee设备序列号',
`device_name` varchar(64) DEFAULT NULL COMMENT 'FastBee设备名称',
`platform_device_id` varchar(100) DEFAULT NULL COMMENT 'DCP侧deviceId,入网后自动回填',
`platform_point_id` varchar(100) DEFAULT NULL COMMENT 'DCP侧pointId,入网后自动回填',
`sensor_type` varchar(50) NOT NULL COMMENT '传感器类型,来自字典 platform_sensor_type',
@ -145,6 +146,10 @@ ALTER TABLE `platform_device_mapping`
ADD COLUMN IF NOT EXISTS `device_type` tinyint(1) NOT NULL DEFAULT 0
COMMENT '设备类型: 0=直连设备 1=网关设备(多子设备)' AFTER `sensor_type`;
ALTER TABLE `platform_device_mapping`
ADD COLUMN IF NOT EXISTS `device_name` varchar(64) DEFAULT NULL
COMMENT 'FastBee设备名称' AFTER `serial_number`;
-- ----------------------------
-- 5.2 子设备映射(网关多子设备通用)
-- ----------------------------
@ -152,6 +157,7 @@ CREATE TABLE IF NOT EXISTS `platform_device_sub_mapping` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`parent_mapping_id` bigint(20) NOT NULL COMMENT '父映射ID (platform_device_mapping.id)',
`sub_serial_number` varchar(64) NOT NULL COMMENT '子设备序列号',
`sub_device_name` varchar(64) DEFAULT NULL COMMENT '子设备名称',
`device_role` varchar(20) NOT NULL DEFAULT 'monitor' COMMENT '角色: reference=基准 monitor=监测',
`platform_device_id` varchar(100) DEFAULT NULL COMMENT '平台分配的设备ID (入网后回填)',
`platform_point_id` varchar(100) DEFAULT NULL COMMENT '平台分配的测点/辅助ID (入网后回填)',
@ -165,6 +171,10 @@ CREATE TABLE IF NOT EXISTS `platform_device_sub_mapping` (
UNIQUE KEY `uk_parent_sub` (`parent_mapping_id`, `sub_serial_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台设备子映射(网关多子设备通用)';
ALTER TABLE `platform_device_sub_mapping`
ADD COLUMN IF NOT EXISTS `sub_device_name` varchar(64) DEFAULT NULL
COMMENT '子设备名称' AFTER `sub_serial_number`;
-- ----------------------------
-- 6. 推送日志
-- ----------------------------

View File

@ -15,3 +15,11 @@ export function retryPlatformPushLog(id) {
export function delPlatformPushLog(ids) {
return request({ url: `/platform/push-log/${ids}`, method: 'delete' })
}
// 清空推送日志
export function cleanPushLog() {
return request({
url: '/platform/push-log/clean',
method: 'delete'
})
}

View File

@ -156,8 +156,9 @@
<el-option label="平台设备ID" value="platform_device_id" />
<el-option label="平台测点ID" value="platform_point_id" />
<el-option label="系统设备编号" value="serial_number" />
<el-option label="系统设备名称" value="device_name" />
</el-select>
<el-input v-else-if="scope.row.source === 'onboard_config'" v-model="scope.row.field" size="mini" placeholder="入网参数字段,如 sensorDepths" />
<el-input v-else-if="scope.row.source === 'onboard_config'" v-model="scope.row.value" size="mini" placeholder="入网参数值,数组填 [1.7,3.4]" />
<span v-else-if="scope.row.source === 'uuid'" class="schema-hint">每次请求自动生成UUID</span>
<span v-else-if="scope.row.source === 'subDeviceSensorIds'" class="schema-hint">自动取设备绑定页子设备映射中的 sensorId 列表</span>
<span v-else-if="scope.row.source === 'referenceSubDeviceSensorIdPairs'" class="schema-hint">按基准点与每个监测点成对拆分请求</span>
@ -388,7 +389,12 @@ export default {
if (!json) return []
try {
const arr = JSON.parse(json)
return arr.map(f => ({ ...f, itemSchema: f.itemSchema || [], required: !!f.required }))
return arr.map(f => ({
...f,
value: f.source === 'onboard_config' ? this.stringifySchemaValue(f.value) : f.value,
itemSchema: f.itemSchema || [],
required: !!f.required
}))
} catch (e) { return [] }
},
handleDelete(row) {
@ -474,7 +480,8 @@ export default {
})
}
if (r.source === 'fixed') field.value = r.value
if (r.source === 'auth' || r.source === 'extra' || r.source === 'device' || r.source === 'onboard_config') field.field = r.field
if (r.source === 'onboard_config') field.value = this.parseSchemaValue(r.value)
if (r.source === 'auth' || r.source === 'extra' || r.source === 'device') field.field = r.field
if (r.source === 'context') field.contextKey = r.contextKey
return field
}))
@ -490,6 +497,27 @@ export default {
this.getList()
}).finally(() => { this.submitLoading = false })
})
},
parseSchemaValue(value) {
if (typeof value !== 'string') return value
const text = value.trim()
if (!text) return ''
if ((text.startsWith('[') && text.endsWith(']')) || (text.startsWith('{') && text.endsWith('}'))) {
return JSON.parse(text)
}
if (/^-?\d+(\.\d+)?$/.test(text)) {
return Number(text)
}
if (text === 'true' || text === 'false') {
return text === 'true'
}
return value
},
stringifySchemaValue(value) {
if (Array.isArray(value) || (value && typeof value === 'object')) {
return JSON.stringify(value)
}
return value == null ? '' : String(value)
}
}
}

View File

@ -43,6 +43,7 @@
<div v-if="scope.row.deviceType === 1 && getSubDevicePairs(scope.row).length > 0" style="padding:10px 50px">
<el-table :data="getSubDevicePairs(scope.row)" size="mini" border>
<el-table-column label="子设备序列号" prop="subSerialNumber" min-width="140" />
<el-table-column label="子设备名称" prop="subDeviceName" min-width="140" show-overflow-tooltip />
<el-table-column label="角色" prop="deviceRole" width="80">
<template slot-scope="s">
<el-tag size="mini" :type="s.row.deviceRole === 'reference' ? 'warning' : ''">{{ s.row.deviceRole === 'reference' ? '基准' : '监测' }}</el-tag>
@ -64,7 +65,7 @@
<template slot-scope="scope">{{ platformName(scope.row.platformId) }}</template>
</el-table-column>
<el-table-column label="设备名称" min-width="140">
<template slot-scope="scope">{{ deviceName(scope.row.deviceId) }}</template>
<template slot-scope="scope">{{ scope.row.deviceName || deviceName(scope.row.deviceId) }}</template>
</el-table-column>
<el-table-column label="序列号" prop="serialNumber" width="140" />
<el-table-column label="传感器类型" width="110" align="center">
@ -128,6 +129,9 @@
<el-form-item label="序列号">
<el-input v-model="form.serialNumber" disabled />
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="form.deviceName" disabled />
</el-form-item>
<el-form-item label="传感器类型" prop="sensorType">
<el-select v-model="form.sensorType" placeholder="选择或输入设备类型" filterable allow-create default-first-option style="width:100%">
<el-option v-for="s in sensorTypeOptions" :key="s.value" :label="s.label" :value="s.value" />
@ -167,11 +171,16 @@
<el-select v-if="subDeviceOptions.length > 0"
v-model="scope.row.subSerialNumber" size="mini" placeholder="选择子设备" filterable style="width:100%"
@change="onSubSerialChange(scope.row)">
<el-option v-for="device in subDeviceOptions" :key="device.serialNumber" :label="device.serialNumber" :value="device.serialNumber" />
<el-option v-for="device in subDeviceOptions" :key="device.serialNumber" :label="subDeviceOptionLabel(device)" :value="device.serialNumber" />
</el-select>
<el-input v-else v-model="scope.row.subSerialNumber" size="mini" placeholder="填写子设备编号" @change="onSubSerialChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="子设备名称" min-width="140">
<template slot-scope="scope">
<el-input v-model="scope.row.subDeviceName" size="mini" placeholder="默认同子设备编号" />
</template>
</el-table-column>
<el-table-column label="角色" width="120" align="center">
<template slot-scope="scope">
<el-select v-model="scope.row.deviceRole" size="mini" style="width:100%" @change="onSubRoleChange(scope.row)">
@ -258,6 +267,7 @@
import { listPlatformDeviceMapping, addPlatformDeviceMapping, updatePlatformDeviceMapping, delPlatformDeviceMapping, onboardDevice, listSubMappings, syncSubMappings } from '@/api/platform/deviceMapping'
import { listPlatformInstance } from '@/api/platform/instance'
import { listPlatformInterface } from '@/api/platform/interface'
import { listPlatformFlow, getPlatformFlow } from '@/api/platform/flow'
import { listDevice, listThingsModel, getDevice, listDeviceShort } from '@/api/iot/device'
import { listGateway } from '@/api/iot/gateway'
import { getDicts } from '@/api/system/dict/data'
@ -283,7 +293,7 @@ export default {
subMappingCache: {},
sensorTypeDictOptions: [],
queryParams: { pageNum: 1, pageSize: 20, platformId: null, serialNumber: '', sensorType: '', netStatus: null },
form: { enable: '1', deviceType: 0, reportIntervalSec: 300 },
form: { enable: '1', deviceType: 0, reportIntervalSec: 300, deviceName: '' },
rules: {
platformId: [{ required: true, message: '请选择平台', trigger: 'change' }],
deviceId: [{ required: true, message: '请选择设备', trigger: 'change' }],
@ -363,6 +373,11 @@ export default {
const sn = device.serialNumber ? `(${device.serialNumber})` : ''
return `${name}${sn} - ${typeText}`
},
subDeviceOptionLabel(device) {
const name = device.deviceName || device.serialNumber
const sn = device.serialNumber && device.deviceName ? `(${device.serialNumber})` : ''
return `${name}${sn}`
},
sensorLabel(v) {
const option = this.sensorTypeOptions.find(item => item.value === v)
return option ? option.label : v
@ -378,7 +393,7 @@ export default {
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
handleAdd() {
this.form = { enable: '1', deviceType: 0, pushInterfaceId: null, reportIntervalSec: 300 }
this.form = { enable: '1', deviceType: 0, pushInterfaceId: null, reportIntervalSec: 300, deviceName: '' }
this.fieldMappingRows = []
this.subMappingRows = []
this.itemFieldMappingRows = []
@ -389,7 +404,7 @@ export default {
this.title = '新增设备绑定'; this.open = true
},
handleUpdate(row) {
this.form = { ...row, reportIntervalSec: 300 }
this.form = { ...row, deviceName: row.deviceName || '', reportIntervalSec: 300 }
this.fieldMappingRows = this.safeParseJson(row.fieldMapping, [])
this.subMappingRows = []
this.itemFieldMappingRows = []
@ -408,7 +423,7 @@ export default {
// 加载子设备映射(网关设备)
if (row.deviceType === 1 && row.id) {
listSubMappings(row.id).then(res => {
this.subMappingRows = (res.data || []).map(s => ({ ...s }))
this.subMappingRows = (res.data || []).map(s => ({ ...s, subDeviceName: s.subDeviceName || s.subSerialNumber }))
})
}
this.loadDeviceContext(row.deviceId)
@ -438,6 +453,7 @@ export default {
this.subMappingRows = this.subDeviceOptions.map(device => {
return {
subSerialNumber: device.serialNumber,
subDeviceName: device.deviceName || device.serialNumber,
deviceRole: 'monitor',
onboardConfig: JSON.stringify({ sensorId: device.serialNumber }),
platformDeviceId: '',
@ -489,9 +505,11 @@ export default {
const d = this.deviceOptions.find(x => x.deviceId === deviceId)
if (d) {
this.form.serialNumber = d.serialNumber
this.form.deviceName = d.deviceName || d.serialNumber
this.loadSubDeviceOptions(d)
} else {
this.form.serialNumber = ''
this.form.deviceName = ''
this.subDeviceOptions = []
this.thingsModelOptions = []
}
@ -537,6 +555,7 @@ export default {
this.deviceOptions = [{ deviceId: device.deviceId, deviceName: device.deviceName, serialNumber: device.serialNumber }]
this.form.deviceId = device.deviceId
this.form.serialNumber = device.serialNumber
this.form.deviceName = device.deviceName || device.serialNumber
this.loadThingsModel(device.deviceId)
this.loadSubDeviceOptions(device)
}).catch(() => {
@ -577,6 +596,7 @@ export default {
this.subMappingRows = this.subDeviceOptions.map(device => {
return {
subSerialNumber: device.serialNumber,
subDeviceName: device.deviceName || device.serialNumber,
deviceRole: 'monitor',
onboardConfig: JSON.stringify({ sensorId: device.serialNumber }),
platformDeviceId: '',
@ -594,7 +614,7 @@ export default {
},
addFieldMapping() { this.fieldMappingRows.push({ identifier: '', platformField: '' }) },
addSubMapping() {
this.subMappingRows.push({ subSerialNumber: '', deviceRole: 'monitor', onboardConfig: '', platformDeviceId: '', platformPointId: '' })
this.subMappingRows.push({ subSerialNumber: '', subDeviceName: '', deviceRole: 'monitor', onboardConfig: '', platformDeviceId: '', platformPointId: '' })
},
onSubRoleChange(row) {
if (row.deviceRole === 'reference') {
@ -608,7 +628,13 @@ export default {
},
onSubSerialChange(row) {
if (!row.subSerialNumber) return
row.onboardConfig = JSON.stringify({ sensorId: row.subSerialNumber })
const selected = this.subDeviceOptions.find(device => device.serialNumber === row.subSerialNumber)
if (selected) {
row.subDeviceName = selected.deviceName || selected.serialNumber
}
const config = this.safeParseJson(row.onboardConfig, {})
config.sensorId = config.sensorId || row.subSerialNumber
row.onboardConfig = JSON.stringify(config)
this.refreshSubMappingOnboardConfigs()
},
findReferenceRow(rows = this.subMappingRows) {
@ -693,8 +719,9 @@ export default {
// 网关设备:保存后同步子映射
if (this.form.deviceType === 1 && mappingId) {
const subData = normalizedSubRows
.map(r => ({
.map(r => ({
subSerialNumber: r.subSerialNumber,
subDeviceName: r.subDeviceName || r.subSerialNumber,
deviceRole: r.deviceRole || 'monitor',
onboardConfig: r.onboardConfig || null,
platformDeviceId: r.platformDeviceId || null,

View File

@ -205,7 +205,7 @@
<el-option label="平台测点ID" value="platform_point_id" />
<el-option label="系统设备编号" value="serial_number" />
</el-select>
<el-input v-else-if="scope.row.source === 'onboard_config'" v-model="scope.row.field" size="mini" placeholder="入网参数字段,如 sensorDepths" />
<el-input v-else-if="scope.row.source === 'onboard_config'" v-model="scope.row.value" size="mini" placeholder="入网参数,数组值与子设备数量相等" />
<span v-else-if="scope.row.source === 'uuid'" class="schema-hint">每次请求自动生成UUID</span>
<span v-else-if="scope.row.source === 'subDeviceSensorIds'" class="schema-hint">自动取设备绑定页子设备映射中的 sensorId 列表</span>
<span v-else-if="scope.row.source === 'referenceSubDeviceSensorIdPairs'" class="schema-hint">按基准点与每个监测点成对拆分请求</span>
@ -449,6 +449,27 @@ export default {
return defaultValue
}
},
parseSchemaValue(value) {
if (typeof value !== 'string') return value
const text = value.trim()
if (!text) return ''
if ((text.startsWith('[') && text.endsWith(']')) || (text.startsWith('{') && text.endsWith('}'))) {
return JSON.parse(text)
}
if (/^-?\d+(\.\d+)?$/.test(text)) {
return Number(text)
}
if (text === 'true' || text === 'false') {
return text === 'true'
}
return value
},
stringifySchemaValue(value) {
if (Array.isArray(value) || (value && typeof value === 'object')) {
return JSON.stringify(value)
}
return value == null ? '' : String(value)
},
parseRequestSchema(interfaceId) {
const iface = this.interfaceOptions.find(item => item.id === interfaceId)
return this.safeParseJson(iface && iface.requestSchema, [])
@ -459,7 +480,7 @@ export default {
if (field.source === 'auth') return `认证参数 / ${field.field || ''}`
if (field.source === 'extra') return `平台参数 / ${field.field || ''}`
if (field.source === 'context') return `上一步结果 / ${field.contextKey || ''}`
if (field.source === 'onboard_config') return `入网参数 / ${field.field || ''}`
if (field.source === 'onboard_config') return `入网参数 / ${this.stringifySchemaValue(field.value)}`
if (field.source === 'device') {
const deviceLabels = {
platform_device_id: '平台设备ID',
@ -529,7 +550,7 @@ export default {
name: field.name,
defaultSource: this.describeFieldSource(field),
source: saved.source || '',
value: saved.value !== undefined ? saved.value : '',
value: saved.value !== undefined ? this.stringifySchemaValue(saved.value) : '',
field: saved.field || '',
contextKey: saved.contextKey || ''
}
@ -547,7 +568,8 @@ export default {
if (!row.name || !row.source) return
const item = { source: row.source }
if (row.source === 'fixed') item.value = row.value
if (row.source === 'auth' || row.source === 'extra' || row.source === 'device' || row.source === 'onboard_config') item.field = row.field
if (row.source === 'onboard_config') item.value = this.parseSchemaValue(row.value)
if (row.source === 'auth' || row.source === 'extra' || row.source === 'device') item.field = row.field
if (row.source === 'context') item.contextKey = row.contextKey
mappings[row.name] = item
})

View File

@ -11,6 +11,11 @@
<el-form-item label="设备序列号" prop="serialNumber">
<el-input v-model="queryParams.serialNumber" placeholder="序列号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="设备类型" prop="interfaceCode">
<el-select v-model="queryParams.interfaceCode" placeholder="全部" clearable filterable style="width:140px">
<el-option v-for="s in sensorTypeOptions" :key="s.value" :label="s.label" :value="s.interfaceCode" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:100px">
<el-option label="进行中" :value="0" />
@ -30,6 +35,9 @@
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="selectedIds.length === 0" @click="handleBatchDelete" v-hasPermi="['platform:pushLog:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" @click="handleClean" v-hasPermi="['monitor:logininfor:remove']">{{ $t('operlog.874509-7') }}</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
</el-row>
@ -105,8 +113,11 @@
</template>
<script>
import { listPlatformPushLog, getPlatformPushLog, retryPlatformPushLog, delPlatformPushLog } from '@/api/platform/pushLog'
import { listPlatformPushLog, getPlatformPushLog, retryPlatformPushLog, delPlatformPushLog, cleanPushLog } from '@/api/platform/pushLog'
import { listPlatformInstance } from '@/api/platform/instance'
import { getDicts } from '@/api/system/dict/data'
const SENSOR_TYPE_DICT = 'platform_sensor_type'
export default {
name: 'PlatformPushLog',
@ -117,20 +128,36 @@ export default {
total: 0,
list: [],
platformList: [],
sensorTypeOptions: [],
selectedIds: [],
activeTab: '1',
retryingId: null,
detailOpen: false,
detailTab: 'req',
detail: {},
queryParams: { pageNum: 1, pageSize: 20, platformId: null, serialNumber: '', status: null, logType: 1 }
queryParams: { pageNum: 1, pageSize: 20, platformId: null, serialNumber: '', interfaceCode: '', status: null, logType: 1 }
}
},
created() {
listPlatformInstance({ pageNum: 1, pageSize: 100 }).then(res => { this.platformList = res.rows })
this.loadSensorTypeOptions()
this.getList()
},
methods: {
loadSensorTypeOptions() {
getDicts(SENSOR_TYPE_DICT).then(res => {
this.sensorTypeOptions = ((res && res.data) || []).map(item => {
const value = item.dictValue
return {
value,
label: item.dictLabel || value,
interfaceCode: `dcp_push_${value}`
}
}).filter(item => item.value)
}).catch(() => {
this.sensorTypeOptions = []
})
},
getList() {
this.loading = true
listPlatformPushLog(this.queryParams).then(res => {
@ -169,6 +196,7 @@ export default {
this.retryingId = null
})
},
/** 删除按钮操作 */
handleBatchDelete() {
if (this.selectedIds.length === 0) return
this.$confirm(`确认删除选中的 ${this.selectedIds.length} 条日志?`).then(() => {
@ -179,6 +207,19 @@ export default {
})
})
},
/** 清空按钮操作 */
handleClean() {
this.$modal
.confirm(this.$t('是否确认清空所有推送日志数据项'))
.then(function () {
return cleanPushLog();
})
.then(() => {
this.getList();
this.$modal.msgSuccess(this.$t('operlog.874509-29'));
})
.catch(() => {});
},
formatJson(str) {
if (!str) return ''
try { return JSON.stringify(JSON.parse(str), null, 2) } catch { return str }