20260606:静力水准仪接入平台对接

This commit is contained in:
zhugongping 2026-06-06 16:35:49 +08:00
parent 59072beb11
commit ecc6b1677c
14 changed files with 3009 additions and 371 deletions

View File

@ -94,9 +94,14 @@ CREATE TABLE `platform_interface` (
- `fixed` — 固定值(value 字段)
- `auth` — 从 `auth_config` 取(field 字段指定 key)
- `extra` — 从 `extra_config` 取(初始化后保存的 userId/projectId 等)
- `context` — 从流程上下文取(contextKey 指定,如 `step1.access_token`)
- `context` — 从流程上下文或运行时上下文取(contextKey 指定,如 `step1.access_token`)
- `device` — 从设备绑定关系取(如 `platform_device_id`)
- `thingsModel` — 从设备物模型实时值取(identifier 字段)
- `thingsModel` — 从设备物模型实时值取(设备绑定页选择 identifier)
- `ts` — 自动取设备上报时间
**数组字段补充:**
- `type=array` 的字段通过 `itemSchema` 描述数组元素结构
- `itemSchema.source` 额外支持 `subDeviceId`,用于数组元素内绑定子设备平台 ID(如静力水准 `sensorId`)
### 3.3 `platform_flow` — 流程定义
@ -159,7 +164,9 @@ CREATE TABLE `platform_device_mapping` (
`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 '传感器类型: inclino/stress/hyeto/static_level',
`push_interface_id` bigint DEFAULT NULL COMMENT '推送接口ID,决定设备绑定页动态字段',
`field_mapping` json DEFAULT NULL COMMENT '字段映射 [{identifier,platformField,transform}]',
`extra_mapping` json DEFAULT NULL COMMENT '扩展映射,如数组字段子设备映射/基准点配置',
`net_status` tinyint NOT NULL DEFAULT 0 COMMENT '入网状态(0=未入网,1=已入网)',
`enable` char(1) NOT NULL DEFAULT '1',
`remark` varchar(255) DEFAULT NULL,
@ -281,10 +288,11 @@ before any HTTP call:
```
// 挂接在 RuleProcess.processRuleScript() 调用方之后,异步执行
List<PlatformDeviceMapping> mappings = findBySerialNumber(serialNumber)
// 既匹配主设备 serial_number,也匹配 extra_mapping 中登记的子设备 serial_number
for each mapping where enable=1 and net_status=1:
adapter = getAdapter(mapping.platformId) // 按 platform.code 路由
adapter.pushData(mapping, payload)
// pushData 内部:字段映射转换 → Token检查 → HTTP调用 → 写日志
adapter.pushData(platform, mapping, reportSerialNumber, payload)
// pushData 内部:按 request_schema / itemSchema 动态组装 → Token检查 → HTTP调用 → 写日志
```
### 5.4 DCP 传感器推送策略
@ -294,10 +302,29 @@ for each mapping where enable=1 and net_status=1:
| inclino | POST /api/app/inclino-data/many | xDeg, yDeg, temperature, epoch | 无 |
| stress | POST /api/app/stress-data/many | e, epoch | e 必须严格递增 |
| hyeto | POST /api/app/hyeto-data/many | value, epoch | value 必须严格递增 |
| static_level | POST /api/app/static-level-data/many | sensorData[{height,sensorId,epoch}] | 需两个 sensorId |
| static_level | POST /api/app/static-level-data/many | sensorData[{epoch,height,temperature,sensorId}] | 网关下子设备成对推送 |
递增约束处理:推送前查询 `platform_push_log` 中上次成功推送的值,若当前值 ≤ 上次值则跳过并记录警告。
### 5.5 数组入参通用方案
当前实现不再把静力水准写成硬编码特例,而是抽象成“数组字段推送”:
1. 推送接口在 `platform_interface.request_schema` 中定义 `type=array` 字段及 `itemSchema`
2. 设备绑定页读取所选推送接口的 `request_schema`
3. 标量字段中 `source=thingsModel` 的字段,渲染为普通字段映射
4. 数组字段中 `itemSchema.source=thingsModel` 的字段,渲染为数组项字段映射
5. 数组字段中 `itemSchema.source=subDeviceId` 的字段,渲染为子设备与平台字段映射
6. 运行时由 `DeviceReportTrigger` 按实际上报的子设备序列号命中绑定关系,再由适配器组装完整数组请求体
静力水准是第一类落地场景:
- 绑定的是网关级设备关系,`platform_device_id` 也是网关级
- `extra_mapping.arrayFields[].subDeviceMappings[]` 保存子设备序列号与 DCP `sensorId` 的对应关系
- 其中一个子设备标记为基准点
- 基准点上报时,推送 `[基准点, 基准点]`
- 监测点上报时,若基准点缓存仍在有效期内,则推送 `[基准点, 当前监测点]`
- 若监测点先到,则先暂存,待下一次基准点上报后补推
---
## 6. 前端页面(yack-web)
@ -320,7 +347,13 @@ for each mapping where enable=1 and net_status=1:
- 新增弹窗:
- 设备搜索选择器(输入名称/序列号模糊搜索)
- 传感器类型下拉
- 字段映射:物模型字段下拉(自动加载该设备产品的物模型)+ DCP字段(根据传感器类型预填建议值)
- 推送接口选择器:从页面选择平台下已定义的推送接口
- 字段映射:根据所选推送接口 `request_schema` 动态渲染,不再按传感器类型写死
- 若 `request_schema` 含 `type=array` 字段,则自动出现“数组字段配置”区域
- 数组字段配置包含:
- 子设备序列号 ↔ 平台子设备字段映射(如 `sensorId`)
- 数组元素中 `source=thingsModel` 字段的物模型映射(如 `height`、`temperature`)
- 基准点选择与缓存有效间隔
- 操作:**入网**(执行 flow_type=2 流程,成功后自动回填 DCP deviceId)、启用/禁用
### 推送日志
@ -334,7 +367,7 @@ for each mapping where enable=1 and net_status=1:
系统内置 DCP 平台的接口定义和流程配置(SQL 初始化数据),用户只需填入账号密码即可使用。
**内置接口(6个):**
**内置接口(12个):**
1. `dcp_auth` — POST /connect/token
2. `dcp_get_user` — GET /api/app/user/user-id
3. `dcp_get_projects` — GET /api/app/project/by-user-id/{userId}
@ -343,13 +376,24 @@ for each mapping where enable=1 and net_status=1:
6. `dcp_bind_stress` — POST /api/app/stress-sensor
7. `dcp_bind_hyeto` — POST /api/app/hyeto-sensor
8. `dcp_bind_static_level` — POST /api/app/static-level-device
9. `dcp_push_inclino` — POST /api/app/inclino-data/many
10. `dcp_push_stress` — POST /api/app/stress-data/many
11. `dcp_push_hyeto` — POST /api/app/hyeto-data/many
12. `dcp_push_static_level` — POST /api/app/static-level-data/many
**内置流程(5个):**
**内置流程(6个):**
1. `dcp_init`(flow_type=1):Step1=dcp_auth → Step2=dcp_get_user → Step3=dcp_get_projects
2. `dcp_token_refresh`(flow_type=3):Step1=dcp_auth
3. `dcp_onboard_inclino`(flow_type=2, sensor_type=inclino):Step1=dcp_create_point → Step2=dcp_bind_inclino
4. `dcp_onboard_stress`(flow_type=2, sensor_type=stress):Step1=dcp_create_point → Step2=dcp_bind_stress
5. `dcp_onboard_hyeto`(flow_type=2, sensor_type=hyeto):Step1=dcp_create_point → Step2=dcp_bind_hyeto
6. `dcp_onboard_static_level`(flow_type=2, sensor_type=static_level):Step1=dcp_create_point → Step2=dcp_bind_static_level
补充:
- 上述接口和流程既可通过 SQL 初始化,也可直接在页面维护
- 当前已具备接口管理页、流程编排页、设备绑定页,不需要再靠改 SQL 才能新增运行系统
- 接口入参支持 `uuid` 来源:运行时每次请求生成新的 UUID,避免把 `auto` 字符串直接发送给目标平台
- 接口入参支持 `referenceSubDeviceSensorIdPairs` 来源:按设备绑定页配置的基准点,将子设备拆成 `[基准点, 监测点]` 多个请求批次,适合静力水准等成组设备绑定场景
---

View File

@ -0,0 +1,95 @@
package com.fastbee.iot.platform.adapter.dcp;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
/**
* 数组字段推送缓冲服务
*/
@Slf4j
@Component
public class StaticLevelBufferService {
private static final String REF_PREFIX = "static_level:ref:";
private static final String MONITOR_PREFIX = "static_level:monitor:";
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 保存基准点最新 item 数据
*/
public void saveReferenceData(long platformId, String fieldName, String refSerialNumber,
JSONObject itemData, int reportIntervalSec) {
String key = refKey(platformId, fieldName, refSerialNumber);
JSONObject data = new JSONObject();
data.put("itemData", itemData);
data.put("ts", Instant.now().toEpochMilli());
redisTemplate.opsForValue().set(key, data.toJSONString(), reportIntervalSec * 2L, TimeUnit.SECONDS);
log.debug("[StaticLevelBuffer] 保存基准点缓冲 key={}", key);
}
/**
* 保存监测点最新 item 数据。用于监测点先到、基准点后到的补推场景。
*/
public void saveMonitorData(long platformId, String fieldName, String serialNumber,
JSONObject itemData, JSONObject bodyFields, int reportIntervalSec) {
String key = monitorKey(platformId, fieldName, serialNumber);
JSONObject data = new JSONObject();
data.put("itemData", itemData);
data.put("bodyFields", bodyFields);
data.put("ts", Instant.now().toEpochMilli());
redisTemplate.opsForValue().set(key, data.toJSONString(), reportIntervalSec * 2L, TimeUnit.SECONDS);
log.debug("[StaticLevelBuffer] 保存监测点缓冲 key={}", key);
}
/**
* 获取基准点最新 item 数据,如果超过阈值则返回 null
*/
public JSONObject getReferenceDataIfFresh(long platformId, String fieldName, String refSerialNumber,
int reportIntervalSec) {
return getDataIfFresh(refKey(platformId, fieldName, refSerialNumber), reportIntervalSec, "基准点");
}
/**
* 获取监测点最新 item 数据,如果超过阈值则返回 null。
*/
public JSONObject getMonitorDataIfFresh(long platformId, String fieldName, String serialNumber,
int reportIntervalSec) {
return getDataIfFresh(monitorKey(platformId, fieldName, serialNumber), reportIntervalSec, "监测点");
}
public void removeMonitorData(long platformId, String fieldName, String serialNumber) {
redisTemplate.delete(monitorKey(platformId, fieldName, serialNumber));
}
private JSONObject getDataIfFresh(String key, int reportIntervalSec, String dataName) {
String val = redisTemplate.opsForValue().get(key);
if (val == null) {
return null;
}
JSONObject data = JSON.parseObject(val);
long ts = data.getLongValue("ts", 0L);
long maxStaleMs = (long) (reportIntervalSec * 1.5 * 1000);
if (Instant.now().toEpochMilli() - ts > maxStaleMs) {
log.debug("[StaticLevelBuffer] {}数据已过期 key={}", dataName, key);
return null;
}
return data;
}
private String refKey(long platformId, String fieldName, String serialNumber) {
return REF_PREFIX + platformId + ":" + fieldName + ":" + serialNumber;
}
private String monitorKey(long platformId, String fieldName, String serialNumber) {
return MONITOR_PREFIX + platformId + ":" + fieldName + ":" + serialNumber;
}
}

View File

@ -48,6 +48,9 @@ public class PlatformDeviceMapping implements Serializable {
@ApiModelProperty("字段映射 [{identifier,platformField,transform}]")
private String fieldMapping;
@ApiModelProperty("扩展映射配置,如静力水准子设备编号与DCP sensorId映射")
private String extraMapping;
@ApiModelProperty("入网状态(0=未入网,1=已入网)")
private Integer netStatus;

View File

@ -5,9 +5,13 @@ import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.dtflys.forest.http.ForestRequest;
import com.fastbee.common.core.mq.SubDeviceBo;
import com.fastbee.common.utils.spring.SpringUtils;
import com.fastbee.http.model.HttpClientConfig;
import com.fastbee.http.service.HttpClientFactory;
import com.fastbee.iot.domain.Device;
import com.fastbee.iot.mapper.DeviceMapper;
import com.fastbee.iot.mapper.SubGatewayMapper;
import com.fastbee.iot.platform.domain.*;
import com.fastbee.iot.platform.mapper.PlatformDeviceMappingMapper;
import com.fastbee.iot.platform.mapper.PlatformFlowStepMapper;
@ -40,6 +44,10 @@ public class PlatformFlowExecutor {
private PlatformInstanceMapper instanceMapper;
@Autowired
private IPlatformPushLogService pushLogService;
@Autowired
private SubGatewayMapper subGatewayMapper;
@Autowired
private DeviceMapper deviceMapper;
/**
* 执行流程
@ -62,6 +70,39 @@ public class PlatformFlowExecutor {
private void executeStep(PlatformFlow flow, PlatformFlowStep step,
PlatformInterface iface, FlowContext context) {
long startMs = System.currentTimeMillis();
PlatformPushLog logEntry = createStepLog(flow, step, iface);
try {
Map<String, Object> baseParams = resolveParams(iface, step, context);
List<Map<String, Object>> paramSets = expandBatchParams(iface, step, baseParams);
Object responseData = executeParamSets(flow, step, iface, context, paramSets, logEntry, startMs);
// 7. 将出参写入上下文:
// 优先按 response_schema.path 提取,保证前端新增接口定义后运行链路可以直接消费这些配置。
if (StringUtils.hasText(step.getResultKey()) && responseData != null) {
context.putStepResult(step.getResultKey(), responseData);
}
// 8. 持久化指定出参字段
if (StringUtils.hasText(step.getResultSave())) {
persistResultSave(step, responseData, context);
}
} catch (Exception e) {
if (logEntry.getStatus() == null || logEntry.getStatus() == 0) {
log.error("[PlatformFlow] 步骤执行失败 flowId={} step={} interface={}",
flow.getId(), step.getStepOrder(), iface.getCode(), e);
logEntry.setStatus(2);
logEntry.setErrorMsg(e.getMessage());
logEntry.setCostMs((int) (System.currentTimeMillis() - startMs));
}
throw new RuntimeException("流程步骤执行失败: " + iface.getName() + " - " + e.getMessage(), e);
} finally {
pushLogService.asyncSave(logEntry);
}
}
private PlatformPushLog createStepLog(PlatformFlow flow, PlatformFlowStep step, PlatformInterface iface) {
PlatformPushLog logEntry = new PlatformPushLog();
logEntry.setPlatformId(flow.getPlatformId());
logEntry.setFlowId(flow.getId());
@ -70,16 +111,56 @@ public class PlatformFlowExecutor {
logEntry.setStatus(0);
logEntry.setRetryCount(0);
logEntry.setLogType(2); // 操作日志(认证/入网等流程步骤)
return logEntry;
}
private Object executeParamSets(PlatformFlow flow, PlatformFlowStep step, PlatformInterface iface,
FlowContext context, List<Map<String, Object>> paramSets,
PlatformPushLog firstLog, long stepStartMs) {
if (paramSets == null || paramSets.isEmpty()) {
return null;
}
JSONArray batchResponses = new JSONArray();
Object singleResponse = null;
boolean batchMode = paramSets.size() > 1;
for (int i = 0; i < paramSets.size(); i++) {
PlatformPushLog currentLog = i == 0 ? firstLog : createStepLog(flow, step, iface);
long requestStartMs = System.currentTimeMillis();
try {
// 1. 解析入参
Map<String, Object> params = resolveParams(iface, step, context);
Object responseData = executeHttpOnce(flow, step, iface, context,
new LinkedHashMap<>(paramSets.get(i)), currentLog, requestStartMs);
if (batchMode) {
batchResponses.add(responseData);
} else {
singleResponse = responseData;
}
} catch (Exception e) {
log.error("[PlatformFlow] 步骤执行失败 flowId={} step={} interface={} batchIndex={}",
flow.getId(), step.getStepOrder(), iface.getCode(), i + 1, e);
currentLog.setStatus(2);
currentLog.setErrorMsg(e.getMessage());
currentLog.setCostMs((int) (System.currentTimeMillis() - requestStartMs));
throw new RuntimeException(e.getMessage(), e);
} finally {
if (i > 0) {
pushLogService.asyncSave(currentLog);
}
}
}
if (batchMode) {
firstLog.setCostMs((int) (System.currentTimeMillis() - stepStartMs));
return batchResponses;
}
return singleResponse;
}
// 2. 解析 URL(替换路径变量)
private Object executeHttpOnce(PlatformFlow flow, PlatformFlowStep step, PlatformInterface iface,
FlowContext context, Map<String, Object> params,
PlatformPushLog logEntry, long startMs) {
String url = resolvePath(context.getPlatform().getBaseUrl() + iface.getPath(), params);
logEntry.setRequestUrl(url);
// 3. 构建请求
String method = iface.getMethod().toUpperCase();
String contentType = StringUtils.hasText(iface.getContentType())
? iface.getContentType() : "application/json";
@ -93,14 +174,12 @@ public class PlatformFlowExecutor {
headers.put("Content-Type", contentType);
if ("GET".equals(method)) {
// GET:移除路径变量后剩余参数作为 query string
removePathVars(params, iface.getPath());
if (!params.isEmpty()) {
builder.querys(params);
}
logEntry.setRequestBody(JSON.toJSONString(params));
} else if (isFormUrlEncoded) {
// form-urlencoded:移除路径变量后,拼接为 key=value&key=value
removePathVars(params, iface.getPath());
String formBody = params.entrySet().stream()
.filter(e -> e.getValue() != null)
@ -116,66 +195,32 @@ public class PlatformFlowExecutor {
builder.body(formBody);
logEntry.setRequestBody(formBody);
} else {
// application/json
removePathVars(params, iface.getPath());
String body = JSON.toJSONString(params);
builder.body(body);
logEntry.setRequestBody(body);
}
// 4. 注入 Token
String token = resolveToken(flow, step, context);
if (StringUtils.hasText(token)) {
headers.put("Authorization", "Bearer " + token);
}
builder.headers(headers);
// 5. 校验路径变量是否全部替换(Forest 会把 {xxx} 当模板变量解析,必须提前替换完)
if (url.contains("{") || url.contains("}")) {
throw new RuntimeException("URL路径变量未解析: " + url + ",请检查步骤参数配置");
}
// 6. 执行请求
ForestRequest<?> request = HttpClientFactory.instance(builder.build());
String responseBody = request.executeAsString();
int responseCode = request.getResponseEncode() != null ? Integer.parseInt(request.getResponseEncode()) : 200;
Object responseData = buildResponseData(iface, responseBody);
logEntry.setResponseBody(responseBody);
logEntry.setResponseCode(responseCode);
logEntry.setStatus(1);
logEntry.setCostMs((int) (System.currentTimeMillis() - startMs));
// 7. 将出参写入上下文(数组响应取第一个元素)
if (StringUtils.hasText(step.getResultKey()) && StringUtils.hasText(responseBody)) {
String trimmed = responseBody.trim();
if (trimmed.startsWith("[")) {
JSONArray arr = JSON.parseArray(trimmed);
context.putStepResult(step.getResultKey(),
(arr != null && !arr.isEmpty()) ? arr.getJSONObject(0) : null);
} else {
try {
context.putStepResult(step.getResultKey(), JSON.parseObject(trimmed));
} catch (Exception e) {
context.putStepResult(step.getResultKey(), trimmed);
}
}
}
// 8. 持久化指定出参字段
if (StringUtils.hasText(step.getResultSave())) {
persistResultSave(step, responseBody, context);
}
} catch (Exception e) {
log.error("[PlatformFlow] 步骤执行失败 flowId={} step={} interface={}",
flow.getId(), step.getStepOrder(), iface.getCode(), e);
logEntry.setStatus(2);
logEntry.setErrorMsg(e.getMessage());
logEntry.setCostMs((int) (System.currentTimeMillis() - startMs));
throw new RuntimeException("流程步骤执行失败: " + iface.getName() + " - " + e.getMessage(), e);
} finally {
pushLogService.asyncSave(logEntry);
}
return responseData;
}
/**
@ -231,6 +276,8 @@ public class PlatformFlowExecutor {
switch (source) {
case "fixed":
return fd.get("value");
case "uuid":
return newGeneratedId();
case "auth":
return authConfig.get(fd.getString("field"));
case "extra":
@ -244,11 +291,309 @@ public class PlatformFlowExecutor {
if ("platform_point_id".equals(df)) return ctx.getDeviceMapping().getPlatformPointId();
if ("serial_number".equals(df)) return ctx.getDeviceMapping().getSerialNumber();
return null;
case "subDeviceSensorIds":
return collectSubDeviceSensorIds(ctx.getDeviceMapping());
case "referenceSubDeviceSensorIdPairs":
return collectReferenceSubDeviceSensorIdPairs(ctx.getDeviceMapping());
default:
return null;
}
}
private String newGeneratedId() {
return UUID.randomUUID().toString();
}
private List<Map<String, Object>> expandBatchParams(PlatformInterface iface, PlatformFlowStep step,
Map<String, Object> baseParams) {
Map.Entry<String, Object> batchEntry = findBatchParam(baseParams);
List<Map<String, Object>> paramSets = new ArrayList<>();
if (batchEntry == null) {
Map<String, Object> params = new LinkedHashMap<>(baseParams);
refreshGeneratedParams(iface, step, params);
paramSets.add(params);
return paramSets;
}
List<?> batches = toList(batchEntry.getValue());
for (Object batchItem : batches) {
Map<String, Object> params = new LinkedHashMap<>(baseParams);
params.put(batchEntry.getKey(), normalizeBatchValue(batchItem));
refreshGeneratedParams(iface, step, params);
paramSets.add(params);
}
return paramSets;
}
private Map.Entry<String, Object> findBatchParam(Map<String, Object> params) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (isListOfLists(entry.getValue())) {
return entry;
}
}
return null;
}
private boolean isListOfLists(Object value) {
List<?> list = toList(value);
if (list == null || list.isEmpty()) {
return false;
}
Object first = list.get(0);
return toList(first) != null;
}
private List<?> toList(Object value) {
if (value instanceof List) {
return (List<?>) value;
}
if (value instanceof JSONArray) {
return (JSONArray) value;
}
return null;
}
private Object normalizeBatchValue(Object value) {
List<?> list = toList(value);
if (list == null) {
return value;
}
return new ArrayList<>(list);
}
private void refreshGeneratedParams(PlatformInterface iface, PlatformFlowStep step, Map<String, Object> params) {
for (String fieldName : resolveGeneratedFieldNames(iface, step)) {
params.put(fieldName, newGeneratedId());
}
}
private Set<String> resolveGeneratedFieldNames(PlatformInterface iface, PlatformFlowStep step) {
LinkedHashMap<String, String> sources = new LinkedHashMap<>();
try {
if (iface != null && StringUtils.hasText(iface.getRequestSchema())) {
JSONArray schema = JSON.parseArray(iface.getRequestSchema());
for (int i = 0; i < schema.size(); i++) {
JSONObject field = schema.getJSONObject(i);
if (field != null && StringUtils.hasText(field.getString("name"))) {
sources.put(field.getString("name"), field.getString("source"));
}
}
}
if (step != null && StringUtils.hasText(step.getParamMapping())) {
JSONObject overrides = JSON.parseObject(step.getParamMapping());
for (String fieldName : overrides.keySet()) {
JSONObject override = overrides.getJSONObject(fieldName);
if (override != null && StringUtils.hasText(override.getString("source"))) {
sources.put(fieldName, override.getString("source"));
}
}
}
} catch (Exception e) {
log.warn("[PlatformFlow] 解析自动生成参数失败 interfaceId={}, error={}",
iface == null ? null : iface.getId(), e.getMessage());
}
return sources.entrySet().stream()
.filter(entry -> "uuid".equals(entry.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
private List<String> collectSubDeviceSensorIds(PlatformDeviceMapping mapping) {
return collectSubDeviceSensorBindings(mapping).stream()
.map(item -> item.sensorId)
.filter(StringUtils::hasText)
.distinct()
.collect(Collectors.toList());
}
/**
* 静力水准类成组设备按“基准点 + 监测点”执行绑定。
* 返回二维数组后由通用批量参数展开逻辑拆成多次 HTTP 请求。
*/
private List<List<String>> collectReferenceSubDeviceSensorIdPairs(PlatformDeviceMapping mapping) {
List<SubDeviceSensorBinding> bindings = collectSubDeviceSensorBindings(mapping);
if (bindings.size() < 2) {
return Collections.emptyList();
}
SubDeviceSensorBinding reference = bindings.stream()
.filter(item -> item.reference)
.findFirst()
.orElse(bindings.get(0));
List<List<String>> pairs = new ArrayList<>();
for (SubDeviceSensorBinding binding : bindings) {
if (sameText(reference.serialNumber, binding.serialNumber)
|| !StringUtils.hasText(reference.sensorId)
|| !StringUtils.hasText(binding.sensorId)) {
continue;
}
pairs.add(Arrays.asList(reference.sensorId, binding.sensorId));
}
return pairs;
}
private List<SubDeviceSensorBinding> collectSubDeviceSensorBindings(PlatformDeviceMapping mapping) {
if (mapping == null) {
return Collections.emptyList();
}
LinkedHashMap<String, SubDeviceSensorBinding> bindings = new LinkedHashMap<>();
String referenceSerialNumber = appendConfiguredSubDeviceBindings(bindings, mapping);
appendGatewaySubDeviceBindings(bindings, mapping);
if (!StringUtils.hasText(referenceSerialNumber) && !bindings.isEmpty()) {
referenceSerialNumber = bindings.keySet().iterator().next();
}
if (StringUtils.hasText(referenceSerialNumber) && bindings.containsKey(referenceSerialNumber)) {
bindings.get(referenceSerialNumber).reference = true;
}
return new ArrayList<>(bindings.values());
}
private String appendConfiguredSubDeviceBindings(LinkedHashMap<String, SubDeviceSensorBinding> bindings,
PlatformDeviceMapping mapping) {
if (!StringUtils.hasText(mapping.getExtraMapping())) {
return null;
}
try {
JSONObject extra = JSON.parseObject(mapping.getExtraMapping());
String referenceSerialNumber = null;
JSONArray arrayFields = extra.getJSONArray("arrayFields");
if (arrayFields != null) {
for (int i = 0; i < arrayFields.size(); i++) {
JSONObject arrayField = arrayFields.getJSONObject(i);
referenceSerialNumber = appendConfiguredMappingRows(bindings,
arrayField == null ? null : arrayField.getJSONArray("subDeviceMappings"),
referenceSerialNumber);
}
}
JSONObject staticLevel = extra.getJSONObject("staticLevel");
if (staticLevel != null) {
referenceSerialNumber = normalizeText(staticLevel.getString("referenceSerialNumber"));
referenceSerialNumber = appendConfiguredMappingRows(
bindings, staticLevel.getJSONArray("sensorIdMappings"), referenceSerialNumber);
}
return referenceSerialNumber;
} catch (Exception e) {
log.warn("[PlatformFlow] 解析子设备传感器列表失败 mappingId={}, error={}",
mapping.getId(), e.getMessage());
return null;
}
}
private String appendConfiguredMappingRows(LinkedHashMap<String, SubDeviceSensorBinding> bindings,
JSONArray mappings, String referenceSerialNumber) {
if (mappings == null || mappings.isEmpty()) {
return referenceSerialNumber;
}
String ref = referenceSerialNumber;
for (int i = 0; i < mappings.size(); i++) {
JSONObject item = mappings.getJSONObject(i);
if (item == null) {
continue;
}
String serialNumber = normalizeText(item.getString("serialNumber"));
if (!StringUtils.hasText(serialNumber)) {
continue;
}
String sensorId = normalizeText(item.getString("sensorId"));
upsertSubDeviceBinding(bindings, serialNumber,
StringUtils.hasText(sensorId) ? sensorId : serialNumber);
if (Boolean.TRUE.equals(item.getBoolean("isReference"))) {
ref = serialNumber;
}
}
return ref;
}
private void appendGatewaySubDeviceBindings(LinkedHashMap<String, SubDeviceSensorBinding> bindings,
PlatformDeviceMapping mapping) {
String gatewaySerialNumber = resolveGatewaySerialNumber(mapping);
if (!StringUtils.hasText(gatewaySerialNumber)) {
return;
}
try {
List<SubDeviceBo> relationRows = subGatewayMapper.getSubDeviceListByGw(gatewaySerialNumber);
if (relationRows != null) {
for (SubDeviceBo subDevice : relationRows) {
appendGatewaySubDeviceBinding(bindings, subDevice == null ? null : subDevice.getSubDeviceNo());
}
}
} catch (Exception e) {
log.warn("[PlatformFlow] 查询网关子设备关系失败 gateway={}, error={}",
gatewaySerialNumber, e.getMessage());
}
try {
Device query = new Device();
query.setGwDevCode(gatewaySerialNumber);
List<Device> deviceRows = deviceMapper.selectDeviceList(query);
if (deviceRows != null) {
for (Device device : deviceRows) {
appendGatewaySubDeviceBinding(bindings, device == null ? null : device.getSerialNumber());
}
}
} catch (Exception e) {
log.warn("[PlatformFlow] 查询网关子设备列表失败 gateway={}, error={}",
gatewaySerialNumber, e.getMessage());
}
}
private void appendGatewaySubDeviceBinding(LinkedHashMap<String, SubDeviceSensorBinding> bindings,
String serialNumber) {
String normalizedSerialNumber = normalizeText(serialNumber);
if (!StringUtils.hasText(normalizedSerialNumber)) {
return;
}
upsertSubDeviceBinding(bindings, normalizedSerialNumber, normalizedSerialNumber);
}
private void upsertSubDeviceBinding(LinkedHashMap<String, SubDeviceSensorBinding> bindings,
String serialNumber, String sensorId) {
SubDeviceSensorBinding binding = bindings.computeIfAbsent(serialNumber, SubDeviceSensorBinding::new);
if (StringUtils.hasText(sensorId)) {
binding.sensorId = sensorId;
}
}
private String resolveGatewaySerialNumber(PlatformDeviceMapping mapping) {
String serialNumber = normalizeText(mapping.getSerialNumber());
if (StringUtils.hasText(serialNumber)) {
return serialNumber;
}
if (mapping.getDeviceId() == null) {
return null;
}
try {
Device device = deviceMapper.selectDeviceByDeviceId(mapping.getDeviceId());
return device == null ? null : normalizeText(device.getSerialNumber());
} catch (Exception e) {
log.warn("[PlatformFlow] 查询网关设备编号失败 deviceId={}, error={}",
mapping.getDeviceId(), e.getMessage());
return null;
}
}
private boolean sameText(String left, String right) {
return Objects.equals(normalizeText(left), normalizeText(right));
}
private String normalizeText(String value) {
return value == null ? null : value.trim();
}
private static class SubDeviceSensorBinding {
private final String serialNumber;
private String sensorId;
private boolean reference;
private SubDeviceSensorBinding(String serialNumber) {
this.serialNumber = serialNumber;
this.sensorId = serialNumber;
}
}
/** 替换 URL 路径变量,如 /project/by-user-id/{userId} */
private String resolvePath(String url, Map<String, Object> params) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
@ -270,20 +615,170 @@ public class PlatformFlowExecutor {
}
}
private Object buildResponseData(PlatformInterface iface, String responseBody) {
if (!StringUtils.hasText(responseBody)) {
return null;
}
Object parsed = parseResponseBody(responseBody);
if (parsed == null) {
return null;
}
JSONObject extracted = extractResponseBySchema(iface, parsed);
if (!extracted.isEmpty()) {
return extracted;
}
if (parsed instanceof JSONArray) {
JSONArray arr = (JSONArray) parsed;
return (arr != null && !arr.isEmpty()) ? arr.get(0) : null;
}
return parsed;
}
private Object parseResponseBody(String responseBody) {
String trimmed = responseBody == null ? null : responseBody.trim();
if (!StringUtils.hasText(trimmed)) {
return null;
}
try {
if (trimmed.startsWith("[")) {
return JSON.parseArray(trimmed);
}
if (trimmed.startsWith("{")) {
return JSON.parseObject(trimmed);
}
} catch (Exception e) {
log.warn("[PlatformFlow] 解析响应JSON失败: {}", e.getMessage());
}
return trimmed;
}
private JSONObject extractResponseBySchema(PlatformInterface iface, Object parsedResponse) {
JSONObject extracted = new JSONObject();
if (iface == null || !StringUtils.hasText(iface.getResponseSchema()) || parsedResponse == null) {
return extracted;
}
try {
JSONArray schema = JSON.parseArray(iface.getResponseSchema());
for (int i = 0; i < schema.size(); i++) {
JSONObject field = schema.getJSONObject(i);
if (field == null || !StringUtils.hasText(field.getString("name"))) {
continue;
}
Object value = readPathValue(parsedResponse, field.getString("path"));
if (value != null) {
extracted.put(field.getString("name"), value);
}
}
} catch (Exception e) {
log.warn("[PlatformFlow] 解析 response_schema 失败, interfaceId={}, error={}",
iface.getId(), e.getMessage());
}
return extracted;
}
private Object readPathValue(Object root, String path) {
if (root == null || !StringUtils.hasText(path)) {
return null;
}
Object current = root;
for (String segment : splitPath(path)) {
if (current == null) {
return null;
}
current = readSegmentValue(current, segment);
}
return current;
}
private List<String> splitPath(String path) {
String normalized = path.trim();
if (normalized.startsWith("$.")) {
normalized = normalized.substring(2);
} else if ("$".equals(normalized)) {
normalized = "";
} else if (normalized.startsWith("$")) {
normalized = normalized.substring(1);
}
List<String> segments = new ArrayList<>();
StringBuilder current = new StringBuilder();
int bracketDepth = 0;
for (int i = 0; i < normalized.length(); i++) {
char ch = normalized.charAt(i);
if (ch == '.' && bracketDepth == 0) {
if (current.length() > 0) {
segments.add(current.toString());
current.setLength(0);
}
continue;
}
if (ch == '[') {
bracketDepth++;
} else if (ch == ']') {
bracketDepth = Math.max(0, bracketDepth - 1);
}
current.append(ch);
}
if (current.length() > 0) {
segments.add(current.toString());
}
return segments;
}
private Object readSegmentValue(Object current, String segment) {
if (!StringUtils.hasText(segment)) {
return current;
}
int bracketIndex = segment.indexOf('[');
String fieldName = bracketIndex >= 0 ? segment.substring(0, bracketIndex) : segment;
Object value = current;
if (StringUtils.hasText(fieldName)) {
if (value instanceof JSONObject) {
value = ((JSONObject) value).get(fieldName);
} else if (value instanceof Map) {
value = ((Map<?, ?>) value).get(fieldName);
} else {
return null;
}
}
int cursor = bracketIndex >= 0 ? bracketIndex : segment.length();
while (cursor >= 0 && cursor < segment.length()) {
int left = segment.indexOf('[', cursor);
if (left < 0) {
break;
}
int right = segment.indexOf(']', left);
if (right < 0) {
return null;
}
String indexText = segment.substring(left + 1, right).trim();
if (!StringUtils.hasText(indexText)) {
return null;
}
int index;
try {
index = Integer.parseInt(indexText);
} catch (NumberFormatException e) {
return null;
}
if (value instanceof JSONArray) {
JSONArray arr = (JSONArray) value;
value = (index >= 0 && index < arr.size()) ? arr.get(index) : null;
} else if (value instanceof List) {
List<?> list = (List<?>) value;
value = (index >= 0 && index < list.size()) ? list.get(index) : null;
} else {
return null;
}
cursor = right + 1;
}
return value;
}
/** 将 result_save 中指定字段持久化到 platform_instance.extra_config 或 platform_device_mapping */
private void persistResultSave(PlatformFlowStep step, String responseBody, FlowContext context) {
private void persistResultSave(PlatformFlowStep step, Object responseData, FlowContext context) {
try {
JSONArray saves = JSON.parseArray(step.getResultSave());
// 响应可能是数组(如项目列表),取第一个元素
JSONObject response;
String trimmed = responseBody.trim();
if (trimmed.startsWith("[")) {
JSONArray arr = JSON.parseArray(trimmed);
if (arr == null || arr.isEmpty()) return;
response = arr.getJSONObject(0);
} else {
response = JSON.parseObject(trimmed);
}
JSONObject response = toJsonObject(responseData);
if (response == null) return;
PlatformInstance platform = context.getPlatform();
@ -360,6 +855,31 @@ public class PlatformFlowExecutor {
}
}
private JSONObject toJsonObject(Object data) {
if (data == null) {
return null;
}
if (data instanceof JSONObject) {
return (JSONObject) data;
}
if (data instanceof JSONArray) {
JSONArray arr = (JSONArray) data;
if (arr.isEmpty()) {
return null;
}
Object first = arr.get(0);
return toJsonObject(first);
}
if (data instanceof Map) {
return new JSONObject((Map<String, Object>) data);
}
try {
return JSON.parseObject(JSON.toJSONString(data));
} catch (Exception e) {
return null;
}
}
/**
* 解析当前步骤应使用的 Token:
* - flow_type=3(Token刷新):不注入,避免循环

View File

@ -39,6 +39,7 @@ public class PlatformDeviceMappingServiceImpl extends ServiceImpl<PlatformDevice
.eq(query.getPlatformId() != null, PlatformDeviceMapping::getPlatformId, query.getPlatformId())
.eq(query.getDeviceId() != null, PlatformDeviceMapping::getDeviceId, query.getDeviceId())
.eq(StringUtils.hasText(query.getSensorType()), PlatformDeviceMapping::getSensorType, query.getSensorType())
.eq(query.getNetStatus() != null, PlatformDeviceMapping::getNetStatus, query.getNetStatus())
.eq(StringUtils.hasText(query.getEnable()), PlatformDeviceMapping::getEnable, query.getEnable())
.orderByDesc(PlatformDeviceMapping::getCreateTime);
return this.list(wrapper);

View File

@ -48,7 +48,7 @@ public class DeviceReportTrigger {
if (!"1".equals(mapping.getEnable())) continue;
PlatformInstance platform = instanceService.getById(mapping.getPlatformId());
if (platform == null || !"1".equals(platform.getEnable())) continue;
dispatchPush(platform, mapping, payload);
dispatchPush(platform, mapping, serialNumber, payload);
}
} catch (Exception e) {
log.error("[DeviceReportTrigger] 推送异常 serialNumber={}", serialNumber, e);
@ -56,10 +56,10 @@ public class DeviceReportTrigger {
}
/** 根据平台 code 路由到对应 Adapter */
private void dispatchPush(PlatformInstance platform, PlatformDeviceMapping mapping, String payload) {
private void dispatchPush(PlatformInstance platform, PlatformDeviceMapping mapping, String reportSerialNumber, String payload) {
switch (platform.getCode()) {
case "dcp":
dcpAdapter.pushData(platform, mapping, payload);
dcpAdapter.pushData(platform, mapping, reportSerialNumber, payload);
break;
default:
log.warn("[DeviceReportTrigger] 未知平台 code: {}", platform.getCode());

View File

@ -12,6 +12,7 @@
<result property="sensorType" column="sensor_type"/>
<result property="pushInterfaceId" column="push_interface_id"/>
<result property="fieldMapping" column="field_mapping"/>
<result property="extraMapping" column="extra_mapping"/>
<result property="netStatus" column="net_status"/>
<result property="enable" column="enable"/>
<result property="remark" column="remark"/>
@ -23,8 +24,14 @@
<select id="selectBySerialNumber" resultMap="PlatformDeviceMappingResult">
SELECT * FROM platform_device_mapping
WHERE serial_number = #{serialNumber}
AND enable = '1'
WHERE enable = '1'
AND (
serial_number = #{serialNumber}
OR JSON_SEARCH(extra_mapping, 'one', #{serialNumber}, NULL,
'$.arrayFields[*].subDeviceMappings[*].serialNumber') IS NOT NULL
OR JSON_SEARCH(extra_mapping, 'one', #{serialNumber}, NULL,
'$.staticLevel.sensorIdMappings[*].serialNumber') IS NOT NULL
)
</select>
</mapper>

View File

@ -73,8 +73,8 @@ VALUES (@dcp_platform_id, '绑定倾角传感器', 'dcp_bind_inclino', 'POST', '
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID,来自创建测点步骤"},
{"name":"deviceType", "type":"number","source":"fixed", "value":5, "required":true,"desc":"设备类型,倾角=5"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"},
{"name":"moduleId", "type":"string","source":"fixed", "value":"auto", "required":true,"desc":"模块ID,auto表示自动生成UUID"},
{"name":"virtualId", "type":"string","source":"fixed", "value":"auto", "required":true,"desc":"虚拟ID,auto表示自动生成UUID"},
{"name":"moduleId", "type":"string","source":"uuid", "required":true,"desc":"模块ID,系统自动生成UUID"},
{"name":"virtualId", "type":"string","source":"uuid", "required":true,"desc":"虚拟ID,系统自动生成UUID"},
{"name":"dH", "type":"number","source":"fixed", "value":0, "required":false,"desc":"高程"}
]',
'[
@ -91,8 +91,8 @@ VALUES (@dcp_platform_id, '绑定轴力计', 'dcp_bind_stress', 'POST', '/api/ap
{"name":"deviceType", "type":"number","source":"fixed", "value":10, "required":true,"desc":"设备类型,轴力计=10"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"},
{"name":"factor", "type":"number","source":"fixed", "value":1, "required":false,"desc":"系数"},
{"name":"moduleId", "type":"string","source":"fixed", "value":"auto", "required":true},
{"name":"virtualId", "type":"string","source":"fixed", "value":"auto", "required":true}
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true}
]',
'[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]',
6, '将轴力计绑定到测点', 'system', NOW());
@ -105,8 +105,8 @@ VALUES (@dcp_platform_id, '绑定雨量计', 'dcp_bind_hyeto', 'POST', '/api/app
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":3, "required":true,"desc":"雨量计=3"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true},
{"name":"moduleId", "type":"string","source":"fixed", "value":"auto", "required":true},
{"name":"virtualId", "type":"string","source":"fixed", "value":"auto", "required":true}
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true}
]',
'[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]',
7, '将雨量计绑定到测点', 'system', NOW());
@ -119,8 +119,9 @@ VALUES (@dcp_platform_id, '绑定静力水准', 'dcp_bind_static_level', 'POST',
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":1002, "required":true,"desc":"静力水准=1002"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true},
{"name":"moduleId", "type":"string","source":"fixed", "value":"auto", "required":true},
{"name":"virtualId", "type":"string","source":"fixed", "value":"auto", "required":true}
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true},
{"name":"sensorIds", "type":"array", "source":"referenceSubDeviceSensorIdPairs","required":true,"desc":"基准点与每个监测点成对绑定"}
]',
'[{"name":"id","type":"string","path":"id","desc":"DCP设备ID"}]',
8, '将静力水准传感器绑定到测点', 'system', NOW());
@ -135,10 +136,10 @@ INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`c
VALUES (@dcp_platform_id, '推送倾角数据', 'dcp_push_inclino', 'POST', '/api/app/inclino-data/many', 'application/json',
'[
{"name":"deviceId", "type":"string","source":"device","field":"platform_device_id","required":true,"desc":"DCP设备ID"},
{"name":"epoch", "type":"string","source":"context","contextKey":"reportTime","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"xDeg", "type":"number","source":"context","contextKey":"xDeg","required":true,"desc":"X轴倾角(度)"},
{"name":"yDeg", "type":"number","source":"context","contextKey":"yDeg","required":true,"desc":"Y轴倾角(度)"},
{"name":"temperature","type":"number","source":"context","contextKey":"temperature","required":false,"desc":"温度"}
{"name":"epoch", "type":"string","source":"ts","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"xDeg", "type":"number","source":"thingsModel","required":true,"desc":"X轴倾角(度)"},
{"name":"yDeg", "type":"number","source":"thingsModel","required":true,"desc":"Y轴倾角(度)"},
{"name":"temperature","type":"number","source":"thingsModel","required":false,"desc":"温度"}
]',
'[]',
9, '推送倾角传感器实时数据到DCP', 'system', NOW());
@ -148,8 +149,8 @@ INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`c
VALUES (@dcp_platform_id, '推送轴力计数据', 'dcp_push_stress', 'POST', '/api/app/stress-data/many', 'application/json',
'[
{"name":"deviceId","type":"string","source":"device","field":"platform_device_id","required":true,"desc":"DCP设备ID"},
{"name":"epoch", "type":"string","source":"context","contextKey":"reportTime","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"e", "type":"number","source":"context","contextKey":"e","required":true,"desc":"累计值(必须递增)"}
{"name":"epoch", "type":"string","source":"ts","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"e", "type":"number","source":"thingsModel","required":true,"desc":"累计值(必须递增)"}
]',
'[]',
10, '推送轴力计实时数据到DCP', 'system', NOW());
@ -159,8 +160,8 @@ INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`c
VALUES (@dcp_platform_id, '推送雨量计数据', 'dcp_push_hyeto', 'POST', '/api/app/hyeto-data/many', 'application/json',
'[
{"name":"deviceId","type":"string","source":"device","field":"platform_device_id","required":true,"desc":"DCP设备ID"},
{"name":"epoch", "type":"string","source":"context","contextKey":"reportTime","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"value", "type":"number","source":"context","contextKey":"value","required":true,"desc":"累计雨量(必须递增)"}
{"name":"epoch", "type":"string","source":"ts","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"value", "type":"number","source":"thingsModel","required":true,"desc":"累计雨量(必须递增)"}
]',
'[]',
11, '推送雨量计实时数据到DCP', 'system', NOW());
@ -170,17 +171,20 @@ INSERT INTO `platform_interface` (`platform_id`,`name`,`code`,`method`,`path`,`c
VALUES (@dcp_platform_id, '推送静力水准数据', 'dcp_push_static_level', 'POST', '/api/app/static-level-data/many', 'application/json',
'[
{"name":"deviceId", "type":"string","source":"device","field":"platform_device_id","required":true,"desc":"DCP设备ID"},
{"name":"epoch", "type":"string","source":"context","contextKey":"reportTime","required":true,"desc":"采集时间,ISO8601格式"},
{"name":"height", "type":"number","source":"context","contextKey":"height","required":true,"desc":"高程"},
{"name":"referHeight","type":"number","source":"context","contextKey":"referHeight","required":false,"desc":"参考高程"},
{"name":"sensorData", "type":"number","source":"context","contextKey":"sensorData","required":false,"desc":"传感器原始数据"}
{"name":"sensorData","type":"array","source":"context","contextKey":"sensorData","required":true,"desc":"静力水准数组","itemSchema":[
{"name":"epoch","type":"string","source":"ts","desc":"采集时间"},
{"name":"height","type":"number","source":"thingsModel","desc":"高程"},
{"name":"temperature","type":"number","source":"thingsModel","desc":"温度"},
{"name":"sensorId","type":"string","source":"subDeviceId","desc":"DCP侧sensorId"}
]},
{"name":"ct","type":"number","source":"fixed","value":1,"required":true,"desc":"采集类型"}
]',
'[]',
12, '推送静力水准实时数据到DCP', 'system', NOW());
-- =============================================
-- 二、流程定义(5个)
-- 二、流程定义(6个)
-- =============================================
-- 流程1:平台初始化(flow_type=1)
@ -288,3 +292,96 @@ VALUES (@flow_hyeto_id, 2,
'step2',
'[{"field":"id","saveTo":"device_mapping.platform_device_id"}]',
'绑定雨量计');
-- 流程6:静力水准设备入网(flow_type=2)
INSERT INTO `platform_flow` (`platform_id`,`name`,`code`,`flow_type`,`sensor_type`,`enable`,`remark`,`create_by`,`create_time`)
VALUES (@dcp_platform_id, 'DCP静力水准入网', 'dcp_onboard_static_level', 2, 'static_level', '1', '创建测点→绑定静力水准', 'system', NOW());
SET @flow_static_level_id = LAST_INSERT_ID();
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_static_level_id, 1,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_create_point' LIMIT 1),
'step1',
'[{"field":"id","saveTo":"device_mapping.platform_point_id"}]',
'创建测点');
INSERT INTO `platform_flow_step` (`flow_id`,`step_order`,`interface_id`,`result_key`,`result_save`,`remark`)
VALUES (@flow_static_level_id, 2,
(SELECT id FROM platform_interface WHERE platform_id=@dcp_platform_id AND code='dcp_bind_static_level' LIMIT 1),
'step2',
'[{"field":"id","saveTo":"device_mapping.platform_device_id"}]',
'绑定静力水准');
-- 更新已落库的平台接口配置,去掉 fixed:auto 和静力水准扁平 sensorIds 配置。
-- 执行前确认平台实例 code,如不是 dcp,请手动替换 @platform_id。
SET @platform_id = (SELECT id FROM platform_instance WHERE code = 'dcp' LIMIT 1);
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true,"desc":"项目ID"},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID,来自创建测点步骤"},
{"name":"deviceType", "type":"number","source":"fixed", "value":5, "required":true,"desc":"设备类型,倾角=5"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"},
{"name":"moduleId", "type":"string","source":"uuid", "required":true,"desc":"模块ID,系统自动生成UUID"},
{"name":"virtualId", "type":"string","source":"uuid", "required":true,"desc":"虚拟ID,系统自动生成UUID"},
{"name":"dH", "type":"number","source":"fixed", "value":0, "required":false,"desc":"高程"}
]',
update_by = 'system',
update_time = NOW()
WHERE platform_id = @platform_id
AND code = 'dcp_bind_inclino';
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true,"desc":"项目ID"},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true,"desc":"测点ID"},
{"name":"deviceType", "type":"number","source":"fixed", "value":10, "required":true,"desc":"设备类型,轴力计=10"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true,"desc":"传感器名称"},
{"name":"factor", "type":"number","source":"fixed", "value":1, "required":false,"desc":"系数"},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true}
]',
update_by = 'system',
update_time = NOW()
WHERE platform_id = @platform_id
AND code = 'dcp_bind_stress';
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":3, "required":true,"desc":"雨量计=3"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true}
]',
update_by = 'system',
update_time = NOW()
WHERE platform_id = @platform_id
AND code = 'dcp_bind_hyeto';
UPDATE platform_interface
SET request_schema = '[
{"name":"projectId", "type":"string","source":"extra", "field":"projectId", "required":true},
{"name":"pointId", "type":"string","source":"context","contextKey":"step1.id","required":true},
{"name":"deviceType", "type":"number","source":"fixed", "value":1002, "required":true,"desc":"静力水准=1002"},
{"name":"name", "type":"string","source":"device", "field":"serial_number","required":true},
{"name":"moduleId", "type":"string","source":"uuid", "required":true},
{"name":"virtualId", "type":"string","source":"uuid", "required":true},
{"name":"sensorIds", "type":"array", "source":"referenceSubDeviceSensorIdPairs","required":true,"desc":"基准点与每个监测点成对绑定"}
]',
update_by = 'system',
update_time = NOW()
WHERE platform_id = @platform_id
AND code = 'dcp_bind_static_level';
SELECT code, request_schema
FROM platform_interface
WHERE platform_id = @platform_id
AND code IN ('dcp_bind_inclino', 'dcp_bind_stress', 'dcp_bind_hyeto', 'dcp_bind_static_level');

View File

@ -98,6 +98,7 @@ CREATE TABLE IF NOT EXISTS `platform_device_mapping` (
`sensor_type` varchar(50) NOT NULL COMMENT '传感器类型: inclino/stress/hyeto/static_level',
`push_interface_id` bigint(20) DEFAULT NULL COMMENT '推送接口ID,用于字段映射参照',
`field_mapping` json DEFAULT NULL COMMENT '字段映射 [{identifier,platformField,transform}]',
`extra_mapping` json DEFAULT NULL COMMENT '扩展映射配置,如静力水准子设备编号与DCP sensorId映射',
`net_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '入网状态(0=未入网,1=已入网)',
`enable` char(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',

View File

@ -98,8 +98,8 @@
<!-- 入参 Schema 可视化表格 -->
<el-divider content-position="left">入参定义(request_schema)</el-divider>
<div class="schema-tip">
定义接口需要的入参字段。<b>source</b> 决定参数值来源:
fixed=固定值、auth=认证配置、extra=平台扩展配置、context=流程上下文(上一步出参)、device=设备信息
定义接口需要的入参字段。参数来源使用业务化名称:
固定值、认证参数、平台参数、上一步结果、当前设备、自动UUID、子设备传感器列表、基准点成对传感器列表、设备物模型、上报时间。
</div>
<el-table :data="requestSchemaRows" size="mini" border style="width:100%;margin-top:8px">
<el-table-column label="字段名" width="130">
@ -118,14 +118,19 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="来源(source)" width="150">
<el-table-column label="参数来源" width="150">
<template slot-scope="scope">
<el-select v-model="scope.row.source" size="mini" style="width:100%" @change="onSourceChange(scope.row)">
<el-option label="fixed 固定值" value="fixed" />
<el-option label="auth 认证配置" value="auth" />
<el-option label="extra 扩展配置" value="extra" />
<el-option label="context 上下文" value="context" />
<el-option label="device 设备信息" value="device" />
<el-option label="固定值" value="fixed" />
<el-option label="认证参数" value="auth" />
<el-option label="平台参数" value="extra" />
<el-option label="上一步结果" value="context" />
<el-option label="当前设备" value="device" />
<el-option label="自动UUID" value="uuid" />
<el-option label="子设备传感器列表" value="subDeviceSensorIds" />
<el-option label="基准点成对传感器列表" value="referenceSubDeviceSensorIdPairs" />
<el-option label="设备物模型" value="thingsModel" />
<el-option label="上报时间" value="ts" />
</el-select>
</template>
</el-table-column>
@ -134,17 +139,28 @@
<!-- fixed: 直接填值 -->
<el-input v-if="scope.row.source === 'fixed'" v-model="scope.row.value" size="mini" placeholder="固定值" />
<!-- auth: 选择认证配置中的字段 -->
<el-input v-else-if="scope.row.source === 'auth'" v-model="scope.row.field" size="mini" placeholder="auth字段名,如 userName" />
<!-- extra: 选择扩展配置中的字段 -->
<el-input v-else-if="scope.row.source === 'extra'" v-model="scope.row.field" size="mini" placeholder="extra字段名,如 projectId" />
<el-select v-else-if="scope.row.source === 'auth' && authFieldOptions.length > 0" v-model="scope.row.field" size="mini" filterable allow-create default-first-option style="width:100%">
<el-option v-for="item in authFieldOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-else-if="scope.row.source === 'auth'" v-model="scope.row.field" size="mini" placeholder="认证参数名,如 userName" />
<!-- extra: 选择平台参数中的字段 -->
<el-select v-else-if="scope.row.source === 'extra' && extraFieldOptions.length > 0" v-model="scope.row.field" size="mini" filterable allow-create default-first-option style="width:100%">
<el-option v-for="item in extraFieldOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-else-if="scope.row.source === 'extra'" v-model="scope.row.field" size="mini" placeholder="平台参数名,如 projectId" />
<!-- context: 上一步结果的key -->
<el-input v-else-if="scope.row.source === 'context'" v-model="scope.row.contextKey" size="mini" placeholder="如 step1.id" />
<el-input v-else-if="scope.row.source === 'context'" v-model="scope.row.contextKey" size="mini" placeholder="流程页可选择,如 step1.id" />
<!-- device: 设备字段 -->
<el-select v-else-if="scope.row.source === 'device'" v-model="scope.row.field" size="mini" style="width:100%">
<el-option label="platform_device_id" value="platform_device_id" />
<el-option label="platform_point_id" value="platform_point_id" />
<el-option label="serial_number" value="serial_number" />
<el-option label="平台设备ID" value="platform_device_id" />
<el-option label="平台测点ID" value="platform_point_id" />
<el-option label="系统设备编号" value="serial_number" />
</el-select>
<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>
<span v-else-if="scope.row.source === 'thingsModel'" class="schema-hint">设备绑定页面选择物模型字段</span>
<span v-else-if="scope.row.source === 'ts'" class="schema-hint">系统自动取设备上报时间</span>
<span v-else class="schema-hint">请先选择来源</span>
</template>
</el-table-column>
@ -158,7 +174,12 @@
<el-input v-model="scope.row.desc" size="mini" placeholder="字段说明" />
</template>
</el-table-column>
<el-table-column label="操作" width="50" align="center">
<el-table-column label="数组项定义" width="100" align="center">
<template slot-scope="scope">
<el-button v-if="scope.row.type === 'array'" size="mini" type="text" icon="el-icon-s-operation" @click="openItemSchemaDialog(scope.row)">数组项</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="requestSchemaRows.splice(scope.$index, 1)" />
</template>
@ -166,6 +187,59 @@
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addRequestField">添加入参字段</el-button>
<!-- 数组项 itemSchema 编辑弹窗 -->
<el-dialog :visible.sync="itemSchemaDialogVisible" title="定义数组项字段" width="650px" append-to-body :close-on-click-modal="false">
<div class="schema-tip" style="margin-bottom:8px">
定义数组中每个元素的字段。可选来源:设备物模型、子设备映射、上报时间、固定值。
</div>
<el-table :data="itemSchemaRows" size="mini" border style="width:100%">
<el-table-column label="字段名" width="130">
<template slot-scope="scope">
<el-input v-model="scope.row.name" size="mini" placeholder="如 height" />
</template>
</el-table-column>
<el-table-column label="类型" width="100">
<template slot-scope="scope">
<el-select v-model="scope.row.type" size="mini" style="width:100%">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="boolean" value="boolean" />
</el-select>
</template>
</el-table-column>
<el-table-column label="参数来源" width="150">
<template slot-scope="scope">
<el-select v-model="scope.row.source" size="mini" style="width:100%">
<el-option label="设备物模型" value="thingsModel" />
<el-option label="子设备映射" value="subDeviceId" />
<el-option label="上报时间" value="ts" />
<el-option label="固定值" value="fixed" />
</el-select>
</template>
</el-table-column>
<el-table-column label="固定值" width="120">
<template slot-scope="scope">
<el-input v-if="scope.row.source === 'fixed'" v-model="scope.row.value" size="mini" placeholder="固定值" />
</template>
</el-table-column>
<el-table-column label="说明" width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.desc" size="mini" placeholder="说明" />
</template>
</el-table-column>
<el-table-column label="操作" width="50" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="itemSchemaRows.splice(scope.$index, 1)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addItemSchemaField">添加字段</el-button>
<div slot="footer">
<el-button @click="itemSchemaDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="confirmItemSchema">确定</el-button>
</div>
</el-dialog>
<!-- 出参 Schema 可视化表格 -->
<el-divider content-position="left">出参定义(response_schema)</el-divider>
<div class="schema-tip">
@ -218,6 +292,7 @@
<script>
import { listPlatformInterface, addPlatformInterface, updatePlatformInterface, delPlatformInterface } from '@/api/platform/interface'
import { getPlatformInstance } from '@/api/platform/instance'
export default {
name: 'PlatformInterfaceDrawer',
@ -235,8 +310,13 @@ export default {
editTitle: '新增接口',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
form: {},
authFieldOptions: [],
extraFieldOptions: [],
requestSchemaRows: [],
responseSchemaRows: [],
itemSchemaDialogVisible: false,
itemSchemaEditingRow: null,
itemSchemaRows: [],
rules: {
name: [{ required: true, message: '请输入接口名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入接口标识', trigger: 'blur' }],
@ -249,6 +329,7 @@ export default {
open() {
this.visible = true
this.editing = false
this.loadPlatformOptions()
this.getList()
},
handleClose(done) {
@ -297,7 +378,7 @@ export default {
if (!json) return []
try {
const arr = JSON.parse(json)
return arr.map(f => ({ ...f, required: !!f.required }))
return arr.map(f => ({ ...f, itemSchema: f.itemSchema || [], required: !!f.required }))
} catch (e) { return [] }
},
handleDelete(row) {
@ -317,18 +398,70 @@ export default {
row.field = undefined
row.contextKey = undefined
},
loadPlatformOptions() {
if (!this.platformId) {
this.authFieldOptions = []
this.extraFieldOptions = []
return
}
getPlatformInstance(this.platformId).then(res => {
const data = res.data || {}
this.authFieldOptions = this.extractJsonKeys(data.authConfig)
this.extraFieldOptions = this.extractJsonKeys(data.extraConfig)
}).catch(() => {
this.authFieldOptions = []
this.extraFieldOptions = []
})
},
extractJsonKeys(value) {
if (!value) return []
try {
return Object.keys(JSON.parse(value))
} catch (e) {
return []
}
},
addRequestField() {
this.requestSchemaRows.push({ name: '', type: 'string', source: 'fixed', value: '', field: '', contextKey: '', required: false, desc: '' })
this.requestSchemaRows.push({ name: '', type: 'string', source: 'fixed', value: '', field: '', contextKey: '', required: false, desc: '', itemSchema: [] })
},
addResponseField() {
this.responseSchemaRows.push({ name: '', type: 'string', path: '', desc: '' })
},
openItemSchemaDialog(row) {
this.itemSchemaEditingRow = row
this.itemSchemaRows = (row.itemSchema || []).map(s => ({ ...s }))
if (this.itemSchemaRows.length === 0) {
// 预填一个默认字段
this.itemSchemaRows.push({ name: '', type: 'string', source: 'thingsModel', value: '', desc: '' })
}
this.itemSchemaDialogVisible = true
},
addItemSchemaField() {
this.itemSchemaRows.push({ name: '', type: 'string', source: 'thingsModel', value: '', desc: '' })
},
confirmItemSchema() {
if (this.itemSchemaEditingRow) {
this.itemSchemaEditingRow.itemSchema = this.itemSchemaRows.filter(s => s.name).map(s => {
const item = { name: s.name, type: s.type || 'string', source: s.source || 'thingsModel', desc: s.desc }
if (s.source === 'fixed') item.value = s.value
return Object.assign({}, item)
})
}
this.itemSchemaDialogVisible = false
},
submitForm() {
this.$refs.ifaceForm.validate(valid => {
if (!valid) return
// 将表格行序列化为 JSON 存入 form
this.form.requestSchema = JSON.stringify(this.requestSchemaRows.filter(r => r.name).map(r => {
const field = { name: r.name, type: r.type, source: r.source, required: !!r.required, desc: r.desc }
if (r.type === 'array' && r.itemSchema && r.itemSchema.length > 0) {
field.itemSchema = r.itemSchema.filter(s => s.name).map(s => {
const item = { name: s.name, type: s.type || 'string', source: s.source || 'thingsModel', desc: s.desc }
if (s.source === 'fixed') item.value = s.value
return item
})
}
if (r.source === 'fixed') field.value = r.value
if (r.source === 'auth' || r.source === 'extra' || r.source === 'device') field.field = r.field
if (r.source === 'context') field.contextKey = r.contextKey

View File

@ -8,9 +8,9 @@
<el-option v-for="p in platformList" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="传感器类型" prop="sensorType">
<el-select v-model="queryParams.sensorType" placeholder="全部" clearable style="width:120px">
<el-option v-for="s in sensorTypes" :key="s.value" :label="s.label" :value="s.value" />
<el-form-item label="设备类型" prop="sensorType">
<el-select v-model="queryParams.sensorType" placeholder="全部" clearable filterable allow-create default-first-option style="width:140px">
<el-option v-for="s in sensorTypeOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="入网状态" prop="netStatus">
@ -42,7 +42,7 @@
<template slot-scope="scope">{{ deviceName(scope.row.deviceId) }}</template>
</el-table-column>
<el-table-column label="序列号" prop="serialNumber" width="140" />
<el-table-column label="传感器类型" width="110" align="center">
<el-table-column label="设备类型" width="110" align="center">
<template slot-scope="scope">
<el-tag size="small">{{ sensorLabel(scope.row.sensorType) }}</el-tag>
</template>
@ -74,7 +74,7 @@
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="title" :visible.sync="open" width="750px" append-to-body :close-on-click-modal="false">
<el-dialog :title="title" :visible.sync="open" width="820px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-form-item label="所属平台" prop="platformId">
<el-select v-model="form.platformId" placeholder="请选择平台" style="width:100%" @change="onPlatformChange">
@ -91,9 +91,9 @@
<el-form-item label="序列号">
<el-input v-model="form.serialNumber" disabled />
</el-form-item>
<el-form-item label="传感器类型" prop="sensorType">
<el-select v-model="form.sensorType" placeholder="请选择" style="width:100%">
<el-option v-for="s in sensorTypes" :key="s.value" :label="s.label" :value="s.value" />
<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" />
</el-select>
</el-form-item>
<el-form-item label="推送接口" prop="pushInterfaceId">
@ -108,13 +108,104 @@
<el-input v-model="form.platformPointId" placeholder="入网后自动回填,也可手动填写" />
</el-form-item>
<!-- 字段映射 -->
<el-form-item label="字段映射">
<!-- 数组字段配置:从推送接口的 requestSchema 中解析 array 类型字段自动生成 -->
<div v-if="hasArrayFields" class="array-fields-section">
<div class="section-label">数组字段配置</div>
<el-card v-for="af in arrayFieldsConfig" :key="af.fieldName" shadow="never" style="margin-bottom:12px">
<div slot="header" style="font-weight:600;font-size:14px">
数组字段:{{ af.fieldName }}
<el-tag size="mini" style="margin-left:8px" type="info">接口定义</el-tag>
</div>
<!-- 子设备映射表 -->
<el-form-item label="子设备映射" label-width="100px" style="margin-bottom:0">
<div class="schema-tip" style="margin-bottom:6px;font-size:12px;color:#909399">
当前配置用于<strong>通用平台接入</strong>中的数组设备映射。
先选择当前网关下参与该数组字段上报的子设备,并指定其中一个为基准点。
`{{ subDeviceFieldLabel(af) }}` 默认跟随所选子设备编号,如目标平台要求其他编码,可手动覆盖保存。
<span style="color:#e6a23c">该映射会同时用于入网和后续数组数据推送。</span>
</div>
<el-table :data="af.subDeviceMappings" size="mini" border style="width:100%">
<el-table-column label="子设备编号" min-width="160">
<template slot-scope="scope">
<el-select
v-if="subDeviceOptions.length > 0"
v-model="scope.row.serialNumber"
size="mini"
placeholder="选择子设备"
filterable
style="width:100%"
@change="onSubDeviceSerialChange(af, scope.row)"
>
<el-option v-for="device in subDeviceOptions" :key="device.serialNumber" :label="device.serialNumber" :value="device.serialNumber" />
</el-select>
<el-input v-else v-model="scope.row.serialNumber" size="mini" placeholder="填写子设备编号" @change="onSubDeviceSerialChange(af, scope.row)" />
</template>
</el-table-column>
<el-table-column :label="subDeviceFieldLabel(af)" min-width="220">
<template slot-scope="scope">
<el-input
v-model="scope.row.sensorId"
size="mini"
:placeholder="subDeviceFieldPlaceholder(af)"
/>
</template>
</el-table-column>
<el-table-column label="基准点" width="80" align="center">
<template slot-scope="scope">
<el-radio
v-model="af.referenceSerialNumber"
class="reference-radio"
:label="scope.row.serialNumber"
:disabled="!scope.row.serialNumber"
>
<span class="reference-radio-label">基准点</span>
</el-radio>
</template>
</el-table-column>
<el-table-column label="操作" width="50" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="removeSubDeviceMapping(af, scope.$index)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="addSubDeviceMapping(af)">添加子设备</el-button>
</el-form-item>
<!-- itemField 标识映射 -->
<el-form-item v-if="af.identifierFields.length > 0" label="字段映射" label-width="100px" style="margin-bottom:0;margin-top:12px">
<div class="schema-tip" style="margin-bottom:6px;font-size:12px;color:#909399">
将数组项中 `source=thingsModel` 的字段映射到物模型标识。
</div>
<el-table :data="af.itemFieldMappings" size="mini" border style="width:100%">
<el-table-column label="数组项字段" width="140">
<template slot-scope="scope">{{ scope.row.platformField }}</template>
</el-table-column>
<el-table-column label="物模型标识" min-width="200">
<template slot-scope="scope">
<el-select v-model="scope.row.identifier" size="mini" placeholder="选择物模型字段" filterable style="width:100%">
<el-option v-for="tm in uniqueThingsModelOptions" :key="tm.identifier" :label="`${tm.modelName}(${tm.identifier})`" :value="tm.identifier" />
</el-select>
</template>
</el-table-column>
</el-table>
</el-form-item>
<!-- 上报间隔 -->
<el-form-item label="上报间隔" label-width="100px" style="margin-bottom:0;margin-top:12px">
<el-input-number v-model="af.reportIntervalSec" :min="10" :max="3600" :step="10" style="width:160px" /> 秒
<span style="margin-left:10px;font-size:12px;color:#909399">基准点数据超时阈值 = 间隔 × 1.5</span>
</el-form-item>
</el-card>
</div>
<!-- 标量字段映射:仅展示 requestSchema 中需要绑定物模型的字段 -->
<el-form-item v-if="fieldMappingRows.length > 0" label="字段映射">
<el-table :data="fieldMappingRows" size="mini" border style="width:100%">
<el-table-column label="物模型标识" min-width="180">
<template slot-scope="scope">
<el-select v-model="scope.row.identifier" size="mini" placeholder="选择物模型字段" filterable style="width:100%">
<el-option v-for="tm in thingsModelOptions" :key="tm.identifier" :label="`${tm.modelName}(${tm.identifier})`" :value="tm.identifier" />
<el-option v-for="tm in uniqueThingsModelOptions" :key="tm.identifier" :label="`${tm.modelName}(${tm.identifier})`" :value="tm.identifier" />
</el-select>
</template>
</el-table-column>
@ -158,11 +249,13 @@
<script>
import { listPlatformDeviceMapping, addPlatformDeviceMapping, updatePlatformDeviceMapping, delPlatformDeviceMapping, onboardDevice } from '@/api/platform/deviceMapping'
import { listPlatformFlow } from '@/api/platform/flow'
import { listPlatformInstance } from '@/api/platform/instance'
import { listPlatformInterface } from '@/api/platform/interface'
import { listDevice, listThingsModel } from '@/api/iot/device'
import { listDevice, listThingsModel, getDevice } from '@/api/iot/device'
import { listGateway } from '@/api/iot/gateway'
const SENSOR_TYPES = [
const DEFAULT_SENSOR_TYPES = [
{ value: 'inclino', label: '倾角' },
{ value: 'stress', label: '轴力计' },
{ value: 'hyeto', label: '雨量计' },
@ -175,20 +268,52 @@ export default {
return {
loading: false, submitLoading: false, showSearch: true,
total: 0, list: [], platformList: [], deviceOptions: [], deviceMap: {},
subDeviceOptions: [],
flowSensorTypes: [],
deviceSearchLoading: false, thingsModelOptions: [],
interfaceOptions: [], platformFieldOptions: [],
open: false, title: '',
sensorTypes: SENSOR_TYPES,
fieldMappingRows: [],
// 通用数组字段配置,格式:
// [{ fieldName, referenceSerialNumber, reportIntervalSec, subDeviceMappings:[{serialNumber,sensorId}], itemFieldMappings:[{platformField,identifier}] , identifierFields:[{platformField}] }]
arrayFieldsConfig: [],
// 当前推送接口的完整 schema 缓存,供内部使用
currentRequestSchema: null,
queryParams: { pageNum: 1, pageSize: 20, platformId: null, sensorType: '', netStatus: null },
form: { enable: '1' },
rules: {
platformId: [{ required: true, message: '请选择平台', trigger: 'change' }],
deviceId: [{ required: true, message: '请选择设备', trigger: 'change' }],
sensorType: [{ required: true, message: '请选择传感器类型', trigger: 'change' }]
sensorType: [{ required: true, message: '请选择或输入设备类型', trigger: 'change' }],
pushInterfaceId: [{ required: true, message: '请选择推送接口', trigger: 'change' }]
}
}
},
computed: {
hasArrayFields() {
return this.arrayFieldsConfig.length > 0
},
uniqueThingsModelOptions() {
// 物模型下拉按 identifier 去重,避免同一标识在数组映射里重复出现。
const deduped = []
const seen = new Set()
;(this.thingsModelOptions || []).forEach(item => {
if (!item || !item.identifier || seen.has(item.identifier)) return
seen.add(item.identifier)
deduped.push(item)
})
return deduped
},
sensorTypeOptions() {
const optionMap = new Map()
DEFAULT_SENSOR_TYPES.forEach(item => optionMap.set(item.value, { ...item }))
;(this.flowSensorTypes || []).forEach(value => this.appendSensorTypeOption(optionMap, value))
;(this.list || []).forEach(item => this.appendSensorTypeOption(optionMap, item && item.sensorType))
this.appendSensorTypeOption(optionMap, this.queryParams.sensorType)
this.appendSensorTypeOption(optionMap, this.form.sensorType)
return Array.from(optionMap.values())
}
},
created() {
listPlatformInstance({ pageNum: 1, pageSize: 100 }).then(res => { this.platformList = res.rows || [] })
this.getList()
@ -198,7 +323,6 @@ export default {
this.loading = true
listPlatformDeviceMapping(this.queryParams).then(res => {
this.list = res.rows; this.total = res.total; this.loading = false
// 加载设备名称
const ids = [...new Set(res.rows.map(r => r.deviceId))]
if (ids.length) {
listDevice({ pageNum: 1, pageSize: 200 }).then(dr => {
@ -211,12 +335,24 @@ export default {
},
platformName(id) { const p = this.platformList.find(x => x.id === id); return p ? p.name : id },
deviceName(id) { return this.deviceMap[id] || id },
sensorLabel(v) { const s = SENSOR_TYPES.find(x => x.value === v); return s ? s.label : v },
appendSensorTypeOption(optionMap, value) {
if (!value || optionMap.has(value)) return
optionMap.set(value, { value, label: value })
},
sensorLabel(v) {
const option = this.sensorTypeOptions.find(item => item.value === v)
return option ? option.label : v
},
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
handleAdd() {
this.form = { enable: '1' }
this.form = { enable: '1', pushInterfaceId: null }
this.fieldMappingRows = []
this.arrayFieldsConfig = []
this.currentRequestSchema = null
this.deviceOptions = []
this.subDeviceOptions = []
this.flowSensorTypes = []
this.thingsModelOptions = []
this.interfaceOptions = []
this.platformFieldOptions = []
@ -224,17 +360,15 @@ export default {
},
handleUpdate(row) {
this.form = { ...row }
this.fieldMappingRows = row.fieldMapping ? JSON.parse(row.fieldMapping) : []
// 加载物模型
this.loadThingsModel(row.deviceId)
this.fieldMappingRows = this.safeParseJson(row.fieldMapping, [])
// 加载该平台的接口列表
if (row.platformId) {
this.loadInterfaces(row.platformId)
}
// 如果有推送接口,解析平台字段
if (row.pushInterfaceId) {
this.$nextTick(() => { this.onPushInterfaceChange(row.pushInterfaceId) })
this.loadInterfaces(row.platformId, row.pushInterfaceId, true)
this.loadFlowSensorTypes(row.platformId)
}
// 加载数组字段配置(兼容新旧格式)
this.arrayFieldsConfig = this.extractArrayFieldsConfig(row.extraMapping)
this.loadDeviceContext(row.deviceId, this.arrayFieldsConfig)
this.title = '编辑设备绑定'; this.open = true
},
handleDelete(row) {
@ -253,26 +387,89 @@ export default {
onPlatformChange(platformId) {
this.interfaceOptions = []
this.platformFieldOptions = []
this.currentRequestSchema = null
this.form.pushInterfaceId = null
this.arrayFieldsConfig = []
this.fieldMappingRows = []
this.subDeviceOptions = []
this.flowSensorTypes = []
if (platformId) {
this.loadInterfaces(platformId)
this.loadFlowSensorTypes(platformId)
}
},
loadInterfaces(platformId) {
listPlatformInterface({ platformId, pageNum: 1, pageSize: 100 }).then(res => {
this.interfaceOptions = res.rows || []
loadFlowSensorTypes(platformId) {
if (!platformId) {
this.flowSensorTypes = []
return
}
listPlatformFlow({ platformId, flowType: 2, pageNum: 1, pageSize: 200 }).then(res => {
const values = []
;(res.rows || []).forEach(item => {
if (item && item.sensorType && values.indexOf(item.sensorType) < 0) {
values.push(item.sensorType)
}
})
this.flowSensorTypes = values
})
},
onPushInterfaceChange(interfaceId) {
loadInterfaces(platformId, selectedInterfaceId, isEditing) {
listPlatformInterface({ platformId, pageNum: 1, pageSize: 100 }).then(res => {
this.interfaceOptions = res.rows || []
if (selectedInterfaceId) {
this.onPushInterfaceChange(selectedInterfaceId, isEditing)
}
})
},
onPushInterfaceChange(interfaceId, isEditing) {
this.platformFieldOptions = []
if (!interfaceId) return
if (!interfaceId) {
this.currentRequestSchema = null
this.arrayFieldsConfig = []
this.fieldMappingRows = []
return
}
const existingArrayFieldsConfig = (this.arrayFieldsConfig || []).map(af => ({
...af,
subDeviceMappings: (af.subDeviceMappings || []).map(item => ({ ...item })),
itemFieldMappings: (af.itemFieldMappings || []).map(item => ({ ...item })),
identifierFields: (af.identifierFields || []).map(item => ({ ...item }))
}))
const iface = this.interfaceOptions.find(x => x.id === interfaceId)
if (iface && iface.requestSchema) {
if (!iface || !iface.requestSchema) {
this.currentRequestSchema = null
this.arrayFieldsConfig = []
this.fieldMappingRows = []
return
}
try {
const schema = JSON.parse(iface.requestSchema)
this.platformFieldOptions = schema.map(f => f.name).filter(Boolean)
// 自动预填字段映射行(仅当映射表为空时)
if (this.fieldMappingRows.length === 0 && this.platformFieldOptions.length > 0) {
this.currentRequestSchema = schema
// 仅提取需要设备字段绑定的标量字段
this.platformFieldOptions = schema
.filter(f => f.type !== 'array' && this.needsFieldMapping(f))
.map(f => f.name)
.filter(Boolean)
// 提取 array 字段
const arrayFields = schema.filter(f => f.type === 'array' && f.itemSchema && f.itemSchema.length > 0)
if (arrayFields.length > 0) {
const generated = this.buildArrayFieldsFromSchema(arrayFields)
this.arrayFieldsConfig = isEditing
? this.mergeArrayFieldsConfig(generated, existingArrayFieldsConfig)
: generated
this.applyDefaultSubDeviceMappings(isEditing ? existingArrayFieldsConfig : [])
} else {
this.arrayFieldsConfig = []
}
// 自动预填普通字段映射(仅限需要 thingsModel 绑定的标量字段)
if (this.platformFieldOptions.length > 0) {
this.fieldMappingRows = this.mergeFieldMappings(this.platformFieldOptions, this.fieldMappingRows)
} else if (!isEditing) {
this.fieldMappingRows = []
}
if (!isEditing && arrayFields.length === 0 && this.fieldMappingRows.length === 0 && this.platformFieldOptions.length > 0) {
this.fieldMappingRows = this.platformFieldOptions.map(name => ({
identifier: '',
platformField: name,
@ -280,13 +477,304 @@ export default {
}))
}
} catch (e) {
this.currentRequestSchema = null
this.platformFieldOptions = []
this.arrayFieldsConfig = []
this.fieldMappingRows = []
}
},
/** 从接口 schema 的 array 字段定义构建 arrayFieldsConfig */
buildArrayFieldsFromSchema(arrayFields) {
return arrayFields.map(af => {
const identifierFields = (af.itemSchema || []).filter(s => this.isThingsModelSource(s.source))
const subDeviceFieldNames = (af.itemSchema || [])
.filter(s => s && s.source === 'subDeviceId' && s.name)
.map(s => s.name)
return this.hydrateArrayFieldConfig({
fieldName: af.name,
referenceSerialNumber: '',
reportIntervalSec: 300,
subDeviceMappings: [
{ serialNumber: '', sensorId: '' }
],
subDeviceFieldNames,
itemFieldMappings: identifierFields.map(s => ({
platformField: s.name,
identifier: ''
})),
// 缓存 identifier 源字段名,用于判断是否渲染字段映射表
identifierFields
})
})
},
mergeArrayFieldsConfig(generated, existing) {
const existingMap = {}
;(existing || []).forEach(item => {
existingMap[item.fieldName] = item
})
return generated.map(item => {
const saved = existingMap[item.fieldName]
if (!saved) return item
const itemFieldMappings = item.itemFieldMappings.map(mapping => {
const savedMapping = (saved.itemFieldMappings || []).find(x => x.platformField === mapping.platformField)
return savedMapping ? { ...mapping, identifier: savedMapping.identifier || '' } : mapping
})
return this.hydrateArrayFieldConfig({
...item,
referenceSerialNumber: saved.referenceSerialNumber || item.referenceSerialNumber,
reportIntervalSec: saved.reportIntervalSec || item.reportIntervalSec,
subDeviceMappings: (saved.subDeviceMappings && saved.subDeviceMappings.length > 0)
? saved.subDeviceMappings
: item.subDeviceMappings,
subDeviceFieldNames: item.subDeviceFieldNames || [],
itemFieldMappings
})
})
},
hydrateArrayFieldConfig(item) {
const subDeviceMappings = (item.subDeviceMappings || []).map(row => {
const serialNumber = (row.serialNumber || '').trim()
const sensorId = ((row.sensorId || serialNumber) || '').trim()
return {
...row,
serialNumber,
sensorId,
_selectedSerial: serialNumber,
_lastSyncedSerial: sensorId === serialNumber ? serialNumber : ''
}
})
return {
...item,
referenceSerialNumber: this.resolveReferenceSerialNumber({
referenceSerialNumber: item.referenceSerialNumber,
subDeviceMappings
}),
subDeviceFieldNames: item.subDeviceFieldNames || [],
subDeviceMappings
}
},
subDeviceFieldLabel(af) {
const fieldNames = (af && af.subDeviceFieldNames) || []
if (fieldNames.length === 0) {
return '设备标识'
}
return fieldNames.join(' / ')
},
subDeviceFieldPlaceholder(af) {
const fieldLabel = this.subDeviceFieldLabel(af)
return `默认展示所选子设备编号,可手动覆盖;保存后用于填充 ${fieldLabel}`
},
mergeFieldMappings(platformFields, existingRows) {
const existingMap = {}
;(existingRows || []).forEach(row => {
if (row.platformField) {
existingMap[row.platformField] = row
}
})
return platformFields.map(platformField => {
const saved = existingMap[platformField]
return saved
? { ...saved, platformField }
: { identifier: '', platformField, transform: '' }
})
},
needsFieldMapping(field) {
if (!field) return false
if (this.isThingsModelSource(field.source)) return true
return field.source === 'context' && field.contextKey && field.contextKey !== 'reportTime'
},
isThingsModelSource(source) {
return source === 'thingsModel' || source === 'identifier'
},
hasArraySchema(schema = this.currentRequestSchema) {
return Array.isArray(schema) && schema.some(f => f.type === 'array' && f.itemSchema && f.itemSchema.length > 0)
},
safeParseJson(value, defaultValue) {
if (!value) return defaultValue
try {
return JSON.parse(value)
} catch (e) {
return defaultValue
}
},
loadDeviceContext(deviceId, existingMappings = []) {
if (!deviceId) return
getDevice(deviceId).then(res => {
const device = res.data || res
if (!device) return
// 编辑态必须按已绑定设备重新回显,不能只依赖远程搜索结果。
this.deviceOptions = [{
deviceId: device.deviceId,
deviceName: device.deviceName,
serialNumber: device.serialNumber
}]
this.form.deviceId = device.deviceId
this.form.serialNumber = device.serialNumber
this.loadThingsModel(device.deviceId)
this.loadSubDeviceOptions(device, existingMappings)
}).catch(() => {
if (this.form.deviceId || this.form.serialNumber) {
this.loadSubDeviceOptions({
deviceId: this.form.deviceId,
serialNumber: this.form.serialNumber
}, existingMappings)
}
this.loadThingsModel(deviceId)
})
},
loadSubDeviceOptions(gateway, existingMappings = []) {
const gatewayInfo = typeof gateway === 'object' && gateway !== null
? gateway
: { serialNumber: gateway }
const gatewayDeviceId = gatewayInfo.deviceId
const gatewaySerialNumber = gatewayInfo.serialNumber
if (!gatewayDeviceId && !gatewaySerialNumber) {
this.subDeviceOptions = []
return
}
const relationRowsPromise = gatewayDeviceId
? this.fetchAllRows(listGateway, { gwDeviceId: gatewayDeviceId })
: Promise.resolve([])
const deviceRowsPromise = gatewaySerialNumber
? this.fetchAllRows(listDevice, { gwDevCode: gatewaySerialNumber })
: Promise.resolve([])
Promise.all([
relationRowsPromise.catch(() => []),
deviceRowsPromise.catch(() => [])
]).then(([relationRows, deviceRows]) => {
const optionMap = new Map()
;(relationRows || []).forEach(item => {
this.appendSubDeviceOption(optionMap, {
deviceId: item.subDeviceId,
deviceName: item.subDeviceName,
serialNumber: item.subDeviceNo,
slaveId: item.slaveId
})
})
;(deviceRows || []).forEach(item => {
this.appendSubDeviceOption(optionMap, item)
})
// 兼容历史绑定数据:即使当前接口未查到,也保留已保存的子设备编号用于回显。
;(existingMappings || []).forEach(mapping => {
;(mapping.subDeviceMappings || []).forEach(item => {
this.appendSubDeviceOption(optionMap, {
deviceId: item.deviceId || null,
deviceName: item.deviceName || item.serialNumber,
serialNumber: item.serialNumber
})
})
})
this.subDeviceOptions = Array.from(optionMap.values())
this.applyDefaultSubDeviceMappings(existingMappings)
})
},
fetchAllRows(api, params, pageSize = 500) {
const requestPage = pageNum => api({ ...params, pageNum, pageSize }).then(res => {
const rows = res.rows || []
const total = Number(res.total || rows.length)
const loaded = pageNum * pageSize
if (rows.length > 0 && loaded < total) {
return requestPage(pageNum + 1).then(nextRows => rows.concat(nextRows))
}
return rows
})
return requestPage(1)
},
appendSubDeviceOption(optionMap, item) {
const serialNumber = (item && (item.serialNumber || item.subDeviceNo) || '').trim()
if (!serialNumber || optionMap.has(serialNumber)) return
optionMap.set(serialNumber, {
deviceId: item.deviceId || item.subDeviceId || null,
deviceName: item.deviceName || item.subDeviceName || serialNumber,
serialNumber,
slaveId: item.slaveId
})
},
applyDefaultSubDeviceMappings(existingMappings = []) {
if (!this.subDeviceOptions.length || !this.arrayFieldsConfig.length) return
const savedFieldMap = {}
;(existingMappings || []).forEach(mapping => {
if (!mapping || !(mapping.subDeviceMappings || []).some(item => item && item.serialNumber)) return
savedFieldMap[mapping.fieldName || ''] = true
})
this.arrayFieldsConfig.forEach(af => {
if (savedFieldMap[af.fieldName || '']) return
const rows = af.subDeviceMappings || []
const onlyBlankRow = rows.length === 0 || rows.every(row =>
!((row.serialNumber || '').trim()) && !((row.sensorId || '').trim())
)
if (!onlyBlankRow) return
af.subDeviceMappings = this.subDeviceOptions.map(device => ({
serialNumber: device.serialNumber,
sensorId: device.serialNumber,
_selectedSerial: device.serialNumber,
_lastSyncedSerial: device.serialNumber
}))
af.referenceSerialNumber = this.resolveReferenceSerialNumber(af)
})
},
/** 从 extraMapping 中提取 arrayFields 配置(兼容旧 staticLevel 格式) */
extractArrayFieldsConfig(extraMapping) {
if (!extraMapping) return []
try {
const extra = JSON.parse(extraMapping)
// 新格式
if (extra.arrayFields && extra.arrayFields.length > 0) {
return extra.arrayFields.map(af => {
const referenceMapping = af.subDeviceMappings
? af.subDeviceMappings.find(s => s.isReference)
: null
const identifierFields = af.itemFieldMappings
? af.itemFieldMappings.map(im => ({ platformField: im.platformField }))
: []
return this.hydrateArrayFieldConfig({
fieldName: af.fieldName,
referenceSerialNumber: referenceMapping ? referenceMapping.serialNumber : ((af.subDeviceMappings || [])[0] || {}).serialNumber || '',
reportIntervalSec: af.reportIntervalSec || 300,
subDeviceMappings: af.subDeviceMappings || [],
subDeviceFieldNames: [],
itemFieldMappings: (af.itemFieldMappings || []).map(im => ({
platformField: im.platformField,
identifier: im.identifier || ''
})),
identifierFields
})
})
}
// 旧格式 { staticLevel: { ... } }
if (extra.staticLevel) {
const sl = extra.staticLevel
const mappings = sl.sensorIdMappings || []
return [this.hydrateArrayFieldConfig({
fieldName: 'sensorData',
referenceSerialNumber: sl.referenceSerialNumber || (mappings[0] && mappings[0].serialNumber) || '',
reportIntervalSec: 300,
subDeviceMappings: mappings.map(m => ({
serialNumber: m.serialNumber || '',
sensorId: m.sensorId || ''
})),
subDeviceFieldNames: ['sensorId'],
itemFieldMappings: [
{ platformField: 'height', identifier: sl.heightIdentifier || '' },
{ platformField: 'temperature', identifier: sl.temperatureIdentifier || '' }
],
identifierFields: [
{ platformField: 'height' },
{ platformField: 'temperature' }
]
})]
}
} catch (e) { /* ignore */ }
return []
},
onDeviceChange(deviceId) {
const d = this.deviceOptions.find(x => x.deviceId === deviceId)
if (d) { this.form.serialNumber = d.serialNumber }
if (d) {
this.form.serialNumber = d.serialNumber
this.loadSubDeviceOptions(d, this.arrayFieldsConfig)
} else {
this.subDeviceOptions = []
}
this.loadThingsModel(deviceId)
},
searchDevice(query) {
@ -303,11 +791,77 @@ export default {
})
},
addFieldMapping() { this.fieldMappingRows.push({ identifier: '', platformField: '', transform: '' }) },
addSubDeviceMapping(af) {
af.subDeviceMappings.push({ serialNumber: '', sensorId: '' })
},
onSubDeviceSerialChange(af, row) {
const previousSerial = (row._selectedSerial || '').trim()
const currentSerial = (row.serialNumber || '').trim()
const currentSensorId = (row.sensorId || '').trim()
const lastSyncedSerial = (row._lastSyncedSerial || '').trim()
if (!currentSensorId || currentSensorId === previousSerial || currentSensorId === lastSyncedSerial) {
row.sensorId = currentSerial
row._lastSyncedSerial = currentSerial
}
if (!af.referenceSerialNumber || af.referenceSerialNumber === previousSerial) {
af.referenceSerialNumber = currentSerial
}
row._selectedSerial = currentSerial
},
removeSubDeviceMapping(af, index) {
const removed = af.subDeviceMappings.splice(index, 1)[0]
if (removed && removed.serialNumber === af.referenceSerialNumber) {
af.referenceSerialNumber = this.resolveReferenceSerialNumber(af)
}
},
resolveReferenceSerialNumber(af) {
const current = (af && af.referenceSerialNumber ? af.referenceSerialNumber : '').trim()
const serialNumbers = (af && af.subDeviceMappings ? af.subDeviceMappings : [])
.map(item => (item && item.serialNumber ? item.serialNumber.trim() : ''))
.filter(Boolean)
if (current && serialNumbers.indexOf(current) >= 0) {
return current
}
return serialNumbers[0] || ''
},
buildExtraMapping() {
if (!this.currentRequestSchema) {
return this.form.extraMapping || null
}
if (!this.hasArraySchema()) {
return null
}
const arrayFields = this.arrayFieldsConfig.filter(af => af.subDeviceMappings.some(m => m.serialNumber))
if (arrayFields.length === 0) return null
// extraMapping 只保存数组字段运行时所需的补充绑定,切换到非数组接口时应被清空。
return JSON.stringify({
arrayFields: arrayFields.map(af => ({
fieldName: af.fieldName,
reportIntervalSec: af.reportIntervalSec || 300,
subDeviceMappings: af.subDeviceMappings
.filter(m => m.serialNumber)
.map(m => ({
serialNumber: (m.serialNumber || '').trim(),
sensorId: ((m.sensorId || m.serialNumber) || '').trim(),
isReference: (m.serialNumber || '').trim() === this.resolveReferenceSerialNumber(af)
})),
itemFieldMappings: af.itemFieldMappings
.filter(im => im.identifier)
.map(im => ({
platformField: im.platformField,
identifier: im.identifier
}))
}))
})
},
cancel() { this.open = false },
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return
this.form.fieldMapping = JSON.stringify(this.fieldMappingRows)
this.form.fieldMapping = this.fieldMappingRows.length > 0
? JSON.stringify(this.fieldMappingRows)
: null
this.form.extraMapping = this.buildExtraMapping()
this.submitLoading = true
const api = this.form.id ? updatePlatformDeviceMapping : addPlatformDeviceMapping
api(this.form).then(() => {
@ -319,3 +873,27 @@ export default {
}
}
</script>
<style scoped>
.schema-tip {
font-size: 12px;
color: #909399;
line-height: 1.6;
}
.array-fields-section {
margin-bottom: 22px;
}
.array-fields-section .section-label {
font-size: 14px;
color: #606266;
font-weight: 600;
margin-bottom: 8px;
padding-left: 4px;
}
.reference-radio {
margin-right: 0;
}
.reference-radio /deep/ .el-radio__label {
display: none;
}
</style>

View File

@ -1,6 +1,5 @@
<template>
<div class="platform-flow">
<!-- 搜索 -->
<el-card v-show="showSearch" style="margin-bottom:10px">
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" style="margin-bottom:-18px">
<el-form-item label="平台" prop="platformId">
@ -43,7 +42,7 @@
<el-tag size="small" type="info" v-if="scope.row.flowType===3">Token刷新</el-tag>
</template>
</el-table-column>
<el-table-column label="传感器类型" prop="sensorType" width="100" align="center">
<el-table-column label="设备类型" prop="sensorType" width="120" align="center">
<template slot-scope="scope">
<span v-if="scope.row.sensorType">{{ sensorLabel(scope.row.sensorType) }}</span>
<span v-else style="color:#c0c4cc">—</span>
@ -66,13 +65,12 @@
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body :close-on-click-modal="false">
<el-dialog :title="title" :visible.sync="open" width="920px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="所属平台" prop="platformId">
<el-select v-model="form.platformId" style="width:100%" @change="loadInterfaces">
<el-select v-model="form.platformId" style="width:100%" @change="onPlatformChange">
<el-option v-for="p in platformList" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
@ -97,12 +95,9 @@
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.flowType===2">
<el-form-item label="传感器类型">
<el-select v-model="form.sensorType" style="width:100%">
<el-option value="inclino" label="倾角" />
<el-option value="stress" label="轴力计" />
<el-option value="hyeto" label="雨量计" />
<el-option value="static_level" label="静力水准" />
<el-form-item label="设备类型" prop="sensorType">
<el-select v-model="form.sensorType" filterable allow-create default-first-option clearable placeholder="选择或输入设备类型" style="width:100%">
<el-option v-for="item in sensorTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
@ -113,23 +108,37 @@
</el-col>
</el-row>
<!-- 步骤列表 -->
<el-divider content-position="left">流程步骤</el-divider>
<div class="schema-tip" style="margin-bottom:8px">
步骤配置支持前端直接定义入参覆盖和出参持久化,不再依赖初始化 SQL。
</div>
<el-table :data="steps" size="mini" border>
<el-table-column label="序号" type="index" width="50" align="center" />
<el-table-column label="接口" min-width="180">
<el-table-column label="接口" min-width="230">
<template slot-scope="scope">
<el-select v-model="scope.row.interfaceId" size="mini" placeholder="选择接口" style="width:100%">
<el-option v-for="i in interfaceOptions" :key="i.id" :label="`${i.name}(${i.method} ${i.path})`" :value="i.id" />
</el-select>
</template>
</el-table-column>
<el-table-column label="出参Key" width="120">
<el-table-column label="出参Key" width="130">
<template slot-scope="scope">
<el-input v-model="scope.row.resultKey" size="mini" placeholder="如 step1" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120">
<el-table-column label="入参覆盖" width="120" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openParamMappingDialog(scope.$index)">配置</el-button>
<el-tag v-if="paramMappingCount(scope.row) > 0" size="mini" type="info">{{ paramMappingCount(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="结果保存" width="120" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="openResultSaveDialog(scope.$index)">配置</el-button>
<el-tag v-if="resultSaveCount(scope.row) > 0" size="mini" type="success">{{ resultSaveCount(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" min-width="150">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" size="mini" />
</template>
@ -142,70 +151,239 @@
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="steps.push({interfaceId:null,resultKey:'',remark:''})">添加步骤</el-button>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="steps.push(createEmptyStep())">添加步骤</el-button>
</el-form>
<div slot="footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading">保存</el-button>
</div>
</el-dialog>
<el-dialog :visible.sync="paramMappingDialogVisible" title="步骤入参绑定" width="860px" append-to-body :close-on-click-modal="false">
<div class="schema-tip" style="margin-bottom:8px">
仅填写需要覆盖接口默认定义的参数。默认来源来自接口定义;覆盖后按当前步骤执行。
</div>
<el-table v-if="stepParamRows.length > 0" :data="stepParamRows" size="mini" border>
<el-table-column label="参数名" width="160">
<template slot-scope="scope">{{ scope.row.name }}</template>
</el-table-column>
<el-table-column label="默认来源" min-width="180">
<template slot-scope="scope">{{ scope.row.defaultSource }}</template>
</el-table-column>
<el-table-column label="覆盖来源" width="150">
<template slot-scope="scope">
<el-select v-model="scope.row.source" size="mini" clearable placeholder="不覆盖" style="width:100%" @change="onStepOverrideSourceChange(scope.row)">
<el-option label="固定值" value="fixed" />
<el-option label="认证参数" value="auth" />
<el-option label="平台参数" value="extra" />
<el-option label="上一步结果" value="context" />
<el-option label="当前设备" value="device" />
<el-option label="自动UUID" value="uuid" />
<el-option label="子设备传感器列表" value="subDeviceSensorIds" />
<el-option label="基准点成对传感器列表" value="referenceSubDeviceSensorIdPairs" />
</el-select>
</template>
</el-table-column>
<el-table-column label="取值配置" min-width="220">
<template slot-scope="scope">
<el-input v-if="scope.row.source === 'fixed'" v-model="scope.row.value" size="mini" placeholder="固定值" />
<el-select v-else-if="scope.row.source === 'auth' && platformAuthFields.length > 0" v-model="scope.row.field" size="mini" filterable allow-create default-first-option style="width:100%">
<el-option v-for="item in platformAuthFields" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-else-if="scope.row.source === 'auth'" v-model="scope.row.field" size="mini" placeholder="认证参数名,如 userName" />
<el-select v-else-if="scope.row.source === 'extra' && platformExtraFields.length > 0" v-model="scope.row.field" size="mini" filterable allow-create default-first-option style="width:100%">
<el-option v-for="item in platformExtraFields" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-else-if="scope.row.source === 'extra'" v-model="scope.row.field" size="mini" placeholder="平台参数名,如 projectId" />
<el-select v-else-if="scope.row.source === 'context' && previousResultOptions.length > 0" v-model="scope.row.contextKey" size="mini" filterable style="width:100%">
<el-option v-for="item in previousResultOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-input v-else-if="scope.row.source === 'context'" v-model="scope.row.contextKey" size="mini" placeholder="选择上一步结果,如 step1.id" />
<el-select v-else-if="scope.row.source === 'device'" v-model="scope.row.field" size="mini" style="width:100%">
<el-option label="平台设备ID" value="platform_device_id" />
<el-option label="平台测点ID" value="platform_point_id" />
<el-option label="系统设备编号" value="serial_number" />
</el-select>
<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>
<span v-else class="schema-hint">保持接口默认来源</span>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="当前接口未定义可覆盖入参" :image-size="80" />
<div slot="footer">
<el-button @click="paramMappingDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmParamMapping">确定</el-button>
</div>
</el-dialog>
<el-dialog :visible.sync="resultSaveDialogVisible" title="结果保存配置" width="760px" append-to-body :close-on-click-modal="false">
<div class="schema-tip" style="margin-bottom:8px">
`field` 填接口出参字段名;`saveTo` 支持 `extra_config.xxx`、`platform_instance.access_token`、`device_mapping.platform_point_id` 等目标。
</div>
<el-table :data="stepSaveRows" size="mini" border>
<el-table-column label="出参字段" width="180">
<template slot-scope="scope">
<el-input v-model="scope.row.field" size="mini" placeholder="如 id" />
</template>
</el-table-column>
<el-table-column label="保存目标" min-width="360">
<template slot-scope="scope">
<el-input v-model="scope.row.saveTo" size="mini" placeholder="如 extra_config.projectId" />
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" @click="stepSaveRows.splice(scope.$index, 1)" />
</template>
</el-table-column>
</el-table>
<el-button size="mini" icon="el-icon-plus" style="margin-top:8px" @click="stepSaveRows.push({ field: '', saveTo: '' })">添加保存项</el-button>
<div slot="footer">
<el-button @click="resultSaveDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmResultSave">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listPlatformFlow, getPlatformFlow, savePlatformFlow, delPlatformFlow, executePlatformFlow } from '@/api/platform/flow'
import { listPlatformInstance } from '@/api/platform/instance'
import { listPlatformInstance, getPlatformInstance } from '@/api/platform/instance'
import { listPlatformInterface } from '@/api/platform/interface'
const SENSOR_LABELS = { inclino: '倾角', stress: '轴力计', hyeto: '雨量计', static_level: '静力水准' }
const DEFAULT_SENSOR_TYPES = [
{ value: 'inclino', label: '倾角' },
{ value: 'stress', label: '轴力计' },
{ value: 'hyeto', label: '雨量计' },
{ value: 'static_level', label: '静力水准' }
]
const SENSOR_LABELS = DEFAULT_SENSOR_TYPES.reduce((acc, item) => {
acc[item.value] = item.label
return acc
}, {})
export default {
name: 'PlatformFlow',
data() {
return {
loading: false, submitLoading: false, showSearch: true,
total: 0, list: [], platformList: [], interfaceOptions: [],
open: false, title: '', steps: [],
loading: false,
submitLoading: false,
showSearch: true,
total: 0,
list: [],
platformList: [],
interfaceOptions: [],
platformAuthFields: [],
platformExtraFields: [],
open: false,
title: '',
steps: [],
paramMappingDialogVisible: false,
resultSaveDialogVisible: false,
editingStepIndex: -1,
stepParamRows: [],
stepSaveRows: [],
previousResultOptions: [],
queryParams: { pageNum: 1, pageSize: 20, platformId: null, flowType: null },
form: { enable: '1', flowType: 1 },
rules: {
platformId: [{ required: true, message: '请选择平台', trigger: 'change' }],
name: [{ required: true, message: '请输入流程名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入流程标识', trigger: 'blur' }],
flowType: [{ required: true, message: '请选择流程类型', trigger: 'change' }]
flowType: [{ required: true, message: '请选择流程类型', trigger: 'change' }],
sensorType: [{ required: true, message: '请输入设备类型', trigger: 'change' }]
}
}
},
computed: {
sensorTypeOptions() {
const optionMap = new Map()
DEFAULT_SENSOR_TYPES.forEach(item => optionMap.set(item.value, { ...item }))
;(this.list || []).forEach(item => this.appendSensorTypeOption(optionMap, item && item.sensorType))
this.appendSensorTypeOption(optionMap, this.form.sensorType)
return Array.from(optionMap.values())
}
},
created() {
listPlatformInstance({ pageNum: 1, pageSize: 100 }).then(res => { this.platformList = res.rows })
listPlatformInstance({ pageNum: 1, pageSize: 100 }).then(res => { this.platformList = res.rows || [] })
this.getList()
},
methods: {
getList() {
this.loading = true
listPlatformFlow(this.queryParams).then(res => {
this.list = res.rows; this.total = res.total; this.loading = false
this.list = res.rows || []
this.total = res.total
this.loading = false
})
},
platformName(id) { const p = this.platformList.find(x => x.id === id); return p ? p.name : id },
sensorLabel(v) { return SENSOR_LABELS[v] || v },
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
appendSensorTypeOption(optionMap, value) {
if (!value || optionMap.has(value)) return
optionMap.set(value, { value, label: SENSOR_LABELS[value] || value })
},
platformName(id) {
const platform = this.platformList.find(item => item.id === id)
return platform ? platform.name : id
},
sensorLabel(value) {
const option = this.sensorTypeOptions.find(item => item.value === value)
return option ? option.label : value
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
createEmptyStep() {
return { interfaceId: null, paramMapping: null, resultKey: '', resultSave: null, remark: '' }
},
normalizeStep(step) {
return {
id: step.id,
flowId: step.flowId,
stepOrder: step.stepOrder,
interfaceId: step.interfaceId,
paramMapping: step.paramMapping || null,
resultKey: step.resultKey || '',
resultSave: step.resultSave || null,
remark: step.remark || ''
}
},
handleAdd() {
this.form = { enable: '1', flowType: 1 }; this.steps = []
this.title = '新增流程'; this.open = true
this.form = { enable: '1', flowType: 1, sensorType: '' }
this.steps = []
this.interfaceOptions = []
this.platformAuthFields = []
this.platformExtraFields = []
this.title = '新增流程'
this.open = true
},
handleUpdate(row) {
getPlatformFlow(row.id).then(res => {
this.form = res.data.flow
this.steps = res.data.steps || []
this.form = { ...res.data.flow }
this.steps = (res.data.steps || []).map(step => this.normalizeStep(step))
this.loadInterfaces(this.form.platformId)
this.title = '编辑流程'; this.open = true
this.loadPlatformOptions(this.form.platformId)
this.title = '编辑流程'
this.open = true
})
},
onPlatformChange(platformId) {
this.loadInterfaces(platformId)
this.loadPlatformOptions(platformId)
},
handleDelete(row) {
this.$confirm('确认删除?').then(() => {
delPlatformFlow(row.id).then(() => { this.getList(); this.$modal.msgSuccess('删除成功') })
delPlatformFlow(row.id).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
})
})
},
handleEnableChange(row) {
@ -213,32 +391,214 @@ export default {
},
handleExecute(row) {
this.$confirm(`确认手动执行流程【${row.name}】?`).then(() => {
executePlatformFlow(row.id).then(() => { this.$modal.msgSuccess('执行完成') })
executePlatformFlow(row.id).then(() => {
this.$modal.msgSuccess('执行完成')
})
})
},
loadInterfaces(platformId) {
if (!platformId) return
if (!platformId) {
this.interfaceOptions = []
return
}
listPlatformInterface({ platformId, pageNum: 1, pageSize: 200 }).then(res => {
this.interfaceOptions = res.rows
this.interfaceOptions = res.rows || []
})
},
moveStep(idx, dir) {
const arr = this.steps
const target = idx + dir
if (target < 0 || target >= arr.length) return
const tmp = arr[idx]; arr.splice(idx, 1); arr.splice(target, 0, tmp)
loadPlatformOptions(platformId) {
if (!platformId) {
this.platformAuthFields = []
this.platformExtraFields = []
return
}
getPlatformInstance(platformId).then(res => {
const data = res.data || {}
this.platformAuthFields = this.extractJsonKeys(data.authConfig)
this.platformExtraFields = this.extractJsonKeys(data.extraConfig)
}).catch(() => {
this.platformAuthFields = []
this.platformExtraFields = []
})
},
extractJsonKeys(value) {
if (!value) return []
try {
return Object.keys(JSON.parse(value))
} catch (e) {
return []
}
},
moveStep(index, direction) {
const target = index + direction
if (target < 0 || target >= this.steps.length) return
const step = this.steps[index]
this.steps.splice(index, 1)
this.steps.splice(target, 0, step)
},
safeParseJson(value, defaultValue) {
if (!value) return defaultValue
try {
return JSON.parse(value)
} catch (e) {
return defaultValue
}
},
parseRequestSchema(interfaceId) {
const iface = this.interfaceOptions.find(item => item.id === interfaceId)
return this.safeParseJson(iface && iface.requestSchema, [])
},
describeFieldSource(field) {
if (!field || !field.source) return '—'
if (field.source === 'fixed') return `固定值 / ${field.value}`
if (field.source === 'auth') return `认证参数 / ${field.field || ''}`
if (field.source === 'extra') return `平台参数 / ${field.field || ''}`
if (field.source === 'context') return `上一步结果 / ${field.contextKey || ''}`
if (field.source === 'device') {
const deviceLabels = {
platform_device_id: '平台设备ID',
platform_point_id: '平台测点ID',
serial_number: '系统设备编号'
}
return `当前设备 / ${deviceLabels[field.field] || field.field || ''}`
}
if (field.source === 'subDeviceSensorIds') return '子设备传感器列表'
if (field.source === 'referenceSubDeviceSensorIdPairs') return '基准点成对传感器列表'
if (field.source === 'uuid') return '自动UUID'
if (field.source === 'thingsModel') return '设备物模型'
if (field.source === 'ts') return '上报时间'
return field.source
},
parseResponseSchema(interfaceId) {
const iface = this.interfaceOptions.find(item => item.id === interfaceId)
return this.safeParseJson(iface && iface.responseSchema, [])
},
buildPreviousResultOptions(stepIndex) {
const options = []
for (let i = 0; i < stepIndex; i++) {
const step = this.steps[i]
if (!step || !step.resultKey) continue
const iface = this.interfaceOptions.find(item => item.id === step.interfaceId)
const responseFields = this.parseResponseSchema(step.interfaceId)
if (responseFields.length === 0) {
options.push({
value: step.resultKey,
label: `${step.resultKey}${iface ? `(${iface.name})` : ''}`
})
continue
}
responseFields.forEach(field => {
if (!field || !field.name) return
options.push({
value: `${step.resultKey}.${field.name}`,
label: `${step.resultKey}.${field.name}${iface ? `(${iface.name})` : ''}`
})
})
}
return options
},
paramMappingCount(step) {
const mappings = this.safeParseJson(step && step.paramMapping, {})
return mappings ? Object.keys(mappings).length : 0
},
resultSaveCount(step) {
const saves = this.safeParseJson(step && step.resultSave, [])
return Array.isArray(saves) ? saves.length : 0
},
openParamMappingDialog(stepIndex) {
const step = this.steps[stepIndex]
if (!step || !step.interfaceId) {
this.$modal.msgWarning('请先选择接口')
return
}
const schema = this.parseRequestSchema(step.interfaceId)
const savedMappings = this.safeParseJson(step.paramMapping, {})
this.editingStepIndex = stepIndex
this.previousResultOptions = this.buildPreviousResultOptions(stepIndex)
this.stepParamRows = schema
.filter(field => field && field.name)
.map(field => {
const saved = savedMappings[field.name] || {}
return {
name: field.name,
defaultSource: this.describeFieldSource(field),
source: saved.source || '',
value: saved.value !== undefined ? saved.value : '',
field: saved.field || '',
contextKey: saved.contextKey || ''
}
})
this.paramMappingDialogVisible = true
},
onStepOverrideSourceChange(row) {
row.value = ''
row.field = ''
row.contextKey = ''
},
confirmParamMapping() {
const mappings = {}
this.stepParamRows.forEach(row => {
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') item.field = row.field
if (row.source === 'context') item.contextKey = row.contextKey
mappings[row.name] = item
})
this.steps[this.editingStepIndex].paramMapping = Object.keys(mappings).length > 0 ? JSON.stringify(mappings) : null
this.paramMappingDialogVisible = false
},
openResultSaveDialog(stepIndex) {
this.editingStepIndex = stepIndex
this.stepSaveRows = this.safeParseJson(this.steps[stepIndex].resultSave, []).map(item => ({
field: item.field || '',
saveTo: item.saveTo || ''
}))
this.resultSaveDialogVisible = true
},
confirmResultSave() {
const saves = this.stepSaveRows
.filter(item => item.field && item.saveTo)
.map(item => ({ field: item.field, saveTo: item.saveTo }))
this.steps[this.editingStepIndex].resultSave = saves.length > 0 ? JSON.stringify(saves) : null
this.resultSaveDialogVisible = false
},
cancel() {
this.open = false
},
cancel() { this.open = false },
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return
this.submitLoading = true
savePlatformFlow({ flow: this.form, steps: this.steps }).then(() => {
const flow = {
...this.form,
sensorType: this.form.flowType === 2 ? (this.form.sensorType || '') : null
}
const steps = this.steps.map((step, index) => ({
...step,
stepOrder: index + 1,
paramMapping: step.paramMapping || null,
resultSave: step.resultSave || null
}))
savePlatformFlow({ flow, steps }).then(() => {
this.$modal.msgSuccess('保存成功')
this.open = false; this.getList()
this.open = false
this.getList()
}).finally(() => { this.submitLoading = false })
})
}
}
}
</script>
<style scoped>
.schema-tip {
font-size: 12px;
color: #909399;
line-height: 1.6;
}
.schema-hint {
font-size: 12px;
color: #c0c4cc;
}
</style>

View File

@ -95,10 +95,10 @@
</el-form-item>
<!-- 密码Token认证配置 -->
<template v-if="form.authType===1">
<el-form-item label="用户名" prop="authConfigParsed.userName">
<el-form-item label="用户名">
<el-input v-model="authConfigParsed.userName" />
</el-form-item>
<el-form-item label="密码" prop="authConfigParsed.password">
<el-form-item label="密码">
<el-input v-model="authConfigParsed.password" type="password" show-password />
</el-form-item>
<el-form-item label="clientId">
@ -117,6 +117,18 @@
<el-input v-model="authConfigParsed.apiKey" type="password" show-password />
</el-form-item>
</template>
<el-divider content-position="left">平台参数</el-divider>
<div class="schema-tip" style="margin-bottom:8px">
这里维护平台运行所需的业务参数,例如 `projectId`、项目名称、站点编码等;初始化流程写回的值也会保存在这里,可直接前端补录或修改。
</div>
<key-value-editor
v-model="platformConfigRows"
add-text="添加平台参数"
:show-enabled="false"
:show-description="true"
key-placeholder="参数名,如 projectId"
value-placeholder="参数值"
/>
<el-form-item label="启用">
<el-switch v-model="form.enable" active-value="1" inactive-value="0" />
</el-form-item>
@ -138,10 +150,11 @@
<script>
import { listPlatformInstance, getPlatformInstance, addPlatformInstance, updatePlatformInstance, delPlatformInstance, initializePlatform } from '@/api/platform/instance'
import InterfaceDrawer from '../components/InterfaceDrawer'
import KeyValueEditor from '../components/KeyValueEditor'
export default {
name: 'PlatformInstance',
components: { InterfaceDrawer },
components: { InterfaceDrawer, KeyValueEditor },
data() {
return {
loading: false,
@ -157,6 +170,7 @@ export default {
queryParams: { pageNum: 1, pageSize: 10, name: '', enable: '' },
form: { authType: 1, enable: '1' },
authConfigParsed: { userName: '', password: '', clientId: '', scope: '' },
platformConfigRows: [],
rules: {
name: [{ required: true, message: '请输入平台名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入平台标识', trigger: 'blur' }],
@ -185,6 +199,7 @@ export default {
handleAdd() {
this.form = { authType: 1, enable: '1' }
this.authConfigParsed = { userName: '', password: '', clientId: '', scope: '' }
this.platformConfigRows = []
this.title = '新增平台'
this.open = true
},
@ -192,6 +207,7 @@ export default {
getPlatformInstance(row.id).then(res => {
this.form = res.data
this.authConfigParsed = this.form.authConfig ? JSON.parse(this.form.authConfig) : {}
this.platformConfigRows = this.parseObjectRows(this.form.extraConfig)
this.title = '编辑平台'
this.open = true
})
@ -220,11 +236,33 @@ export default {
this.currentPlatformName = row.name
this.$nextTick(() => { this.$refs.interfaceDrawer.open() })
},
parseObjectRows(value) {
if (!value) return []
try {
const parsed = JSON.parse(value)
return Object.keys(parsed).map(key => ({
key,
value: parsed[key] == null ? '' : String(parsed[key]),
description: ''
}))
} catch (e) {
return []
}
},
buildObjectJson(rows) {
const result = {}
;(rows || []).forEach(item => {
if (!item || !item.key) return
result[item.key] = item.value
})
return Object.keys(result).length > 0 ? JSON.stringify(result) : null
},
cancel() { this.open = false },
submitForm() {
this.$refs.form.validate(valid => {
if (!valid) return
this.form.authConfig = JSON.stringify(this.authConfigParsed)
this.form.extraConfig = this.buildObjectJson(this.platformConfigRows)
this.submitLoading = true
const api = this.form.id ? updatePlatformInstance : addPlatformInstance
api(this.form).then(() => {
@ -237,3 +275,11 @@ export default {
}
}
</script>
<style scoped>
.schema-tip {
font-size: 12px;
color: #909399;
line-height: 1.6;
}
</style>