20260315:1、网关产品支持一键导入子产品物模型;

2、网关设备支持跨子设备物模型计算公式;
3、历史数据支持网关设备数据分析
This commit is contained in:
George 2026-03-15 16:21:03 +08:00
parent ec4f8be1ad
commit 90b078323b
12 changed files with 1623 additions and 97 deletions

View File

@ -10,6 +10,7 @@ import com.fastbee.common.core.thingsModel.SceneThingsModelItem;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem; import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.common.exception.ServiceException; import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.spring.SpringUtils; import com.fastbee.common.utils.spring.SpringUtils;
import com.fastbee.iot.data.service.IFunctionInvoke; import com.fastbee.iot.data.service.IFunctionInvoke;
@ -76,7 +77,8 @@ public class SceneContext {
*/ */
private List<SceneThingsModelItem> sceneThingsModelItems; private List<SceneThingsModelItem> sceneThingsModelItems;
public SceneContext(String deviceNum, Long productId, int type, List<ThingsModelSimpleItem> thingsModelSimpleItems) { public SceneContext(String deviceNum, Long productId, int type,
List<ThingsModelSimpleItem> thingsModelSimpleItems) {
this.deviceNum = deviceNum; this.deviceNum = deviceNum;
this.productId = productId; this.productId = productId;
this.type = type; this.type = type;
@ -94,7 +96,7 @@ public class SceneContext {
String requestId = "scene/" + scriptTemplate.getChainName(); String requestId = "scene/" + scriptTemplate.getChainName();
scene_logger.info("[{}]=====+>场景联动规则开始执行!", requestId); scene_logger.info("[{}]=====+>场景联动规则开始执行!", requestId);
scene_logger.info("[{}]=====+>规则引擎脚本配置:{},规则引擎上下文:{}", requestId, scriptTemplate, this); scene_logger.info("[{}]=====+>规则引擎脚本配置:{},规则引擎上下文:{}", requestId, scriptTemplate, this);
//MDC.put("id", scriptTemplate.getChainName()); // MDC.put("id", scriptTemplate.getChainName());
// 触发器 // 触发器
if (scriptTemplate.getPurpose() == 2) { if (scriptTemplate.getPurpose() == 2) {
// 触发器,检查静默时间 // 触发器,检查静默时间
@ -142,6 +144,7 @@ public class SceneContext {
/*** /***
* 设备触发脚本处理 * 设备触发脚本处理
*
* @param scriptTemplate 解析后的Json脚本数据 * @param scriptTemplate 解析后的Json脚本数据
* @return * @return
*/ */
@ -169,18 +172,21 @@ public class SceneContext {
if (sceneThingsModelItems == null) { if (sceneThingsModelItems == null) {
sceneThingsModelItems = new ArrayList<>(); sceneThingsModelItems = new ArrayList<>();
} }
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), matchItem.getValue(), type, SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(),
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum, matchItem.getRemark()); matchItem.getValue(), type,
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
deviceNum, matchItem.getRemark());
sceneThingsModelItems.add(sceneItem); sceneThingsModelItems.add(sceneItem);
// 3.设备上报值匹配 // 3.设备上报值匹配
return matchValue(requestId, scriptTemplate.getOperator(), scriptTemplate.getValue(), matchItem.getValue()); return matchValue(requestId, scriptTemplate.getOperator(), scriptTemplate.getValue(),
matchItem.getValue());
} }
} else { } else {
//设备上线清理 离线延时匹配任务 // 设备上线清理 离线延时匹配任务
if (type == 5 && scriptTemplate.getCheckdelay() != 0) { if (type == 5 && scriptTemplate.getCheckdelay() != 0) {
String key = "CHECK_" + scriptTemplate.getSceneId() + deviceNum; String key = "CHECK_" + scriptTemplate.getSceneId() + deviceNum;
//设备上线删除告警任务 // 设备上线删除告警任务
ruleService.removeCheckTask(key); ruleService.removeCheckTask(key);
scene_logger.info("[{}]=====+>设备上线清理 离线延时匹配任务:{}", requestId, key); scene_logger.info("[{}]=====+>设备上线清理 离线延时匹配任务:{}", requestId, key);
} }
@ -190,8 +196,10 @@ public class SceneContext {
if (sceneThingsModelItems == null) { if (sceneThingsModelItems == null) {
sceneThingsModelItems = new ArrayList<>(); sceneThingsModelItems = new ArrayList<>();
} }
SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline", type == 5 ? "1" : "0", type, SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline",
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum, ""); type == 5 ? "1" : "0", type,
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
deviceNum, "");
sceneThingsModelItems.add(sceneItem); sceneThingsModelItems.add(sceneItem);
// 记录结果 // 记录结果
return true; return true;
@ -236,7 +244,8 @@ public class SceneContext {
sceneThingsModelItems = new ArrayList<>(); sceneThingsModelItems = new ArrayList<>();
} }
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), value, type, SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), value, type,
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), num, ""); scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
num, "");
sceneThingsModelItems.add(sceneItem); sceneThingsModelItems.add(sceneItem);
resultCount++; resultCount++;
} }
@ -249,6 +258,7 @@ public class SceneContext {
/*** /***
* 产品触发脚本处理 * 产品触发脚本处理
*
* @param scriptTemplate * @param scriptTemplate
* @return * @return
*/ */
@ -276,18 +286,21 @@ public class SceneContext {
if (sceneThingsModelItems == null) { if (sceneThingsModelItems == null) {
sceneThingsModelItems = new ArrayList<>(); sceneThingsModelItems = new ArrayList<>();
} }
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), matchItem.getValue(), type, SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(),
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum, matchItem.getRemark()); matchItem.getValue(), type,
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
deviceNum, matchItem.getRemark());
sceneThingsModelItems.add(sceneItem); sceneThingsModelItems.add(sceneItem);
// 设备上报值匹配 // 设备上报值匹配
return matchValue(requestId, scriptTemplate.getOperator(), scriptTemplate.getValue(), matchItem.getValue()); return matchValue(requestId, scriptTemplate.getOperator(), scriptTemplate.getValue(),
matchItem.getValue());
} }
} else { } else {
//设备上线清理 离线延时匹配任务 // 设备上线清理 离线延时匹配任务
if (type == 5 && scriptTemplate.getCheckdelay() != 0) { if (type == 5 && scriptTemplate.getCheckdelay() != 0) {
String key = "CHECK_" + scriptTemplate.getSceneId() + deviceNum; String key = "CHECK_" + scriptTemplate.getSceneId() + deviceNum;
//设备上线删除告警任务 // 设备上线删除告警任务
ruleService.removeCheckTask(key); ruleService.removeCheckTask(key);
scene_logger.info("[{}]=====+>设备上线清理 离线延时匹配任务:{}", requestId, key); scene_logger.info("[{}]=====+>设备上线清理 离线延时匹配任务:{}", requestId, key);
} }
@ -297,8 +310,10 @@ public class SceneContext {
if (sceneThingsModelItems == null) { if (sceneThingsModelItems == null) {
sceneThingsModelItems = new ArrayList<>(); sceneThingsModelItems = new ArrayList<>();
} }
SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline", type == 5 ? "1" : "0", type, SceneThingsModelItem sceneItem = new SceneThingsModelItem(type == 5 ? "online" : "offline",
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), deviceNum, ""); type == 5 ? "1" : "0", type,
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
deviceNum, "");
sceneThingsModelItems.add(sceneItem); sceneThingsModelItems.add(sceneItem);
// 记录结果 // 记录结果
return true; return true;
@ -344,7 +359,8 @@ public class SceneContext {
sceneThingsModelItems = new ArrayList<>(); sceneThingsModelItems = new ArrayList<>();
} }
SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), value, type, SceneThingsModelItem sceneItem = new SceneThingsModelItem(scriptTemplate.getId(), value, type,
scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(), num, ""); scriptTemplate.getScriptId(), scriptTemplate.getSceneId(), scriptTemplate.getProductId(),
num, "");
sceneThingsModelItems.add(sceneItem); sceneThingsModelItems.add(sceneItem);
resultCount++; resultCount++;
} }
@ -427,11 +443,11 @@ public class SceneContext {
alertLog.setCreateBy(scriptTemplate.getId()); alertLog.setCreateBy(scriptTemplate.getId());
alertLog.setSerialNumber(deviceNum); alertLog.setSerialNumber(deviceNum);
alertLog.setStatus(3); alertLog.setStatus(3);
//自动设置告警处理状态 // 自动设置告警处理状态
alertLogService.updateAlertLogStatus(alertLog); alertLogService.updateAlertLogStatus(alertLog);
//如果存在延时确认任务,则删除 // 如果存在延时确认任务,则删除
String key = "CHECK_" + scriptTemplate.getId() + deviceNum; String key = "CHECK_" + scriptTemplate.getId() + deviceNum;
//设备上线删除告警任务 // 设备上线删除告警任务
ruleService.removeCheckTask(key); ruleService.removeCheckTask(key);
scene_logger.info("[{}]=====+>告警恢复!", requestId); scene_logger.info("[{}]=====+>告警恢复!", requestId);
} }
@ -442,14 +458,17 @@ public class SceneContext {
* @param sceneId 场景ID * @param sceneId 场景ID
*/ */
private void alert(Long sceneId, boolean isNotify) { private void alert(Long sceneId, boolean isNotify) {
Set<Long> sceneIdSet = sceneThingsModelItems.stream().map(SceneThingsModelItem::getSceneId).collect(Collectors.toSet()); Set<Long> sceneIdSet = sceneThingsModelItems.stream().map(SceneThingsModelItem::getSceneId)
.collect(Collectors.toSet());
List<SceneTerminalUserVO> sceneTerminalUserVOList = sceneService.selectTerminalUserBySceneIds(sceneIdSet); List<SceneTerminalUserVO> sceneTerminalUserVOList = sceneService.selectTerminalUserBySceneIds(sceneIdSet);
Map<Long, SceneTerminalUserVO> sceneTerminalUserMap = sceneTerminalUserVOList.stream().collect(Collectors.toMap(SceneTerminalUserVO::getSceneId, Function.identity())); Map<Long, SceneTerminalUserVO> sceneTerminalUserMap = sceneTerminalUserVOList.stream()
.collect(Collectors.toMap(SceneTerminalUserVO::getSceneId, Function.identity()));
List<AlertLog> alertLogList = new ArrayList<>(); List<AlertLog> alertLogList = new ArrayList<>();
for (SceneThingsModelItem sceneThingsModelItem : sceneThingsModelItems) { for (SceneThingsModelItem sceneThingsModelItem : sceneThingsModelItems) {
// 查询设备信息 // 查询设备信息
Device device = deviceService.selectDeviceBySerialNumber(sceneThingsModelItem.getDeviceNumber()); Device device = deviceService.selectDeviceBySerialNumber(sceneThingsModelItem.getDeviceNumber());
Optional.ofNullable(device).orElseThrow(() -> new ServiceException("告警推送,设备不存在" + "[{" + sceneThingsModelItem.getDeviceNumber() + "}]")); Optional.ofNullable(device).orElseThrow(
() -> new ServiceException("告警推送,设备不存在" + "[{" + sceneThingsModelItem.getDeviceNumber() + "}]"));
// 判断是否是终端用户的场景 // 判断是否是终端用户的场景
SceneTerminalUserVO sceneTerminalUserVO = sceneTerminalUserMap.get(sceneId); SceneTerminalUserVO sceneTerminalUserVO = sceneTerminalUserMap.get(sceneId);
if (1 == sceneTerminalUserVO.getTerminalUser()) { if (1 == sceneTerminalUserVO.getTerminalUser()) {
@ -469,7 +488,8 @@ public class SceneContext {
// 开启通知发送 // 开启通知发送
if (isNotify) { if (isNotify) {
AlertPushParams alertPushParams = buildAlertPushParams(device, sceneThingsModelItem); AlertPushParams alertPushParams = buildAlertPushParams(device, sceneThingsModelItem);
List<AlertNotifyTemplate> alertNotifyTemplateList = alertService.listAlertNotifyTemplate(alertSceneSendVO.getAlertId()); List<AlertNotifyTemplate> alertNotifyTemplateList = alertService
.listAlertNotifyTemplate(alertSceneSendVO.getAlertId());
alertPushParams.setAlertName(alertSceneSendVO.getAlertName()); alertPushParams.setAlertName(alertSceneSendVO.getAlertName());
for (AlertNotifyTemplate alertNotifyTemplate : alertNotifyTemplateList) { for (AlertNotifyTemplate alertNotifyTemplate : alertNotifyTemplateList) {
alertPushParams.setNotifyTemplateId(alertNotifyTemplate.getNotifyTemplateId()); alertPushParams.setNotifyTemplateId(alertNotifyTemplate.getNotifyTemplateId());
@ -492,15 +512,23 @@ public class SceneContext {
alertPushParams.setDeviceId(device.getDeviceId()); alertPushParams.setDeviceId(device.getDeviceId());
alertPushParams.setDeviceName(device.getDeviceName()); alertPushParams.setDeviceName(device.getDeviceName());
alertPushParams.setSerialNumber(sceneThingsModelItem.getDeviceNumber()); alertPushParams.setSerialNumber(sceneThingsModelItem.getDeviceNumber());
// List<DeviceUser> deviceUserList = deviceUserService.getDeviceUserAndShare(device.getDeviceId()); // List<DeviceUser> deviceUserList = deviceUserService.getDeviceUserAndShare(device.getDeviceId());
// 多租户改版查询自己配置的告警用户 // 多租户改版查询自己配置的告警用户 默认优先推送本账号超级管理员
Set<Long> userIdSet = new HashSet<>();
try {
userIdSet.add(SecurityUtils.getUserId());
} catch (Exception e) {
userIdSet.add(device.getUserId() == null ? device.getTenantId() : device.getUserId());
}
DeviceAlertUser deviceAlertUser = new DeviceAlertUser(); DeviceAlertUser deviceAlertUser = new DeviceAlertUser();
deviceAlertUser.setDeviceId(device.getDeviceId()); deviceAlertUser.setDeviceId(device.getDeviceId());
List<DeviceAlertUser> deviceUserList = deviceAlertUserService.selectDeviceAlertUserList(deviceAlertUser); List<DeviceAlertUser> deviceUserList = deviceAlertUserService.selectDeviceAlertUserList(deviceAlertUser);
if (CollectionUtils.isNotEmpty(deviceUserList)) { if (CollectionUtils.isNotEmpty(deviceUserList)) {
alertPushParams.setUserPhoneSet(deviceUserList.stream().map(DeviceAlertUser::getPhoneNumber).filter(StringUtils::isNotEmpty).collect(Collectors.toSet())); alertPushParams.setUserPhoneSet(deviceUserList.stream().map(DeviceAlertUser::getPhoneNumber)
alertPushParams.setUserIdSet(deviceUserList.stream().map(DeviceAlertUser::getUserId).collect(Collectors.toSet())); .filter(StringUtils::isNotEmpty).collect(Collectors.toSet()));
userIdSet.addAll(deviceUserList.stream().map(DeviceAlertUser::getUserId).collect(Collectors.toSet()));
} }
alertPushParams.setUserIdSet(userIdSet);
String address; String address;
if (StringUtils.isNotEmpty(device.getNetworkAddress())) { if (StringUtils.isNotEmpty(device.getNetworkAddress())) {
address = device.getNetworkAddress(); address = device.getNetworkAddress();
@ -546,7 +574,8 @@ public class SceneContext {
* @return com.fastbee.iot.domain.AlertLog * @return com.fastbee.iot.domain.AlertLog
* @param: device * @param: device
*/ */
private AlertLog getAlertLog(AlertSceneSendVO alertSceneSendVO, Device device, SceneThingsModelItem sceneThingsModelItem) { private AlertLog getAlertLog(AlertSceneSendVO alertSceneSendVO, Device device,
SceneThingsModelItem sceneThingsModelItem) {
AlertLog alertLog = new AlertLog(); AlertLog alertLog = new AlertLog();
alertLog.setAlertName(alertSceneSendVO.getAlertName()); alertLog.setAlertName(alertSceneSendVO.getAlertName());
alertLog.setAlertLevel(alertSceneSendVO.getAlertLevel()); alertLog.setAlertLevel(alertSceneSendVO.getAlertLevel());
@ -609,14 +638,16 @@ public class SceneContext {
// 比较值用英文中划线分割 - // 比较值用英文中划线分割 -
String[] triggerValues = triggerValue.split("-"); String[] triggerValues = triggerValue.split("-");
if (isNumeric(value) && isNumeric(triggerValues[0]) && isNumeric(triggerValues[1])) { if (isNumeric(value) && isNumeric(triggerValues[0]) && isNumeric(triggerValues[1])) {
result = Double.parseDouble(value) >= Double.parseDouble(triggerValues[0]) && Double.parseDouble(value) <= Double.parseDouble(triggerValues[1]); result = Double.parseDouble(value) >= Double.parseDouble(triggerValues[0])
&& Double.parseDouble(value) <= Double.parseDouble(triggerValues[1]);
} }
break; break;
case "notBetween": case "notBetween":
// 比较值用英文中划线分割 - // 比较值用英文中划线分割 -
String[] trigValues = triggerValue.split("-"); String[] trigValues = triggerValue.split("-");
if (isNumeric(value) && isNumeric(trigValues[0]) && isNumeric(trigValues[1])) { if (isNumeric(value) && isNumeric(trigValues[0]) && isNumeric(trigValues[1])) {
result = Double.parseDouble(value) <= Double.parseDouble(trigValues[0]) || Double.parseDouble(value) >= Double.parseDouble(trigValues[1]); result = Double.parseDouble(value) <= Double.parseDouble(trigValues[0])
|| Double.parseDouble(value) >= Double.parseDouble(trigValues[1]);
} }
break; break;
case "contain": case "contain":

View File

@ -7,8 +7,10 @@ import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.core.domain.AjaxResult; import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.domain.ThingsModel;
import com.fastbee.iot.model.AlertCountVO; import com.fastbee.iot.model.AlertCountVO;
import com.fastbee.iot.model.DeviceHistoryParam; import com.fastbee.iot.model.DeviceHistoryParam;
import com.fastbee.iot.model.SubDeviceCompareParam;
import com.fastbee.iot.model.ThingsModelLogCountVO; import com.fastbee.iot.model.ThingsModelLogCountVO;
import com.fastbee.iot.model.dto.ThingsModelDTO; import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.param.DataCenterParam; import com.fastbee.iot.model.param.DataCenterParam;
@ -66,6 +68,184 @@ public class DataCenterController {
return AjaxResult.success(jsonObject); return AjaxResult.success(jsonObject);
} }
@ApiOperation("网关产品-可导入的子产品物模型列表(按identifier去重)")
@PreAuthorize("@ss.hasPermi('iot:model:list')")
@GetMapping("/subProductThingsModel")
public AjaxResult listSubProductThingsModel(@RequestParam Long gwProductId) {
List<ThingsModel> list = dataCenterService.listSubProductThingsModelForImport(gwProductId);
return AjaxResult.success(list);
}
@ApiOperation("网关产品-一键导入子产品物模型")
@PreAuthorize("@ss.hasPermi('iot:model:add')")
@PostMapping("/importSubProductThingsToGateway")
public AjaxResult importSubProductThingsToGateway(@RequestBody Map<String, Long> body) {
Long gwProductId = body != null ? body.get("gwProductId") : null;
if (gwProductId == null) {
return AjaxResult.error("请传入网关产品ID");
}
int count = dataCenterService.importSubProductThingsToGateway(gwProductId);
return AjaxResult.success("导入成功,共 " + count + " 个物模型", count);
}
@ApiOperation("网关计算物模型历史(查询时按公式计算,按分钟对齐,缺数沿用上一分钟)")
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
@PostMapping("/deviceHistoryCalculated")
public AjaxResult deviceHistoryCalculated(@RequestBody DeviceHistoryParam deviceHistoryParam) {
if (deviceHistoryParam.getDeviceId() == null || StringUtils.isEmpty(deviceHistoryParam.getSerialNumber())) {
return AjaxResult.error("请选择网关设备");
}
if (StringUtils.isNotEmpty(deviceHistoryParam.getSerialNumber())) {
deviceHistoryParam.setSerialNumber(deviceHistoryParam.getSerialNumber().toUpperCase());
}
List<JSONObject> list = dataCenterService.deviceHistoryCalculated(deviceHistoryParam);
return AjaxResult.success(list);
}
@ApiOperation("导出网关计算物模型历史Excel")
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
@PostMapping("/deviceHistoryCalculated/export")
public void exportDeviceHistoryCalculated(@RequestBody DeviceHistoryParam deviceHistoryParam,
HttpServletResponse response) throws IOException {
if (deviceHistoryParam.getDeviceId() == null || StringUtils.isEmpty(deviceHistoryParam.getSerialNumber())) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("请选择网关设备");
return;
}
if (StringUtils.isNotEmpty(deviceHistoryParam.getSerialNumber())) {
deviceHistoryParam.setSerialNumber(deviceHistoryParam.getSerialNumber().toUpperCase());
}
List<JSONObject> dataList = dataCenterService.deviceHistoryCalculated(deviceHistoryParam);
if (dataList == null || dataList.isEmpty()) {
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.getWriter().write("没有数据可导出");
return;
}
List<com.fastbee.iot.model.dto.ThingsModelDTO> modelList = dataCenterService
.listGatewayCalculatedModels(deviceHistoryParam.getDeviceId());
Map<String, String> identifierNameMap = new LinkedHashMap<>();
for (com.fastbee.iot.model.dto.ThingsModelDTO m : modelList) {
identifierNameMap.put(m.getIdentifier(), m.getModelName() != null ? m.getModelName() : m.getIdentifier());
}
List<List<String>> headList = new ArrayList<>();
headList.add(Collections.singletonList("时间"));
List<String> identifierList = new ArrayList<>();
if (deviceHistoryParam.getIdentifierList() != null) {
for (DeviceHistoryParam.IdentifierVO vo : deviceHistoryParam.getIdentifierList()) {
identifierList.add(vo.getIdentifier());
headList.add(Collections
.singletonList(identifierNameMap.getOrDefault(vo.getIdentifier(), vo.getIdentifier())));
}
}
List<List<Object>> rows = new ArrayList<>();
for (JSONObject jsonObject : dataList) {
String time = jsonObject.keySet().iterator().next();
Object dataValue = jsonObject.get(time);
List<Object> rowData = new ArrayList<>();
rowData.add(time);
if (dataValue instanceof List) {
@SuppressWarnings("unchecked")
List<JSONObject> identifierDataList = (List<JSONObject>) dataValue;
Map<String, Object> valueMap = new HashMap<>();
for (JSONObject identifierObj : identifierDataList) {
for (Map.Entry<String, Object> entry : identifierObj.entrySet()) {
valueMap.put(entry.getKey(), entry.getValue());
}
}
for (String id : identifierList) {
rowData.add(valueMap.get(id));
}
}
rows.add(rowData);
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder
.encode(deviceHistoryParam.getSerialNumber() + "_calculated_"
+ DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, new Date()), "UTF-8")
.replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).head(headList).build();
WriteSheet writeSheet = EasyExcel.writerSheet(0, "网关计算量历史").build();
excelWriter.write(rows, writeSheet);
excelWriter.finish();
}
@ApiOperation("网关设备仅计算物模型列表(历史记录-网关计算量 变量下拉用)")
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
@GetMapping("/gatewayCalculatedModels")
public AjaxResult listGatewayCalculatedModels(@RequestParam Long deviceId) {
return AjaxResult.success(dataCenterService.listGatewayCalculatedModels(deviceId));
}
@ApiOperation("多设备对比历史(支持多变量,按分钟对齐,缺数沿用上一分钟)")
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
@PostMapping("/subDeviceComparativeHistory")
public AjaxResult subDeviceComparativeHistory(@RequestBody SubDeviceCompareParam param) {
boolean hasIdentifier = (param.getIdentifierList() != null && !param.getIdentifierList().isEmpty())
|| !StringUtils.isEmpty(param.getIdentifier());
if (param.getGwDeviceId() == null || !hasIdentifier) {
return AjaxResult.error("请选择网关设备和变量");
}
if (param.getType() == null) {
param.setType(1);
}
List<JSONObject> list = dataCenterService.subDeviceComparativeHistory(param);
return AjaxResult.success(list);
}
@ApiOperation("导出多设备对比历史Excel")
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
@PostMapping("/subDeviceComparativeHistory/export")
public void exportSubDeviceComparativeHistory(@RequestBody SubDeviceCompareParam param,
HttpServletResponse response) throws IOException {
boolean hasIdentifier = (param.getIdentifierList() != null && !param.getIdentifierList().isEmpty())
|| !StringUtils.isEmpty(param.getIdentifier());
if (param.getGwDeviceId() == null || !hasIdentifier) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("请选择网关设备和变量");
return;
}
if (param.getType() == null) {
param.setType(1);
}
List<JSONObject> dataList = dataCenterService.subDeviceComparativeHistory(param);
if (dataList == null || dataList.isEmpty()) {
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.getWriter().write("没有数据可导出");
return;
}
List<List<String>> headList = new ArrayList<>();
headList.add(Collections.singletonList("时间"));
Set<String> keys = new LinkedHashSet<>(dataList.get(0).keySet());
keys.remove("time");
for (String k : keys) {
headList.add(Collections.singletonList(k));
}
List<List<Object>> rows = new ArrayList<>();
for (JSONObject row : dataList) {
List<Object> r = new ArrayList<>();
r.add(row.getString("time"));
for (String k : keys) {
r.add(row.get(k));
}
rows.add(r);
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String idPart = (param.getIdentifierList() != null && !param.getIdentifierList().isEmpty())
? String.join("-", param.getIdentifierList()) : param.getIdentifier();
String fileName = URLEncoder
.encode("sub_compare_" + idPart + "_"
+ DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, new Date()), "UTF-8")
.replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).head(headList).build();
WriteSheet writeSheet = EasyExcel.writerSheet(0, "多设备对比").build();
excelWriter.write(rows, writeSheet);
excelWriter.finish();
}
@ApiOperation("查询场景变量历史数据") @ApiOperation("查询场景变量历史数据")
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')") @PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
@GetMapping("/sceneHistory") @GetMapping("/sceneHistory")
@ -138,7 +318,8 @@ public class DataCenterController {
@ApiOperation("导出设备历史数据Excel") @ApiOperation("导出设备历史数据Excel")
@PreAuthorize("@ss.hasPermi('dataCenter:history:list')") @PreAuthorize("@ss.hasPermi('dataCenter:history:list')")
@PostMapping("/deviceHistory/export") @PostMapping("/deviceHistory/export")
public void exportDeviceHistory(@RequestBody DeviceHistoryParam deviceHistoryParam, HttpServletResponse response) throws IOException { public void exportDeviceHistory(@RequestBody DeviceHistoryParam deviceHistoryParam, HttpServletResponse response)
throws IOException {
final String serialNumber = deviceHistoryParam.getSerialNumber(); final String serialNumber = deviceHistoryParam.getSerialNumber();
if (StringUtils.isEmpty(serialNumber)) { if (StringUtils.isEmpty(serialNumber)) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
@ -178,11 +359,13 @@ public class DataCenterController {
for (String identifier : identifierList) { for (String identifier : identifierList) {
ThingsModelDTO thingsModel = thingsModelMap.get(identifier); ThingsModelDTO thingsModel = thingsModelMap.get(identifier);
String headerName = thingsModel != null && StringUtils.isNotEmpty(thingsModel.getModelName()) String headerName = thingsModel != null && StringUtils.isNotEmpty(thingsModel.getModelName())
? thingsModel.getModelName() : identifier; ? thingsModel.getModelName()
: identifier;
identifierNameMap.put(identifier, headerName); identifierNameMap.put(identifier, headerName);
// 获取数据类型 // 获取数据类型
String dataType = "string"; // 默认字符串类型 String dataType = "string"; // 默认字符串类型
if (thingsModel != null && thingsModel.getDatatype() != null && StringUtils.isNotEmpty(thingsModel.getDatatype().getType())) { if (thingsModel != null && thingsModel.getDatatype() != null
&& StringUtils.isNotEmpty(thingsModel.getDatatype().getType())) {
dataType = thingsModel.getDatatype().getType(); dataType = thingsModel.getDatatype().getType();
} }
identifierTypeMap.put(identifier, dataType); identifierTypeMap.put(identifier, dataType);
@ -220,11 +403,13 @@ public class DataCenterController {
for (String identifier : identifierSet) { for (String identifier : identifierSet) {
ThingsModelDTO thingsModel = thingsModelMap.get(identifier); ThingsModelDTO thingsModel = thingsModelMap.get(identifier);
String headerName = thingsModel != null && StringUtils.isNotEmpty(thingsModel.getModelName()) String headerName = thingsModel != null && StringUtils.isNotEmpty(thingsModel.getModelName())
? thingsModel.getModelName() : identifier; ? thingsModel.getModelName()
: identifier;
identifierNameMap.put(identifier, headerName); identifierNameMap.put(identifier, headerName);
// 获取数据类型 // 获取数据类型
String dataType = "string"; // 默认字符串类型 String dataType = "string"; // 默认字符串类型
if (thingsModel != null && thingsModel.getDatatype() != null && StringUtils.isNotEmpty(thingsModel.getDatatype().getType())) { if (thingsModel != null && thingsModel.getDatatype() != null
&& StringUtils.isNotEmpty(thingsModel.getDatatype().getType())) {
dataType = thingsModel.getDatatype().getType(); dataType = thingsModel.getDatatype().getType();
} }
identifierTypeMap.put(identifier, dataType); identifierTypeMap.put(identifier, dataType);
@ -290,7 +475,9 @@ public class DataCenterController {
// 设置响应头 // 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode(serialNumber + "_" + DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, new Date()), "UTF-8").replaceAll("\\+", "%20"); String fileName = URLEncoder
.encode(serialNumber + "_" + DateUtils.parseDateToStr(DateUtils.YYYYMMDDHHMMSS, new Date()), "UTF-8")
.replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 使用EasyExcel写入数据 // 使用EasyExcel写入数据

View File

@ -0,0 +1,30 @@
package com.fastbee.iot.model;
import lombok.Data;
import java.util.List;
/**
* 多设备对比查询参数(支持多变量,历史记录按分钟对齐,缺数沿用上一分钟最后值)
* <p>
* 传 identifierList 时支持多变量对比;仅传 identifier 时按单变量兼容。
* </p>
*
* @author fastbee
*/
@Data
public class SubDeviceCompareParam {
/** 网关设备ID */
private Long gwDeviceId;
/** 物模型标识符(单变量时使用,与 identifierList 二选一) */
private String identifier;
/** 物模型标识符列表(多变量对比时使用,优先于 identifier) */
private List<String> identifierList;
/** 指定参与对比的子设备ID列表;为空或 null 时表示全部子设备 */
private List<Long> subDeviceIds;
/** 类型 1-属性 2-功能 3-事件,对比仅支持属性 */
private Integer type;
private String beginTime;
private String endTime;
}

View File

@ -1,17 +1,16 @@
package com.fastbee.iot.service; package com.fastbee.iot.service;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.fastbee.iot.domain.ThingsModel;
import com.fastbee.iot.model.AlertCountVO; import com.fastbee.iot.model.AlertCountVO;
import com.fastbee.iot.model.DeviceHistoryParam; import com.fastbee.iot.model.DeviceHistoryParam;
import com.fastbee.iot.model.HistoryModel; import com.fastbee.iot.model.SubDeviceCompareParam;
import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.ThingsModelLogCountVO; import com.fastbee.iot.model.ThingsModelLogCountVO;
import com.fastbee.iot.model.param.DataCenterParam; import com.fastbee.iot.model.param.DataCenterParam;
import com.fastbee.iot.model.scenemodel.SceneHistoryParam; import com.fastbee.iot.model.scenemodel.SceneHistoryParam;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 数据中心服务类 * 数据中心服务类
@ -30,6 +29,38 @@ public interface DataCenterService {
*/ */
List<JSONObject> deviceHistory(DeviceHistoryParam deviceHistoryParam); List<JSONObject> deviceHistory(DeviceHistoryParam deviceHistoryParam);
/**
* 网关产品:查询绑定子产品的物模型列表(用于一键导入,按 identifier 去重)
*
* @param gwProductId 网关产品ID
* @return 去重后的物模型列表
*/
List<ThingsModel> listSubProductThingsModelForImport(Long gwProductId);
/**
* 网关产品:一键将绑定子产品的物模型导入到网关产品(按 identifier 去重,已存在则跳过)
*
* @param gwProductId 网关产品ID
* @return 本次导入数量
*/
int importSubProductThingsToGateway(Long gwProductId);
/**
* 网关计算物模型历史:仅查询时计算,按分钟对齐,缺数沿用上一分钟最后值;公式支持 {slaveId:N.identifier}
*
* @param deviceHistoryParam 网关设备ID、计算物模型 identifier 列表、时间范围
* @return 与 deviceHistory 相同格式
*/
List<JSONObject> deviceHistoryCalculated(DeviceHistoryParam deviceHistoryParam);
/**
* 多设备同一物理量对比历史:按分钟对齐,缺数沿用上一分钟最后值
*
* @param param 网关设备ID、identifier、时间范围
* @return 每行格式:{ "time": "yyyy-MM-dd HH:mm:ss", "设备标签1": v1, "设备标签2": v2, ... }
*/
List<JSONObject> subDeviceComparativeHistory(SubDeviceCompareParam param);
/** /**
* 查询场景变量的历史数据 * 查询场景变量的历史数据
* *
@ -61,4 +92,12 @@ public interface DataCenterService {
* @return com.fastbee.common.core.domain.AjaxResult * @return com.fastbee.common.core.domain.AjaxResult
*/ */
List<ThingsModelLogCountVO> countThingsModelInvoke(DataCenterParam dataCenterParam); List<ThingsModelLogCountVO> countThingsModelInvoke(DataCenterParam dataCenterParam);
/**
* 网关设备仅计算物模型列表(用于历史记录-网关计算量 变量选择)
*
* @param gwDeviceId 网关设备ID
* @return 仅计算物模型的 DTO 列表
*/
List<ThingsModelDTO> listGatewayCalculatedModels(Long gwDeviceId);
} }

View File

@ -3,14 +3,19 @@ package com.fastbee.iot.service.impl;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.enums.scenemodel.SceneModelVariableTypeEnum; import com.fastbee.common.enums.scenemodel.SceneModelVariableTypeEnum;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.CaculateUtils;
import com.fastbee.iot.cache.SceneModelTagCache; import com.fastbee.iot.cache.SceneModelTagCache;
import com.fastbee.iot.domain.*; import com.fastbee.iot.domain.*;
import com.fastbee.iot.model.AlertCountVO; import com.fastbee.iot.model.AlertCountVO;
import com.fastbee.iot.model.DeviceHistoryParam; import com.fastbee.iot.model.DeviceHistoryParam;
import com.fastbee.iot.model.HistoryModel; import com.fastbee.iot.model.HistoryModel;
import com.fastbee.iot.model.SubDeviceCompareParam;
import com.fastbee.iot.model.ThingsModelLogCountVO; import com.fastbee.iot.model.ThingsModelLogCountVO;
import com.fastbee.iot.model.dto.ThingsModelDTO; import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.gateWay.ProductSubGatewayVO;
import com.fastbee.iot.model.gateWay.SubDeviceListVO;
import com.fastbee.iot.model.param.DataCenterParam; import com.fastbee.iot.model.param.DataCenterParam;
import com.fastbee.iot.model.scenemodel.SceneHistoryParam; import com.fastbee.iot.model.scenemodel.SceneHistoryParam;
import com.fastbee.iot.service.*; import com.fastbee.iot.service.*;
@ -20,6 +25,8 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -47,6 +54,17 @@ public class DataCenterServiceImpl implements DataCenterService {
private SceneModelTagCache sceneModelTagCache; private SceneModelTagCache sceneModelTagCache;
@Resource @Resource
private IAlertLogService alertLogService; private IAlertLogService alertLogService;
@Resource
private IProductSubGatewayService productSubGatewayService;
@Resource
private ISubGatewayService subGatewayService;
@Resource
private IProductService productService;
@Resource
private com.fastbee.iot.cache.ITSLCache itslCache;
/** 公式中跨设备引用:{slaveId:N.identifier} */
private static final Pattern CROSS_DEVICE_REF = Pattern.compile("\\{slaveId:(\\d+)\\.([^}]+)\\}");
@Override @Override
public List<JSONObject> deviceHistory(DeviceHistoryParam deviceHistoryParam) { public List<JSONObject> deviceHistory(DeviceHistoryParam deviceHistoryParam) {
@ -66,9 +84,11 @@ public class DataCenterServiceImpl implements DataCenterService {
if (CollectionUtils.isEmpty(identifierVOList)) { if (CollectionUtils.isEmpty(identifierVOList)) {
return resultList; return resultList;
} }
List<String> identifierList = identifierVOList.stream().map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList()); List<String> identifierList = identifierVOList.stream().map(DeviceHistoryParam.IdentifierVO::getIdentifier)
.collect(Collectors.toList());
List<HistoryModel> historyModelList = new ArrayList<>(); List<HistoryModel> historyModelList = new ArrayList<>();
List<String> propertyIdentifierList = identifierVOList.stream().filter(t -> 1 == t.getType()).map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList()); List<String> propertyIdentifierList = identifierVOList.stream().filter(t -> 1 == t.getType())
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(propertyIdentifierList)) { if (CollectionUtils.isNotEmpty(propertyIdentifierList)) {
DeviceLog deviceLog = new DeviceLog(); DeviceLog deviceLog = new DeviceLog();
deviceLog.setIdentityList(propertyIdentifierList); deviceLog.setIdentityList(propertyIdentifierList);
@ -77,16 +97,19 @@ public class DataCenterServiceImpl implements DataCenterService {
deviceLog.setEndTime(deviceHistoryParam.getEndTime()); deviceLog.setEndTime(deviceHistoryParam.getEndTime());
historyModelList.addAll(deviceLogService.listHistory(deviceLog)); historyModelList.addAll(deviceLogService.listHistory(deviceLog));
} }
List<String> functionIdentifierList = identifierVOList.stream().filter(t -> 2 == t.getType()).map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList()); List<String> functionIdentifierList = identifierVOList.stream().filter(t -> 2 == t.getType())
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(functionIdentifierList)) { if (CollectionUtils.isNotEmpty(functionIdentifierList)) {
FunctionLog functionLog = new FunctionLog(); FunctionLog functionLog = new FunctionLog();
functionLog.setIdentifyList(functionIdentifierList); functionLog.setIdentifyList(functionIdentifierList);
functionLog.setSerialNumber(deviceHistoryParam.getSerialNumber()); functionLog.setSerialNumber(deviceHistoryParam.getSerialNumber());
functionLog.setBeginTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getBeginTime())); functionLog
.setBeginTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getBeginTime()));
functionLog.setEndTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getEndTime())); functionLog.setEndTime(DateUtils.dateTime(DateUtils.YY_MM_DD_HH_MM_SS, deviceHistoryParam.getEndTime()));
historyModelList.addAll(functionLogService.listHistory(functionLog)); historyModelList.addAll(functionLogService.listHistory(functionLog));
} }
List<String> eventIdentifierList = identifierVOList.stream().filter(t -> 3 == t.getType()).map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList()); List<String> eventIdentifierList = identifierVOList.stream().filter(t -> 3 == t.getType())
.map(DeviceHistoryParam.IdentifierVO::getIdentifier).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(eventIdentifierList)) { if (CollectionUtils.isNotEmpty(eventIdentifierList)) {
EventLog eventLog = new EventLog(); EventLog eventLog = new EventLog();
eventLog.setIdentityList(eventIdentifierList); eventLog.setIdentityList(eventIdentifierList);
@ -109,7 +132,8 @@ public class DataCenterServiceImpl implements DataCenterService {
for (Map.Entry<Date, List<HistoryModel>> entry : map.entrySet()) { for (Map.Entry<Date, List<HistoryModel>> entry : map.entrySet()) {
JSONObject jsonObject = new JSONObject(); JSONObject jsonObject = new JSONObject();
List<HistoryModel> value = entry.getValue(); List<HistoryModel> value = entry.getValue();
Map<String, HistoryModel> historyModelMap = value.stream().collect(Collectors.toMap(HistoryModel::getIdentity, Function.identity(), (o, n) -> n)); Map<String, HistoryModel> historyModelMap = value.stream()
.collect(Collectors.toMap(HistoryModel::getIdentity, Function.identity(), (o, n) -> n));
List<JSONObject> jsonObjectList = new ArrayList<>(); List<JSONObject> jsonObjectList = new ArrayList<>();
for (String identifier : identifierList) { for (String identifier : identifierList) {
JSONObject jsonObject1 = new JSONObject(); JSONObject jsonObject1 = new JSONObject();
@ -140,11 +164,14 @@ public class DataCenterServiceImpl implements DataCenterService {
return new ArrayList<>(); return new ArrayList<>();
} }
List<String> sceneModelDataIdList = StringUtils.str2List(sceneHistoryParam.getIds(), ",", true, true); List<String> sceneModelDataIdList = StringUtils.str2List(sceneHistoryParam.getIds(), ",", true, true);
List<SceneModelData> sceneModelDataList = sceneModelDataService.selectSceneModelDataListByIds(sceneModelDataIdList.stream().map(Long::parseLong).collect(Collectors.toList())); List<SceneModelData> sceneModelDataList = sceneModelDataService.selectSceneModelDataListByIds(
List<Long> datasourceIdList = sceneModelDataList.stream().map(SceneModelData::getDatasourceId).distinct().collect(Collectors.toList()); sceneModelDataIdList.stream().map(Long::parseLong).collect(Collectors.toList()));
List<Long> datasourceIdList = sceneModelDataList.stream().map(SceneModelData::getDatasourceId).distinct()
.collect(Collectors.toList());
if (SceneModelVariableTypeEnum.THINGS_MODEL.getType().equals(sceneHistoryParam.getVariableType())) { if (SceneModelVariableTypeEnum.THINGS_MODEL.getType().equals(sceneHistoryParam.getVariableType())) {
List<ThingsModel> thingsModelList = thingsModelService.selectThingsModelListByModelIds(datasourceIdList); List<ThingsModel> thingsModelList = thingsModelService.selectThingsModelListByModelIds(datasourceIdList);
Map<Long, Integer> thingsModelMap = thingsModelList.stream().collect(Collectors.toMap(ThingsModel::getModelId, ThingsModel::getType)); Map<Long, Integer> thingsModelMap = thingsModelList.stream()
.collect(Collectors.toMap(ThingsModel::getModelId, ThingsModel::getType));
DeviceHistoryParam deviceHistoryParam = new DeviceHistoryParam(); DeviceHistoryParam deviceHistoryParam = new DeviceHistoryParam();
List<DeviceHistoryParam.IdentifierVO> identifierVOList = new ArrayList<>(); List<DeviceHistoryParam.IdentifierVO> identifierVOList = new ArrayList<>();
for (SceneModelData sceneModelData : sceneModelDataList) { for (SceneModelData sceneModelData : sceneModelDataList) {
@ -189,4 +216,354 @@ public class DataCenterServiceImpl implements DataCenterService {
resultList.addAll(functionLogService.countThingsModelInvoke(dataCenterParam)); resultList.addAll(functionLogService.countThingsModelInvoke(dataCenterParam));
return resultList; return resultList;
} }
@Override
public List<ThingsModel> listSubProductThingsModelForImport(Long gwProductId) {
ProductSubGateway query = new ProductSubGateway();
query.setGwProductId(gwProductId);
List<ProductSubGatewayVO> list = productSubGatewayService.selectProductSubGatewayList(query);
if (CollectionUtils.isEmpty(list)) {
return new ArrayList<>();
}
List<Long> subProductIds = list.stream().map(ProductSubGatewayVO::getSubProductId).distinct()
.collect(Collectors.toList());
Map<String, ThingsModel> dedup = new LinkedHashMap<>();
ThingsModel q = new ThingsModel();
q.setLanguage(SecurityUtils.getLanguage());
q.setType(1);
for (Long subId : subProductIds) {
q.setProductId(subId);
List<ThingsModel> models = thingsModelService.selectThingsModelList(q);
for (ThingsModel m : models) {
dedup.putIfAbsent(m.getIdentifier(), m);
}
}
return new ArrayList<>(dedup.values());
}
@Override
public int importSubProductThingsToGateway(Long gwProductId) {
Product gwProduct = productService.selectProductByProductId(gwProductId);
if (gwProduct == null) {
return 0;
}
List<ThingsModel> toImport = listSubProductThingsModelForImport(gwProductId);
ThingsModel existQ = new ThingsModel();
existQ.setProductId(gwProductId);
existQ.setLanguage(SecurityUtils.getLanguage());
Set<String> existingIds = thingsModelService.selectThingsModelList(existQ).stream()
.map(ThingsModel::getIdentifier).collect(Collectors.toSet());
int count = 0;
for (ThingsModel m : toImport) {
if (existingIds.contains(m.getIdentifier())) {
continue;
}
ThingsModel add = new ThingsModel();
add.setProductId(gwProductId);
add.setProductName(gwProduct.getProductName());
add.setModelName(m.getModelName());
add.setIdentifier(m.getIdentifier());
add.setType(m.getType());
add.setDatatype(m.getDatatype());
add.setSpecs(m.getSpecs());
add.setFormula(m.getFormula());
add.setIsCalculated(m.getIsCalculated() != null ? m.getIsCalculated() : 0);
add.setIsChart(m.getIsChart() != null ? m.getIsChart() : 0);
add.setIsHistory(m.getIsHistory() != null ? m.getIsHistory() : 1);
add.setIsMonitor(m.getIsMonitor() != null ? m.getIsMonitor() : 0);
add.setIsReadonly(m.getIsReadonly() != null ? m.getIsReadonly() : 1);
add.setDeviceId(0L);
add.setDelFlag("0");
if (SecurityUtils.getLoginUser() != null && SecurityUtils.getLoginUser().getUser() != null) {
if (SecurityUtils.getLoginUser().getUser().getDept() != null) {
add.setTenantId(SecurityUtils.getLoginUser().getUser().getDept().getDeptUserId());
add.setTenantName(SecurityUtils.getLoginUser().getUser().getDept().getDeptUserName());
}
}
thingsModelService.insertThingsModel(add);
existingIds.add(add.getIdentifier());
count++;
}
if (count > 0) {
itslCache.setCacheThingsModelByProductId(gwProductId);
}
return count;
}
@Override
public List<JSONObject> deviceHistoryCalculated(DeviceHistoryParam deviceHistoryParam) {
if (deviceHistoryParam.getDeviceId() == null || StringUtils.isEmpty(deviceHistoryParam.getSerialNumber())) {
return Collections.emptyList();
}
Device device = deviceService.selectDeviceByDeviceId(deviceHistoryParam.getDeviceId());
if (device == null) {
return Collections.emptyList();
}
List<DeviceHistoryParam.IdentifierVO> identifierVOList = deviceHistoryParam.getIdentifierList();
if (CollectionUtils.isEmpty(identifierVOList)) {
return Collections.emptyList();
}
ThingsModel queryModel = new ThingsModel();
queryModel.setProductId(device.getProductId());
queryModel.setIsCalculated(1);
List<ThingsModel> allCalculated = thingsModelService.selectThingsModelList(queryModel);
long gwDeviceId = device.getDeviceId();
allCalculated = allCalculated.stream()
.filter(m -> m.getDeviceId() == null || m.getDeviceId() == 0
|| m.getDeviceId().longValue() == gwDeviceId)
.collect(Collectors.toList());
Set<String> calculatedIdentifiers = allCalculated.stream().map(ThingsModel::getIdentifier)
.collect(Collectors.toSet());
List<String> requestedIds = identifierVOList.stream().map(DeviceHistoryParam.IdentifierVO::getIdentifier)
.collect(Collectors.toList());
requestedIds = requestedIds.stream().filter(calculatedIdentifiers::contains).collect(Collectors.toList());
if (requestedIds.isEmpty()) {
return Collections.emptyList();
}
SubGateway subQ = new SubGateway();
subQ.setGwDeviceId(device.getDeviceId());
List<SubDeviceListVO> subDevices = subGatewayService.selectGatewayList(subQ);
// String gwSn = device.getSerialNumber();
List<HistoryModel> computedHistory = new ArrayList<>();
for (String calcIdentifier : requestedIds) {
ThingsModel model = allCalculated.stream().filter(m -> m.getIdentifier().equals(calcIdentifier)).findFirst()
.orElse(null);
if (model == null || StringUtils.isEmpty(model.getFormula())) {
continue;
}
Map<String, String> refs = parseCrossDeviceRefs(model.getFormula());
if (refs.isEmpty()) {
continue;
}
Map<String, List<HistoryModel>> rawByRef = new LinkedHashMap<>();
Set<Long> minutesWithData = new TreeSet<>();
for (Map.Entry<String, String> e : refs.entrySet()) {
String key = e.getKey();
String ident = e.getValue();
int slaveId = Integer.parseInt(key.split("\\.")[0].replace("slaveId:", ""));
String subSn = subDevices.stream().filter(s -> s.getSlaveId() != null && s.getSlaveId() == slaveId)
.findFirst().map(SubDeviceListVO::getSubDeviceNo).orElse(null);
if (subSn == null) {
continue;
}
DeviceLog dl = new DeviceLog();
dl.setSerialNumber(subSn);
dl.setIdentityList(Collections.singletonList(ident));
dl.setBeginTime(deviceHistoryParam.getBeginTime());
dl.setEndTime(deviceHistoryParam.getEndTime());
List<HistoryModel> raw = deviceLogService.listHistory(dl);
rawByRef.put(key, raw);
for (HistoryModel h : raw) {
if (h.getTime() != null) {
long minuteMs = (h.getTime().getTime() / 60_000L) * 60_000L;
minutesWithData.add(minuteMs);
}
}
}
if (minutesWithData.isEmpty()) {
continue;
}
List<Date> minuteSlotsOnlyWithData = new ArrayList<>();
for (Long ms : minutesWithData) {
minuteSlotsOnlyWithData.add(new Date(ms));
}
Map<String, LinkedHashMap<Date, String>> alignedByRef = new LinkedHashMap<>();
for (Map.Entry<String, List<HistoryModel>> entry : rawByRef.entrySet()) {
LinkedHashMap<Date, String> aligned = alignByMinuteWithCarryForward(entry.getValue(),
minuteSlotsOnlyWithData);
alignedByRef.put(entry.getKey(), aligned);
}
for (Date minute : minuteSlotsOnlyWithData) {
Map<String, String> replaceMap = new HashMap<>();
for (Map.Entry<String, LinkedHashMap<Date, String>> entry : alignedByRef.entrySet()) {
String val = entry.getValue().get(minute);
replaceMap.put("{" + entry.getKey() + "}", val != null ? val : "0");
}
try {
String computed = CaculateUtils.execute(model.getFormula(), replaceMap).toString();
HistoryModel hm = new HistoryModel();
hm.setTime(minute);
hm.setIdentity(calcIdentifier);
hm.setValue(computed);
computedHistory.add(hm);
} catch (Exception ignored) {
}
}
}
computedHistory.sort(Comparator.comparing(HistoryModel::getTime));
return handleData(requestedIds, computedHistory);
}
/** 多设备对比列名分隔符:设备标签:::变量标识,用于多变量时前端解析展示名 */
private static final String SUB_COMPARE_COL_SEP = ":::";
@Override
public List<JSONObject> subDeviceComparativeHistory(SubDeviceCompareParam param) {
List<String> identifiers = resolveCompareIdentifiers(param);
if (param.getGwDeviceId() == null || identifiers.isEmpty() || param.getType() == null
|| param.getType() != 1) {
return Collections.emptyList();
}
Device gw = deviceService.selectDeviceByDeviceId(param.getGwDeviceId());
if (gw == null) {
return Collections.emptyList();
}
SubGateway subQ = new SubGateway();
subQ.setGwDeviceId(param.getGwDeviceId());
List<SubDeviceListVO> subList = subGatewayService.selectGatewayList(subQ);
if (CollectionUtils.isEmpty(subList)) {
return Collections.emptyList();
}
if (param.getSubDeviceIds() != null && !param.getSubDeviceIds().isEmpty()) {
subList = subList.stream()
.filter(s -> param.getSubDeviceIds().contains(s.getSubDeviceId()))
.collect(Collectors.toList());
if (subList.isEmpty()) {
return Collections.emptyList();
}
}
String gwSn = gw.getSerialNumber();
Set<Long> minutesWithData = new TreeSet<>();
Map<String, List<HistoryModel>> rawByDevice = new LinkedHashMap<>();
for (SubDeviceListVO sub : subList) {
String subSn = sub.getSubDeviceNo() != null ? sub.getSubDeviceNo() : (gwSn + "_" + sub.getSlaveId());
String label = (sub.getSubDeviceName() != null ? sub.getSubDeviceName() : ("从机" + sub.getSlaveId())) + "(从机"
+ sub.getSlaveId() + ")";
DeviceLog dl = new DeviceLog();
dl.setSerialNumber(subSn);
dl.setIdentityList(identifiers);
dl.setBeginTime(param.getBeginTime());
dl.setEndTime(param.getEndTime());
List<HistoryModel> raw = deviceLogService.listHistory(dl);
rawByDevice.put(label, raw);
for (HistoryModel h : raw) {
if (h.getTime() != null) {
long minuteMs = (h.getTime().getTime() / 60_000L) * 60_000L;
minutesWithData.add(minuteMs);
}
}
}
List<Date> minuteSlotsOnlyWithData = new ArrayList<>();
for (Long ms : minutesWithData) {
minuteSlotsOnlyWithData.add(new Date(ms));
}
boolean singleVar = (identifiers.size() == 1);
Map<String, LinkedHashMap<Date, String>> deviceAligned = new LinkedHashMap<>();
for (Map.Entry<String, List<HistoryModel>> e : rawByDevice.entrySet()) {
String label = e.getKey();
List<HistoryModel> raw = e.getValue();
if (singleVar) {
LinkedHashMap<Date, String> aligned = alignByMinuteWithCarryForward(raw, minuteSlotsOnlyWithData);
deviceAligned.put(label, aligned);
} else {
Map<String, List<HistoryModel>> byIdentity = raw.stream()
.filter(h -> h.getIdentity() != null)
.collect(Collectors.groupingBy(HistoryModel::getIdentity));
for (Map.Entry<String, List<HistoryModel>> entry : byIdentity.entrySet()) {
LinkedHashMap<Date, String> aligned = alignByMinuteWithCarryForward(entry.getValue(),
minuteSlotsOnlyWithData);
deviceAligned.put(label + SUB_COMPARE_COL_SEP + entry.getKey(), aligned);
}
}
}
List<JSONObject> result = new ArrayList<>();
for (Date minute : minuteSlotsOnlyWithData) {
JSONObject row = new JSONObject();
row.put("time", DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, minute));
for (Map.Entry<String, LinkedHashMap<Date, String>> e : deviceAligned.entrySet()) {
row.put(e.getKey(), e.getValue().get(minute));
}
result.add(row);
}
return result;
}
private List<String> resolveCompareIdentifiers(SubDeviceCompareParam param) {
if (param.getIdentifierList() != null && !param.getIdentifierList().isEmpty()) {
return param.getIdentifierList();
}
if (param.getIdentifier() != null && !param.getIdentifier().isEmpty()) {
return Collections.singletonList(param.getIdentifier());
}
return Collections.emptyList();
}
@Override
public List<ThingsModelDTO> listGatewayCalculatedModels(Long gwDeviceId) {
Device device = deviceService.selectDeviceByDeviceId(gwDeviceId);
if (device == null) {
return new ArrayList<>();
}
ThingsModel queryModel = new ThingsModel();
queryModel.setProductId(device.getProductId());
queryModel.setLanguage(SecurityUtils.getLanguage());
queryModel.setIsCalculated(1);
List<ThingsModel> list = thingsModelService.selectThingsModelList(queryModel);
long gwId = device.getDeviceId();
list = list.stream()
.filter(m -> m.getDeviceId() == null || m.getDeviceId() == 0 || m.getDeviceId().longValue() == gwId)
.collect(Collectors.toList());
List<ThingsModelDTO> result = new ArrayList<>();
for (ThingsModel m : list) {
ThingsModelDTO dto = new ThingsModelDTO();
dto.setModelId(m.getModelId());
dto.setModelName(m.getModelName());
dto.setIdentifier(m.getIdentifier());
dto.setType(m.getType());
dto.setProductId(m.getProductId());
dto.setSerialNumber(device.getSerialNumber());
result.add(dto);
}
return result;
}
private Map<String, String> parseCrossDeviceRefs(String formula) {
Map<String, String> refs = new LinkedHashMap<>();
Matcher m = CROSS_DEVICE_REF.matcher(formula);
while (m.find()) {
String key = "slaveId:" + m.group(1) + "." + m.group(2);
refs.put(key, m.group(2));
}
return refs;
}
private List<Date> buildMinuteSlots(String beginTime, String endTime) {
Date start = DateUtils.parseDate(beginTime);
Date end = DateUtils.parseDate(endTime);
if (start == null || end == null || !start.before(end)) {
return new ArrayList<>();
}
long step = 60_000L;
long startMs = (start.getTime() / step) * step;
long endMs = (end.getTime() / step) * step;
List<Date> slots = new ArrayList<>();
for (long t = startMs; t <= endMs; t += step) {
slots.add(new Date(t));
}
return slots;
}
private LinkedHashMap<Date, String> alignByMinuteWithCarryForward(List<HistoryModel> raw, List<Date> minuteSlots) {
if (minuteSlots.isEmpty()) {
return new LinkedHashMap<>();
}
TreeMap<Long, String> byMinute = new TreeMap<>();
for (HistoryModel h : raw) {
if (h.getTime() == null || h.getValue() == null)
continue;
long minuteMs = (h.getTime().getTime() / 60_000L) * 60_000L;
byMinute.put(minuteMs, h.getValue());
}
LinkedHashMap<Date, String> result = new LinkedHashMap<>();
String last = null;
for (Date slot : minuteSlots) {
long slotMs = (slot.getTime() / 60_000L) * 60_000L;
Map.Entry<Long, String> floor = byMinute.floorEntry(slotMs);
if (floor != null && floor.getKey() == slotMs) {
last = floor.getValue();
}
result.put(slot, last);
}
return result;
}
} }

View File

@ -1089,6 +1089,7 @@ public class DeviceServiceImpl implements IDeviceService {
SysUser sysUser = getLoginUser().getUser(); SysUser sysUser = getLoginUser().getUser();
// 添加设备 // 添加设备
device.setCreateTime(DateUtils.getNowDate()); device.setCreateTime(DateUtils.getNowDate());
device.setUserId(sysUser.getUserId());
device.setTenantId(sysUser.getDept().getDeptUserId()); device.setTenantId(sysUser.getDept().getDeptUserId());
device.setTenantName(sysUser.getDept().getDeptUserName()); device.setTenantName(sysUser.getDept().getDeptUserName());
device.setRssi(0); device.setRssi(0);
@ -1096,9 +1097,9 @@ public class DeviceServiceImpl implements IDeviceService {
Product product = productService.selectProductByProductId(device.getProductId()); Product product = productService.selectProductByProductId(device.getProductId());
device.setImgUrl(product.getImgUrl()); device.setImgUrl(product.getImgUrl());
// 随机经纬度和地址 // 随机经纬度和地址
SysUser user = getLoginUser().getUser(); //SysUser user = getLoginUser().getUser();
device.setNetworkIp(user.getLoginIp()); device.setNetworkIp(sysUser.getLoginIp());
setLocation(user.getLoginIp(), device); setLocation(sysUser.getLoginIp(), device);
deviceMapper.insertDevice(device); deviceMapper.insertDevice(device);
// 处理modbus产品 // 处理modbus产品
if (FastBeeConstant.PROTOCOL.ModbusRtu.equals(product.getProtocolCode()) if (FastBeeConstant.PROTOCOL.ModbusRtu.equals(product.getProtocolCode())

View File

@ -54,3 +54,68 @@ export function exportDeviceHistory(data) {
responseType: 'blob', responseType: 'blob',
}); });
} }
// 网关产品-可导入的子产品物模型列表
export function getSubProductThingsModel(gwProductId) {
return request({
url: '/data/center/subProductThingsModel',
method: 'get',
params: { gwProductId },
});
}
// 网关产品-一键导入子产品物模型
export function importSubProductThingsToGateway(gwProductId) {
return request({
url: '/data/center/importSubProductThingsToGateway',
method: 'post',
data: { gwProductId },
});
}
// 网关设备仅计算物模型列表
export function getGatewayCalculatedModels(deviceId) {
return request({
url: '/data/center/gatewayCalculatedModels',
method: 'get',
params: { deviceId },
});
}
// 网关计算物模型历史(查询时计算,按分钟对齐)
export function getDeviceHistoryCalculated(data) {
return request({
url: '/data/center/deviceHistoryCalculated',
method: 'post',
data,
});
}
// 导出网关计算物模型历史Excel
export function exportDeviceHistoryCalculated(data) {
return request({
url: '/data/center/deviceHistoryCalculated/export',
method: 'post',
data,
responseType: 'blob',
});
}
// 多设备同一物理量对比历史
export function getSubDeviceComparativeHistory(data) {
return request({
url: '/data/center/subDeviceComparativeHistory',
method: 'post',
data,
});
}
// 导出多设备对比历史Excel
export function exportSubDeviceComparativeHistory(data) {
return request({
url: '/data/center/subDeviceComparativeHistory/export',
method: 'post',
data,
responseType: 'blob',
});
}

View File

@ -45,5 +45,8 @@
"dataCenter.history.384934-21": "In the past seven days", "dataCenter.history.384934-21": "In the past seven days",
"dataCenter.history.384934-22": "Last 30 days", "dataCenter.history.384934-22": "Last 30 days",
"dataCenter.history.384934-23": "total", "dataCenter.history.384934-23": "total",
"dataCenter.history.384934-24": "Export Excel" "dataCenter.history.384934-24": "Export Excel",
"dataCenter.history.gwCalculated": "Gateway Calculated",
"dataCenter.history.subCompare": "Sub-Device Compare",
"dataCenter.history.gateway": "Gateway Device"
} }

View File

@ -45,5 +45,8 @@
"dataCenter.history.384934-21": "最近七天", "dataCenter.history.384934-21": "最近七天",
"dataCenter.history.384934-22": "最近三十天", "dataCenter.history.384934-22": "最近三十天",
"dataCenter.history.384934-23": "总量", "dataCenter.history.384934-23": "总量",
"dataCenter.history.384934-24": "导出Excel" "dataCenter.history.384934-24": "导出Excel",
"dataCenter.history.gwCalculated": "网关计算量",
"dataCenter.history.subCompare": "多设备对比",
"dataCenter.history.gateway": "网关设备"
} }

View File

@ -7,7 +7,7 @@
<el-form @submit.native.prevent :model="devQueryParams" ref="devQueryForm" size="mini" :inline="true" label-width="68px"> <el-form @submit.native.prevent :model="devQueryParams" ref="devQueryForm" size="mini" :inline="true" label-width="68px">
<el-form-item :label="$t('dataCenter.history.384934-1')" prop="deviceId"> <el-form-item :label="$t('dataCenter.history.384934-1')" prop="deviceId">
<el-select style="width: 100%" v-model="devQueryParams.deviceId" :placeholder="$t('dataCenter.history.384934-2')" filterable @change="handleDevDeviceChange"> <el-select style="width: 100%" v-model="devQueryParams.deviceId" :placeholder="$t('dataCenter.history.384934-2')" filterable @change="handleDevDeviceChange">
<el-option v-for="(item, index) in devDeviceList" :key="index" :label="item.deviceName" :value="item.deviceId"></el-option> <el-option v-for="(item, index) in devDeviceListNonGw" :key="index" :label="item.deviceName" :value="item.deviceId"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="devSubDeviceList.length > 0" label="子设备" prop="subDeviceId"> <el-form-item v-if="devSubDeviceList.length > 0" label="子设备" prop="subDeviceId">
@ -70,6 +70,95 @@
</el-row> </el-row>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="网关设备" name="gateway">
<div class="gateway-wrap">
<!-- 统一 4 步:1 选网关 2 选物模型 3 选子设备(可选) 4 选时间 -->
<el-form @submit.native.prevent :model="gwQueryParams" ref="gwQueryForm" size="mini" :inline="true" label-width="90px">
<el-form-item label="网关设备" prop="gwDeviceId">
<el-select style="width: 200px" v-model="gwQueryParams.gwDeviceId" placeholder="请选择网关设备" filterable @change="handleGwDeviceChange">
<el-option v-for="item in gwDeviceList" :key="item.deviceId" :label="item.deviceName" :value="item.deviceId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="物模型" prop="identifiers">
<el-select style="width: 320px" v-model="gwQueryParams.identifiers" placeholder="请选择变量(可多选)" filterable multiple collapse-tags @change="handleGwIdentifierChange">
<el-option label="全部勾选/反选" value="__selectAll__">
<span style="font-weight: bold; color: #409EFF;">{{ gwGwIsAllSelected ? '全部反选' : '全部勾选' }}</span>
</el-option>
<el-option-group label="计算量(网关纯计算,无需选子设备)" v-if="gwVariableListCalcUnique.length">
<el-option v-for="item in gwVariableListCalcUnique" :key="'calc-' + item.identifier" :label="item.modelName + ' (计算)'" :value="item.identifier"></el-option>
</el-option-group>
<el-option-group label="子设备变量" v-if="gwVariableListSubUnique.length">
<el-option v-for="item in gwVariableListSubUnique" :key="'sub-' + item.identifier" :label="item.modelName" :value="item.identifier"></el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="子设备" prop="subDeviceIds" v-if="gwShowSubDeviceSelect">
<el-select
style="width: 280px"
v-model="gwSubDeviceSelectValue"
placeholder="不选则全部子设备对比,可多选指定子设备"
filterable
multiple
collapse-tags
clearable
>
<el-option label="全部子设备(多设备对比)" value="__all__"></el-option>
<el-option v-for="item in gwSubDeviceList" :key="item.subDeviceId" :label="item.subDeviceName + ' (从机' + item.slaveId + ')'" :value="item.subDeviceId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="dayDaterange">
<el-date-picker
style="width: 315px"
v-model="gwQueryParams.dayDaterange"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
:picker-options="pickerOptions"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleGwQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="handleGwReset">重置</el-button>
<el-button type="success" icon="el-icon-download" size="mini" :disabled="!gwHasResult" @click="handleGwExport" v-loading="exportLoading">导出Excel</el-button>
</el-form-item>
</el-form>
<el-card v-loading="loading" shadow="never">
<div slot="header"><span>{{ gwResultTitle }}</span></div>
<!-- 计算量结果 -->
<template v-if="gwResultMode === 'calc'">
<div v-show="gwCalcDatas.length !== 0" ref="gwCalcLineChart" style="width: 100%; height: 480px"></div>
<el-empty v-if="gwCalcDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型(计算量)和时间范围后查询"></el-empty>
<el-table v-show="gwCalcTotal > 0" style="margin-top: 20px" :data="gwCalcTableList" border>
<el-table-column label="更新时间" prop="time" width="180" />
<el-table-column v-for="item in gwCalcTableHeaderTemp" :key="item.value" :label="item.name" :prop="item.value" />
</el-table>
<pagination v-show="gwCalcTotal > 0" :total="gwCalcTotal" :page.sync="gwCalcPageNum" :limit.sync="gwCalcPageSize" @pagination="getGwCalcTableList" />
</template>
<!-- 多设备对比结果 -->
<template v-else-if="gwResultMode === 'compare'">
<div v-show="subCompareDatas.length !== 0" ref="subCompareLineChart" style="width: 100%; height: 480px"></div>
<el-empty v-if="subCompareDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型(子设备变量)和时间范围,子设备可不选(多设备对比)"></el-empty>
<el-table v-show="subCompareTotal > 0" style="margin-top: 20px" :data="subCompareTableList" border>
<el-table-column v-for="col in subCompareTableColumns" :key="col.prop" :label="col.label" :prop="col.prop" :min-width="col.prop === 'time' ? 180 : 100" />
</el-table>
<pagination v-show="subCompareTotal > 0" :total="subCompareTotal" :page.sync="subComparePageNum" :limit.sync="subComparePageSize" @pagination="getSubCompareTableList" />
</template>
<!-- 单子设备历史结果 -->
<template v-else-if="gwResultMode === 'single'">
<div v-show="gwSingleDatas.length !== 0" ref="gwSingleLineChart" style="width: 100%; height: 480px"></div>
<el-empty v-if="gwSingleDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型、子设备和时间范围后查询"></el-empty>
<el-table v-show="gwSingleTotal > 0" style="margin-top: 20px" :data="gwSingleTableList" border>
<el-table-column label="更新时间" prop="time" width="180" />
<el-table-column v-for="item in gwSingleTableHeaderTemp" :key="item.value" :label="item.name" :prop="item.value" />
</el-table>
<pagination v-show="gwSingleTotal > 0" :total="gwSingleTotal" :page.sync="gwSinglePageNum" :limit.sync="gwSinglePageSize" @pagination="getGwSingleTableList" />
</template>
<el-empty v-else style="height: 480px" description="请按顺序选择:1.网关设备 2.物模型 3.子设备(可选) 4.时间范围,然后查询"></el-empty>
</el-card>
</div>
</el-tab-pane>
<el-tab-pane :label="$t('dataCenter.history.384934-14')" name="scene"> <el-tab-pane :label="$t('dataCenter.history.384934-14')" name="scene">
<div class="scene-wrap"> <div class="scene-wrap">
<el-form @submit.native.prevent :model="sceneQueryParams" ref="sceneQueryForm" size="mini" :inline="true" label-width="68px"> <el-form @submit.native.prevent :model="sceneQueryParams" ref="sceneQueryForm" size="mini" :inline="true" label-width="68px">
@ -155,7 +244,16 @@ import moment from 'moment';
import { listDeviceShort, listThingsModel } from '@/api/iot/device.js'; import { listDeviceShort, listThingsModel } from '@/api/iot/device.js';
import { listGateway } from '@/api/iot/gateway.js'; import { listGateway } from '@/api/iot/gateway.js';
import { getSceneModelList, getSceneModelDetail, getSceneModelDataList } from '@/api/scene/list.js'; import { getSceneModelList, getSceneModelDetail, getSceneModelDataList } from '@/api/scene/list.js';
import { getDataCenterDeviceHistory, getDataCenterSceneHistory, exportDeviceHistory } from '@/api/data/center.js'; import {
getDataCenterDeviceHistory,
getDataCenterSceneHistory,
exportDeviceHistory,
getGatewayCalculatedModels,
getDeviceHistoryCalculated,
exportDeviceHistoryCalculated,
getSubDeviceComparativeHistory,
exportSubDeviceComparativeHistory,
} from '@/api/data/center.js';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { blobValidate } from '@/utils/ruoyi'; import { blobValidate } from '@/utils/ruoyi';
@ -246,8 +344,40 @@ export default {
scenePageNum: 1, scenePageNum: 1,
scenePageSize: 10, scenePageSize: 10,
sceneTotal: 0, sceneTotal: 0,
// 导出loading
exportLoading: false, exportLoading: false,
// 网关设备 tab 统一 4 步(物模型支持多选)
gwQueryParams: {
gwDeviceId: null,
identifiers: [],
/** 子设备:null=全部,[id]=单设备,[id1,id2,...]=多设备对比指定 */
subDeviceIds: null,
dayDaterange: [new Date(Date.now() - 3600 * 1000 * 2), new Date()],
},
gwSubDeviceList: [],
gwCalcIdentifierList: [],
subCompareIdentifierList: [],
gwResultMode: '', // 'calc' | 'compare' | 'single'
// 计算量结果(复用原结构)
gwCalcDatas: [],
gwCalcTableHeaderTemp: [],
gwCalcTableComTemp: [],
gwCalcTotal: 0,
gwCalcPageNum: 1,
gwCalcPageSize: 10,
// 多设备对比结果
subCompareDatas: [],
subCompareTableColumns: [],
subCompareTableComTemp: [],
subCompareTotal: 0,
subComparePageNum: 1,
subComparePageSize: 10,
// 单子设备历史结果
gwSingleDatas: [],
gwSingleTableHeaderTemp: [],
gwSingleTableComTemp: [],
gwSingleTotal: 0,
gwSinglePageNum: 1,
gwSinglePageSize: 10,
}; };
}, },
mounted() { mounted() {
@ -301,6 +431,107 @@ export default {
return allIdentifiers.length > 0 && return allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id)); allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id));
}, },
/** 设备 tab 仅展示非网关设备 */
devDeviceListNonGw() {
return (this.devDeviceList || []).filter((d) => d.deviceType !== 2 && d.deviceType !== 4);
},
/** 网关设备 tab 仅展示网关设备 */
gwDeviceList() {
return (this.devDeviceList || []).filter((d) => d.deviceType === 2);
},
gwCalcTableList() {
const start = (this.gwCalcPageNum - 1) * this.gwCalcPageSize;
return (this.gwCalcTableComTemp || []).slice(start, start + this.gwCalcPageSize);
},
subCompareTableList() {
const start = (this.subComparePageNum - 1) * this.subComparePageSize;
return (this.subCompareTableComTemp || []).slice(start, start + this.subComparePageSize);
},
// 网关统一:计算量变量列表(用于下拉分组)
gwVariableListCalc() {
return this.gwCalcIdentifierList || [];
},
// 计算量按 identifier 去重,避免重复 key
gwVariableListCalcUnique() {
const list = this.gwCalcIdentifierList || [];
const seen = new Set();
return list.filter((i) => {
if (seen.has(i.identifier)) return false;
seen.add(i.identifier);
return true;
});
},
// 网关统一:子设备变量列表
gwVariableListSub() {
return this.subCompareIdentifierList || [];
},
// 子设备变量按 identifier 去重,且排除计算量(跨设备计算量不展示在子设备变量中)
gwVariableListSubUnique() {
const list = this.subCompareIdentifierList || [];
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
const seen = new Set();
return list.filter((i) => {
if (seen.has(i.identifier) || calcSet.has(i.identifier)) return false;
seen.add(i.identifier);
return true;
});
},
// 当前选中的物模型是否全部为计算量(计算量时不展示子设备选择)
gwSelectedIsCalculated() {
const ids = this.gwQueryParams.identifiers || [];
if (ids.length === 0) return false;
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
return ids.every((id) => calcSet.has(id));
},
// 选中的计算量 identifier 列表
gwSelectedCalcIdentifiers() {
const ids = this.gwQueryParams.identifiers || [];
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
return ids.filter((id) => calcSet.has(id));
},
// 选中的子设备变量 identifier 列表(排除同时属于计算量的,避免仅选计算量时报混合选择)
gwSelectedSubIdentifiers() {
const ids = this.gwQueryParams.identifiers || [];
const subSet = new Set((this.subCompareIdentifierList || []).map((i) => i.identifier));
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
return ids.filter((id) => subSet.has(id) && !calcSet.has(id));
},
// 网关物模型“全部勾选”仅针对子设备变量(不包含计算量)
gwGwIsAllSelected() {
const ids = this.gwQueryParams.identifiers || [];
const allSub = this.gwVariableListSubUnique.map((i) => i.identifier);
return allSub.length > 0 && allSub.every((id) => ids.includes(id));
},
gwShowSubDeviceSelect() {
return (this.gwQueryParams.identifiers || []).length > 0 && !this.gwSelectedIsCalculated;
},
/** 子设备多选绑定:null 显示为“全部”,选具体设备时去掉“全部” */
gwSubDeviceSelectValue: {
get() {
const ids = this.gwQueryParams.subDeviceIds;
return ids === null || (Array.isArray(ids) && ids.length === 0) ? ['__all__'] : ids;
},
set(val) {
const ids = (val || []).filter((v) => v !== '__all__');
this.gwQueryParams.subDeviceIds = ids.length === 0 ? null : ids;
},
},
gwResultTitle() {
if (this.gwResultMode === 'calc') return '曲线趋势图(计算量)';
if (this.gwResultMode === 'compare') return '多设备同一变量对比(仅显示有数据的时间点)';
if (this.gwResultMode === 'single') return '单子设备历史';
return '曲线趋势图';
},
gwHasResult() {
if (this.gwResultMode === 'calc') return this.gwCalcDatas.length > 0;
if (this.gwResultMode === 'compare') return this.subCompareDatas.length > 0;
if (this.gwResultMode === 'single') return this.gwSingleDatas.length > 0;
return false;
},
gwSingleTableList() {
const start = (this.gwSinglePageNum - 1) * this.gwSinglePageSize;
return (this.gwSingleTableComTemp || []).slice(start, start + this.gwSinglePageSize);
},
}, },
methods: { methods: {
// 获取设备列表 // 获取设备列表
@ -317,18 +548,13 @@ export default {
} }
}); });
}, },
// 选择设备后 // 选择设备后(设备 tab 仅非网关,直接拉取物模型)
handleDevDeviceChange(val) { handleDevDeviceChange(val) {
this.devQueryParams.identifiers = []; this.devQueryParams.identifiers = [];
this.devQueryParams.subDeviceId = null; this.devQueryParams.subDeviceId = null;
this.devSubDeviceList = []; this.devSubDeviceList = [];
this.devIdentifierList = []; this.devIdentifierList = [];
const device = this.devDeviceList.find((item) => item.deviceId === val); if (val) this.getDevIdentifierList(val);
if (device && device.deviceType === 2) {
this.getDevSubDeviceList(val);
} else {
this.getDevIdentifierList(val);
}
}, },
// 选择子设备后 // 选择子设备后
handleDevSubDeviceChange(val) { handleDevSubDeviceChange(val) {
@ -647,6 +873,434 @@ export default {
}; };
}); });
}, },
// 网关统一:选网关后拉取计算量变量、子设备变量、子设备列表
handleGwDeviceChange(val) {
this.gwQueryParams.identifiers = [];
this.gwQueryParams.subDeviceIds = null;
this.gwCalcIdentifierList = [];
this.subCompareIdentifierList = [];
this.gwSubDeviceList = [];
this.gwResultMode = '';
if (!val) return;
Promise.all([
getGatewayCalculatedModels(val).then((res) => (res.code === 200 ? res.data || [] : [])),
listThingsModel({ deviceId: val, pageNum: 1, pageSize: 9999 }).then((res) => {
if (res.code === 200) {
const rows = res.rows || [];
return rows.filter((r) => r.type === 1);
}
return [];
}),
listGateway({ gwDeviceId: val, pageNum: 1, pageSize: 9999 }).then((res) => (res.code === 200 ? res.rows || [] : [])),
]).then(([calcList, subList, subDevices]) => {
this.gwCalcIdentifierList = calcList;
this.subCompareIdentifierList = subList;
this.gwSubDeviceList = subDevices;
});
},
handleGwIdentifierChange(val) {
const raw = (val || []).filter((v) => v !== '__selectAll__');
const index = (val || []).indexOf('__selectAll__');
if (index > -1) {
// 全部勾选/反选:仅作用于子设备变量,不勾选计算量
const allSub = this.gwVariableListSubUnique.map((i) => i.identifier);
this.$nextTick(() => {
this.gwQueryParams.identifiers = this.gwGwIsAllSelected ? [] : [...allSub];
});
return;
}
// 计算量与子设备变量均支持多选,保留用户选择的全部项(混合选择在查询时校验)
this.gwQueryParams.identifiers = raw;
if (this.gwSelectedIsCalculated) this.gwQueryParams.subDeviceIds = null;
},
handleGwQuery() {
const { gwDeviceId, identifiers, subDeviceIds, dayDaterange } = this.gwQueryParams;
if (!gwDeviceId || !identifiers || identifiers.length === 0 || !dayDaterange || !dayDaterange.length) {
this.$message.warning('请选择网关设备、物模型和时间范围');
return;
}
const calcIds = this.gwSelectedCalcIdentifiers;
const subIds = this.gwSelectedSubIdentifiers;
if (calcIds.length > 0 && subIds.length > 0) {
this.$message.warning('不能混合选择计算量与子设备变量,请只选其中一类');
return;
}
const device = this.devDeviceList.find((d) => d.deviceId === gwDeviceId);
if (!device) return;
const isCalc = this.gwSelectedIsCalculated;
const singleDeviceId = Array.isArray(subDeviceIds) && subDeviceIds.length === 1 ? subDeviceIds[0] : null;
if (!isCalc && this.gwSubDeviceList.length > 0 && singleDeviceId) {
// 单子设备历史(多变量)
const sub = this.gwSubDeviceList.find((s) => s.subDeviceId === singleDeviceId);
if (!sub) {
this.$message.warning('请选择子设备');
return;
}
this.loading = true;
this.gwResultMode = 'single';
const identifierList = subIds.map((id) => {
const item = this.subCompareIdentifierList.find((i) => i.identifier === id) || {};
return { identifier: id, type: item.type || 1 };
});
const params = {
deviceId: sub.subDeviceId,
serialNumber: sub.subDeviceNo,
identifierList,
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
getDataCenterDeviceHistory(params).then((res) => {
if (res.code === 200) {
this.gwSingleDatas = res.data || [];
this.formatGwSingleTableDatas();
this.$nextTick(() => {
if (this.gwSingleDatas.length) this.drawGwSingleLine();
});
}
this.loading = false;
});
return;
}
if (!isCalc) {
// 多设备对比(支持多变量、指定子设备或全部)
this.loading = true;
this.gwResultMode = 'compare';
const params = {
gwDeviceId,
identifierList: subIds,
type: 1,
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
if (Array.isArray(subDeviceIds) && subDeviceIds.length > 0) {
params.subDeviceIds = subDeviceIds;
}
getSubDeviceComparativeHistory(params).then((res) => {
if (res.code === 200) {
this.subCompareDatas = res.data || [];
this.formatSubCompareTableDatas();
this.$nextTick(() => {
if (this.subCompareDatas.length) this.drawSubCompareLine();
});
}
this.loading = false;
});
return;
}
// 计算量(多变量)
this.loading = true;
this.gwResultMode = 'calc';
const identifierList = calcIds.map((id) => {
const item = this.gwCalcIdentifierList.find((i) => i.identifier === id) || {};
return { identifier: id, type: item.type || 1 };
});
const calcParams = {
deviceId: device.deviceId,
serialNumber: device.serialNumber,
identifierList,
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
getDeviceHistoryCalculated(calcParams).then((res) => {
if (res.code === 200) {
this.gwCalcDatas = res.data || [];
this.gwCalcTableHeaderTemp = calcIds.map((id) => {
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
return { name: (m && m.modelName) || id, value: id };
});
this.formatGwCalcTableDatas();
this.$nextTick(() => {
if (this.gwCalcDatas.length) this.drawGwCalcLine();
});
}
this.loading = false;
});
},
handleGwReset() {
this.gwQueryParams.gwDeviceId = null;
this.gwQueryParams.identifiers = [];
this.gwQueryParams.subDeviceIds = null;
this.gwQueryParams.dayDaterange = [new Date(Date.now() - 3600 * 1000 * 2), new Date()];
this.gwCalcIdentifierList = [];
this.subCompareIdentifierList = [];
this.gwSubDeviceList = [];
this.gwResultMode = '';
this.gwCalcDatas = [];
this.gwCalcTableComTemp = [];
this.gwCalcTotal = 0;
this.subCompareDatas = [];
this.subCompareTableComTemp = [];
this.subCompareTotal = 0;
this.gwSingleDatas = [];
this.gwSingleTableComTemp = [];
this.gwSingleTotal = 0;
},
async handleGwExport() {
const { gwDeviceId, identifiers, subDeviceIds, dayDaterange } = this.gwQueryParams;
if (!gwDeviceId || !identifiers || identifiers.length === 0 || !dayDaterange?.length) {
this.$message.warning('请先选择网关、物模型和时间范围');
return;
}
const device = this.devDeviceList.find((d) => d.deviceId === gwDeviceId);
if (!device) return;
const calcIds = this.gwSelectedCalcIdentifiers;
const subIds = this.gwSelectedSubIdentifiers;
if (calcIds.length > 0 && subIds.length > 0) {
this.$message.warning('不能混合选择计算量与子设备变量');
return;
}
try {
this.exportLoading = true;
const beginTime = moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss');
const endTime = moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss');
const singleId = Array.isArray(subDeviceIds) && subDeviceIds.length === 1 ? subDeviceIds[0] : null;
if (this.gwResultMode === 'single' && singleId) {
const sub = this.gwSubDeviceList.find((s) => s.subDeviceId === singleId);
if (!sub || this.gwSingleDatas.length === 0) {
this.$message.warning('没有数据可导出');
return;
}
const identifierList = subIds.map((id) => {
const item = this.subCompareIdentifierList.find((i) => i.identifier === id) || {};
return { identifier: id, type: item.type || 1 };
});
const params = { deviceId: sub.subDeviceId, serialNumber: sub.subDeviceNo, identifierList, beginTime, endTime };
const res = await exportDeviceHistory(params);
if (res instanceof Blob) {
const clonedBlob = res.slice();
const isValidBlob = await blobValidate(clonedBlob);
if (!isValidBlob) {
try {
const text = await res.text();
const err = JSON.parse(text);
this.$message.error(err.msg || '导出失败');
return;
} catch (e) {
this.$message.error('导出失败');
return;
}
}
saveAs(res, `${sub.subDeviceNo}_${subIds.join('-')}_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
this.$message.success('导出成功');
}
return;
}
if (this.gwResultMode === 'compare' && this.subCompareDatas.length > 0) {
const params = { gwDeviceId, identifierList: subIds, type: 1, beginTime, endTime };
if (Array.isArray(subDeviceIds) && subDeviceIds.length > 0) params.subDeviceIds = subDeviceIds;
const res = await exportSubDeviceComparativeHistory(params);
if (res instanceof Blob) {
const clonedBlob = res.slice();
const isValidBlob = await blobValidate(clonedBlob);
if (!isValidBlob) {
try {
const text = await res.text();
const err = JSON.parse(text);
this.$message.error(err.msg || '导出失败');
return;
} catch (e) {
this.$message.error('导出失败');
return;
}
}
const idPart = subIds.length ? subIds.join('-') : 'compare';
saveAs(res, `sub_compare_${idPart}_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
this.$message.success('导出成功');
}
return;
}
if (this.gwResultMode === 'calc' && this.gwCalcDatas.length > 0) {
const identifierList = calcIds.map((id) => {
const item = this.gwCalcIdentifierList.find((i) => i.identifier === id) || {};
return { identifier: id, type: item.type || 1 };
});
const params = { deviceId: device.deviceId, serialNumber: device.serialNumber, identifierList, beginTime, endTime };
const res = await exportDeviceHistoryCalculated(params);
if (res instanceof Blob) {
const clonedBlob = res.slice();
const isValidBlob = await blobValidate(clonedBlob);
if (!isValidBlob) {
try {
const text = await res.text();
const err = JSON.parse(text);
this.$message.error(err.msg || '导出失败');
return;
} catch (e) {
this.$message.error('导出失败');
return;
}
}
saveAs(res, `${device.serialNumber}_calculated_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
this.$message.success('导出成功');
}
return;
}
this.$message.warning('没有数据可导出');
} catch (e) {
this.$message.error(e.message || '导出失败');
} finally {
this.exportLoading = false;
}
},
getGwCalcTableList(e) {
if (e) {
this.gwCalcPageNum = e.page;
this.gwCalcPageSize = e.limit;
}
},
formatGwCalcTableDatas() {
this.gwCalcTableComTemp = (this.gwCalcDatas || []).map((item) => {
const time = Object.keys(item)[0];
const vals = Object.values(item)[0] || [];
const obj = { time };
vals.forEach((v) => {
const k = Object.keys(v)[0];
obj[k] = Object.values(v)[0];
});
return obj;
});
this.gwCalcTotal = this.gwCalcTableComTemp.length;
const ids = this.gwSelectedCalcIdentifiers;
this.gwCalcTableHeaderTemp = ids.map((id) => {
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
return { name: (m && m.modelName) || id, value: id };
});
},
drawGwCalcLine() {
if (!this.$refs.gwCalcLineChart) return;
const chart = this.$echarts.init(this.$refs.gwCalcLineChart);
chart.clear();
const ids = this.gwSelectedCalcIdentifiers;
const times = this.gwCalcDatas.map((d) => Object.keys(d)[0]);
const series = ids.map((id) => {
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
return {
name: (m && m.modelName) || id,
type: 'line',
smooth: true,
data: this.gwCalcDatas.map((d) => {
const arr = Object.values(d)[0] || [];
const o = arr.find((x) => Object.keys(x)[0] === id);
return o ? Object.values(o)[0] : null;
}),
};
});
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: series.map((s) => s.name) },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times },
yAxis: { type: 'value', scale: true },
series,
});
},
getGwSingleTableList(e) {
if (e) {
this.gwSinglePageNum = e.page;
this.gwSinglePageSize = e.limit;
}
},
formatGwSingleTableDatas() {
const ids = this.gwSelectedSubIdentifiers;
this.gwSingleTableComTemp = (this.gwSingleDatas || []).map((item) => {
const time = Object.keys(item)[0];
const vals = Object.values(item)[0] || [];
const obj = { time };
vals.forEach((v) => {
const k = Object.keys(v)[0];
obj[k] = Object.values(v)[0];
});
return obj;
});
this.gwSingleTotal = this.gwSingleTableComTemp.length;
this.gwSingleTableHeaderTemp = ids.map((id) => {
const m = this.subCompareIdentifierList.find((i) => i.identifier === id);
return { name: (m && m.modelName) || id, value: id };
});
},
drawGwSingleLine() {
if (!this.$refs.gwSingleLineChart || !this.gwSingleDatas.length) return;
const chart = this.$echarts.init(this.$refs.gwSingleLineChart);
chart.clear();
const ids = this.gwSelectedSubIdentifiers;
const times = this.gwSingleDatas.map((d) => Object.keys(d)[0]);
const series = ids.map((id) => {
const m = this.subCompareIdentifierList.find((i) => i.identifier === id);
return {
name: (m && m.modelName) || id,
type: 'line',
smooth: true,
data: this.gwSingleDatas.map((d) => {
const arr = Object.values(d)[0] || [];
const o = arr.find((x) => Object.keys(x)[0] === id);
return o ? Object.values(o)[0] : null;
}),
};
});
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: series.map((s) => s.name) },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times },
yAxis: { type: 'value', scale: true },
series,
});
},
getSubCompareTableList(e) {
if (e) {
this.subComparePageNum = e.page;
this.subComparePageSize = e.limit;
}
},
/** 多设备对比列名分隔符,与后端一致 */
subCompareColSep() {
return ':::';
},
/** 多设备对比表格/图例列展示名:后端返回 "设备标签:::identifier" 时解析为 "设备标签 - 变量名" */
getSubCompareColumnLabel(key) {
if (key === 'time') return '更新时间';
const sep = this.subCompareColSep();
if (key && key.includes(sep)) {
const idx = key.indexOf(sep);
const deviceLabel = key.substring(0, idx);
const identifier = key.substring(idx + sep.length);
const model = (this.subCompareIdentifierList || []).find((i) => i.identifier === identifier);
return `${deviceLabel} - ${(model && model.modelName) || identifier}`;
}
return key;
},
formatSubCompareTableDatas() {
this.subCompareTableComTemp = this.subCompareDatas || [];
this.subCompareTotal = this.subCompareTableComTemp.length;
const cols = [];
if (this.subCompareDatas.length) {
Object.keys(this.subCompareDatas[0]).forEach((k) => {
cols.push({ prop: k, label: this.getSubCompareColumnLabel(k) });
});
}
this.subCompareTableColumns = cols;
},
drawSubCompareLine() {
if (!this.$refs.subCompareLineChart || !this.subCompareDatas.length) return;
const chart = this.$echarts.init(this.$refs.subCompareLineChart);
chart.clear();
const times = this.subCompareDatas.map((d) => d.time);
const keys = Object.keys(this.subCompareDatas[0]).filter((k) => k !== 'time');
const series = keys.map((k) => ({
name: this.getSubCompareColumnLabel(k),
type: 'line',
smooth: true,
data: this.subCompareDatas.map((d) => d[k]),
}));
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: series.map((s) => s.name) },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: times },
yAxis: { type: 'value', scale: true },
series,
});
},
// 格式化设备列表数据 // 格式化设备列表数据
formatDevTableDatas() { formatDevTableDatas() {
this.devTableComTemp = this.devDatas.map((item) => { this.devTableComTemp = this.devDatas.map((item) => {
@ -826,5 +1480,8 @@ export default {
.scene-wrap { .scene-wrap {
margin-top: 5px; margin-top: 5px;
} }
.gateway-wrap {
margin-top: 5px;
}
} }
</style> </style>

View File

@ -156,10 +156,27 @@
<el-button style="color: #f56c6c" size="mini" title="右括号" @click="insertOperator(')')">)</el-button> <el-button style="color: #f56c6c" size="mini" title="右括号" @click="insertOperator(')')">)</el-button>
</div> </div>
</div> </div>
<!-- 物模型变量区域 --> <!-- 物模型变量区域:网关设备按从机展示,便于插值计算 -->
<div class="variable-section" v-loading="loadingModels"> <div class="variable-section" v-loading="loadingModels">
<span class="section-label">产品通用物模型变量:</span> <span class="section-label">{{ isGatewayDevice ? '从机物模型变量(点击插入,用于跨设备插值如 从机1-从机2):' : '产品通用物模型变量:' }}</span>
<div class="variable-tags"> <template v-if="isGatewayDevice && gatewayVariableGroups.length">
<div v-for="grp in gatewayVariableGroups" :key="grp.slaveId" class="variable-group">
<span class="group-label">从机{{ grp.slaveId }}:</span>
<el-tag
v-for="item in grp.items"
:key="item.placeholder"
size="small"
type="primary"
effect="plain"
class="var-tag"
@click="insertVariable(item.placeholder)"
:title="item.placeholder"
>
{{ item.modelName }}
</el-tag>
</div>
</template>
<div v-else class="variable-tags">
<el-tag <el-tag
v-for="model in productModels" v-for="model in productModels"
:key="model.identifier" :key="model.identifier"
@ -251,6 +268,21 @@
background-color: #ecf5ff; background-color: #ecf5ff;
} }
/* 网关按从机分组变量 */
.variable-group {
margin-bottom: 8px;
}
.variable-group .group-label {
font-size: 12px;
color: #606266;
font-weight: 600;
margin-right: 8px;
display: inline-block;
min-width: 52px;
}
.variable-group .var-tag {
margin: 2px 4px 2px 0;
}
/* 物模型变量区域 */ /* 物模型变量区域 */
.variable-section { .variable-section {
margin-bottom: 12px; margin-bottom: 12px;
@ -371,6 +403,8 @@
<script> <script>
import { listModel, getModel, addModel, updateModel, delModel } from '@/api/iot/model'; import { listModel, getModel, addModel, updateModel, delModel } from '@/api/iot/model';
import { listGateway } from '@/api/iot/gateway';
import { listThingsModel } from '@/api/iot/device';
export default { export default {
name: 'DeviceThingsModel', name: 'DeviceThingsModel',
@ -439,8 +473,14 @@ export default {
{ validator: this.validateFormula, trigger: 'none' }, { validator: this.validateFormula, trigger: 'none' },
], ],
}, },
gatewayVariableGroups: [], // 网关设备时按从机分组的变量,用于公式占位 { slaveId, items: [{ modelName, placeholder }] }
}; };
}, },
computed: {
isGatewayDevice() {
return this.device && this.device.deviceType === 2;
},
},
watch: { watch: {
device: { device: {
handler(val) { handler(val) {
@ -464,26 +504,27 @@ export default {
} }
// 去除空格后校验 // 去除空格后校验
const formula = value.replace(/\s+/g, ''); const formula = value.replace(/\s+/g, '');
// 1. 校验是否包含非法字符(只允许:数字、小数点、运算符、括号、大括号变量、sqrt) // 1. 校验是否包含非法字符(含大括号变量、跨设备 slaveId:N.xxx 中的冒号)
// 合法字符:0-9 . + - * / % ^ ( ) { } 字母(用于变量和sqrt) const validCharsPattern = /^[0-9a-zA-Z_+\-*/%^(){}.:\s]+$/;
const validCharsPattern = /^[0-9a-zA-Z_+\-*/%^(){}.\s]+$/;
if (!validCharsPattern.test(formula)) { if (!validCharsPattern.test(formula)) {
callback(new Error('公式包含非法字符')); callback(new Error('公式包含非法字符'));
return; return;
} }
// 2. 校验变量格式:{标识符},标识符以字母开头,只含字母、数字、下划线 // 2. 校验变量格式:{标识符} 或 {slaveId:数字.标识符}(跨设备)
const variablePattern = /\{([^}]*)\}/g; const variablePattern = /\{([^}]*)\}/g;
let match; let match;
const simpleVar = /^[a-zA-Z][a-zA-Z0-9_]*$/;
const crossVar = /^slaveId:\d+\.[a-zA-Z][a-zA-Z0-9_]*$/;
while ((match = variablePattern.exec(formula)) !== null) { while ((match = variablePattern.exec(formula)) !== null) {
const varName = match[1]; const varName = match[1];
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(varName)) { if (!simpleVar.test(varName) && !crossVar.test(varName)) {
callback(new Error(`变量格式错误: {${varName}},标识符需以字母开头,只含字母、数字、下划线`)); callback(new Error(`变量格式错误: {${varName}}。支持:{标识符} 或 {slaveId:从机号.标识符}(如 {slaveId:1.WaterH})`));
return; return;
} }
} }
// 3. 校验括号是否匹配 // 3. 校验括号是否匹配
let parenthesesCount = 0; let parenthesesCount = 0;
let braceCount = 0; let braceCount = 0;
@ -509,40 +550,40 @@ export default {
callback(new Error('变量大括号不匹配:左右大括号数量不一致')); callback(new Error('变量大括号不匹配:左右大括号数量不一致'));
return; return;
} }
// 4. 校验不能以运算符开头(除了负号和左括号) // 4. 校验不能以运算符开头(除了负号和左括号)
if (/^[*/%^)]/.test(formula)) { if (/^[*/%^)]/.test(formula)) {
callback(new Error('公式不能以 * / % ^ ) 开头')); callback(new Error('公式不能以 * / % ^ ) 开头'));
return; return;
} }
// 5. 校验不能以运算符结尾(除了右括号和右大括号) // 5. 校验不能以运算符结尾(除了右括号和右大括号)
if (/[+\-*/%^(]$/.test(formula)) { if (/[+\-*/%^(]$/.test(formula)) {
callback(new Error('公式不能以运算符结尾')); callback(new Error('公式不能以运算符结尾'));
return; return;
} }
// 6. 校验不能有连续的运算符(除了负号可以跟在其他运算符后面) // 6. 校验不能有连续的运算符(除了负号可以跟在其他运算符后面)
if (/[+\-*/%^]{2,}/.test(formula.replace(/([+*/%^(])-/g, '$1NEG'))) { if (/[+\-*/%^]{2,}/.test(formula.replace(/([+*/%^(])-/g, '$1NEG'))) {
callback(new Error('公式中不能有连续的运算符')); callback(new Error('公式中不能有连续的运算符'));
return; return;
} }
// 7. 校验sqrt后面必须跟左括号或数字或变量 // 7. 校验sqrt后面必须跟左括号或数字或变量
if (/sqrt(?![({0-9])/.test(formula)) { if (/sqrt(?![({0-9])/.test(formula)) {
callback(new Error('sqrt后面必须跟括号、数字或变量')); callback(new Error('sqrt后面必须跟括号、数字或变量'));
return; return;
} }
// 8. 校验公式必须包含至少一个变量 // 8. 校验公式必须包含至少一个变量
if (!/\{[a-zA-Z][a-zA-Z0-9_]*\}/.test(formula)) { if (!/\{([a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*|[a-zA-Z]+:\d+\.[a-zA-Z][a-zA-Z0-9_]*)}/.test(formula)) {
callback(new Error('计算公式必须包含至少一个物模型变量,如:{temperature}')); callback(new Error('计算公式必须包含至少一个物模型变量,如:{temperature} 或 {slaveId:1.temperature}'));
return; return;
} }
callback(); callback();
}, },
/** 查询设备计算物模型列表 */ /** 查询设备计算物模型列表 */
getList() { getList() {
if (!this.queryParams.deviceId) return; if (!this.queryParams.deviceId) return;
@ -556,10 +597,45 @@ export default {
this.loading = false; this.loading = false;
}); });
}, },
/** 加载产品物模型(用于公式变量选择) */ /** 加载公式变量:网关设备按从机加载(用于跨设备插值),否则加载产品物模型 */
loadProductModels() { loadProductModels() {
if (!this.device.productId) return; if (!this.device || !this.device.deviceId) return;
this.loadingModels = true; this.loadingModels = true;
this.gatewayVariableGroups = [];
if (this.isGatewayDevice) {
listGateway({ gwDeviceId: this.device.deviceId, pageNum: 1, pageSize: 999 })
.then((res) => {
const subs = res.rows || res.data || [];
const promises = subs.map((sub) =>
listThingsModel({ deviceId: sub.subDeviceId, pageNum: 1, pageSize: 200 }).then((r) => ({
slaveId: sub.slaveId,
subName: sub.subDeviceName || '从机' + sub.slaveId,
rows: (r.rows || []).filter((m) => m.isCalculated !== 1),
}))
);
return Promise.all(promises);
})
.then((groups) => {
this.gatewayVariableGroups = groups
.filter((g) => g.rows && g.rows.length)
.map((g) => ({
slaveId: g.slaveId,
items: g.rows.map((m) => ({
modelName: m.modelName || m.identifier,
placeholder: `{slaveId:${g.slaveId}.${m.identifier}}`,
})),
}));
this.productModels = [];
})
.finally(() => {
this.loadingModels = false;
});
return;
}
if (!this.device.productId) {
this.loadingModels = false;
return;
}
listModel({ listModel({
productId: this.device.productId, productId: this.device.productId,
deviceId: 0, deviceId: 0,
@ -567,7 +643,6 @@ export default {
pageSize: 200, pageSize: 200,
}) })
.then((response) => { .then((response) => {
// 过滤掉纯计算物模型(只保留设备上报的物模型)
this.productModels = (response.rows || []).filter((m) => m.isCalculated !== 1); this.productModels = (response.rows || []).filter((m) => m.isCalculated !== 1);
}) })
.finally(() => { .finally(() => {
@ -648,9 +723,13 @@ export default {
insertOperator(op) { insertOperator(op) {
this.insertAtCursor(op); this.insertAtCursor(op);
}, },
/** 插入变量(标识符使用大括号包裹) */ /** 插入变量:普通变量传 identifier 自动加大括号;网关跨设备变量传完整占位符如 {slaveId:1.WaterH} */
insertVariable(identifier) { insertVariable(identifierOrPlaceholder) {
this.insertAtCursor(`{${identifier}}`); const text =
identifierOrPlaceholder && (identifierOrPlaceholder.startsWith('{') || identifierOrPlaceholder.includes('slaveId:'))
? identifierOrPlaceholder
: `{${identifierOrPlaceholder}}`;
this.insertAtCursor(text);
}, },
/** 在光标位置插入内容(支持替换选中文本) */ /** 在光标位置插入内容(支持替换选中文本) */
insertAtCursor(text) { insertAtCursor(text) {
@ -665,15 +744,15 @@ export default {
this.form.formula = (this.form.formula || '') + text; this.form.formula = (this.form.formula || '') + text;
return; return;
} }
const formula = this.form.formula || ''; const formula = this.form.formula || '';
const start = textarea.selectionStart; const start = textarea.selectionStart;
const end = textarea.selectionEnd; const end = textarea.selectionEnd;
// 在光标位置插入或替换选中的文本 // 在光标位置插入或替换选中的文本
const newFormula = formula.substring(0, start) + text + formula.substring(end); const newFormula = formula.substring(0, start) + text + formula.substring(end);
this.form.formula = newFormula; this.form.formula = newFormula;
// 设置光标位置到插入内容之后 // 设置光标位置到插入内容之后
this.$nextTick(() => { this.$nextTick(() => {
const newCursorPos = start + text.length; const newCursorPos = start + text.length;

View File

@ -39,6 +39,11 @@
{{ $t('product.product-things-model.142341-1') }} {{ $t('product.product-things-model.142341-1') }}
</el-button> </el-button>
</el-col> </el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-download" size="mini" @click="handleImportFromSubProduct" v-if="productInfo.status == 1 && productInfo.isOwner != 0" v-hasPermi="['iot:model:add']">
从子产品导入
</el-button>
</el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="getList" v-hasPermi="['iot:model:list']">{{ $t('product.product-things-model.142341-2') }}</el-button> <el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="getList" v-hasPermi="['iot:model:list']">{{ $t('product.product-things-model.142341-2') }}</el-button>
</el-col> </el-col>
@ -361,6 +366,21 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- 从子产品一键导入物模型 -->
<el-dialog title="从子产品导入物模型" :visible.sync="openSubProductImport" width="700px" append-to-body>
<p style="margin-bottom: 10px; color: #909399;">将已绑定子产品的物模型按 identifier 去重后导入到当前网关产品,已存在的 identifier 会跳过。</p>
<el-table :data="subProductModelList" max-height="400" border size="small">
<el-table-column prop="modelName" label="物模型名称" min-width="120" />
<el-table-column prop="identifier" label="标识符" width="140" />
<el-table-column prop="datatype" label="数据类型" width="90" />
</el-table>
<div v-if="subProductModelList.length === 0" style="text-align: center; padding: 20px; color: #909399;">当前产品未绑定子产品或子产品下暂无物模型</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmSubProductImport" :loading="subProductImportLoading" :disabled="subProductModelList.length === 0">确认导入</el-button>
<el-button @click="openSubProductImport = false">取消</el-button>
</div>
</el-dialog>
<!-- {{ $t('product.product-things-model.142341-101') }}JSON --> <!-- {{ $t('product.product-things-model.142341-101') }}JSON -->
<el-dialog :title="title" :visible.sync="openThingsModel" width="600px" append-to-body> <el-dialog :title="title" :visible.sync="openThingsModel" width="600px" append-to-body>
<div style="border: 1px solid #ccc; margin-top: -15px; height: 600px; overflow: scroll"> <div style="border: 1px solid #ccc; margin-top: -15px; height: 600px; overflow: scroll">
@ -390,6 +410,7 @@ import JsonViewer from 'vue-json-viewer';
import 'vue-json-viewer/style.css'; import 'vue-json-viewer/style.css';
import thingsParameter from '../template/parameter'; import thingsParameter from '../template/parameter';
import { listModel, getModel, delModel, addModel, updateModel, importModel } from '@/api/iot/model'; import { listModel, getModel, delModel, addModel, updateModel, importModel } from '@/api/iot/model';
import { getSubProductThingsModel, importSubProductThingsToGateway } from '@/api/data/center';
import importBatch from './components/batchImportThingsModel'; import importBatch from './components/batchImportThingsModel';
export default { export default {
@ -454,6 +475,9 @@ export default {
open: false, open: false,
openSelect: false, openSelect: false,
openThingsModel: false, openThingsModel: false,
openSubProductImport: false,
subProductModelList: [],
subProductImportLoading: false,
// 查询参数 // 查询参数
queryParams: { queryParams: {
productId: 0, productId: 0,
@ -709,6 +733,36 @@ export default {
}); });
} }
}, },
/** 从子产品导入:打开弹窗并拉取可导入列表 */
handleImportFromSubProduct() {
if (!this.productInfo || !this.productInfo.productId) {
this.$modal.msgWarning('请先选择产品');
return;
}
this.openSubProductImport = true;
this.subProductModelList = [];
getSubProductThingsModel(this.productInfo.productId).then((res) => {
if (res.code === 200 && res.data) {
this.subProductModelList = res.data;
}
});
},
/** 从子产品导入:确认导入 */
confirmSubProductImport() {
if (!this.productInfo || !this.productInfo.productId) return;
this.subProductImportLoading = true;
importSubProductThingsToGateway(this.productInfo.productId)
.then((res) => {
this.subProductImportLoading = false;
this.openSubProductImport = false;
this.$modal.msgSuccess(res.msg || '导入成功');
this.getList();
this.$emit('updateModel');
})
.catch(() => {
this.subProductImportLoading = false;
});
},
containsUnderscore(value) { containsUnderscore(value) {
// 使用正则表达式检查值中是否包含下划线 // 使用正则表达式检查值中是否包含下划线
return /_/.test(value); return /_/.test(value);