20260413:支持数据采集页面查看跨设备计算物模型物理量展示

This commit is contained in:
George 2026-04-13 08:27:29 +08:00
parent 8c9fe1e355
commit acea720181
8 changed files with 133 additions and 76 deletions

View File

@ -17,6 +17,12 @@ import java.util.regex.Pattern;
*/
public class CalculateUtils {
/**
* 公式中跨设备引用正则:{slaveId:N.identifier}
*/
private static final Pattern CROSS_DEVICE_REF = Pattern.compile("\\{slaveId:(\\d+)\\.([^}]+)}");
/**
* /*
* 支持加减乘除、幂运算、取余及括号的应用
@ -219,6 +225,25 @@ public class CalculateUtils {
return isNum.matches();
}
/**
* 解析公式中所有跨设备引用占位符
*
* @param formula 计算公式,形如 {slaveId:1.Ang_z} + {slaveId:2.Ang_z}
* @return key=slaveId:N.identifier, value=identifier
*/
public static Map<String, String> parseCrossDeviceRefs(String formula) {
Map<String, String> refs = new LinkedHashMap<>();
if (formula == null || formula.isEmpty()) {
return refs;
}
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;
}
public static String calculateReplace(String str, Map<String, String> map) {
for (Map.Entry<String, String> entry : map.entrySet()) {
// 对于包含特殊字符的占位符(如 {identifier}),需要转义正则表达式特殊字符

View File

@ -8,7 +8,6 @@ import com.fastbee.common.core.notify.config.WeChatConfigParams;
import com.fastbee.common.core.notify.msg.WechatMsgParams;
import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.enums.DataLevelEnum;
import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.wechat.WechatUtils;
import com.fastbee.common.wechat.WeChatAppResult;
@ -58,8 +57,6 @@ public class ThingsModelPushHandler {
private static final RestTemplate REST_TEMPLATE = new RestTemplate();
private static final Pattern VAR_PATTERN = Pattern.compile("\\{\\{([^}]+)}}");
/** 公式中跨设备引用:{slaveId:N.identifier} */
private static final Pattern CROSS_DEVICE_REF = Pattern.compile("\\{slaveId:(\\d+)\\.([^}]+)}");
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@ -258,6 +255,7 @@ public class ThingsModelPushHandler {
/**
* 计算网关跨设备计算物模型的当前值(只基于各从机当前缓存值)
* 委托 IThingsModelService.computeCrossDeviceCurrentValue 统一计算
*/
private ComputedValue tryComputeGatewayCalculatedValue(ThingsModelPushConfig config,
String gwSerialNumber,
@ -286,65 +284,16 @@ public class ThingsModelPushHandler {
model.getIsCalculated() != 1 || StringUtils.isEmpty(model.getFormula())) {
return null;
}
Map<String, String> refs = parseCrossDeviceRefs(model.getFormula());
if (refs.isEmpty()) {
List<SubDeviceListVO> subDeviceList = subDeviceBySlaveId != null
? new ArrayList<>(subDeviceBySlaveId.values())
: Collections.emptyList();
ValueItem result = thingsModelService.computeCrossDeviceCurrentValue(model.getFormula(), subDeviceList);
if (result == null) {
log.warn("[Push] configId={} sn={} identifier={} 跨设备公式计算失败",
config.getId(), gwSerialNumber, identifier);
return null;
}
Map<String, String> replaceMap = new HashMap<>();
Date maxTs = null;
for (Map.Entry<String, String> ref : refs.entrySet()) {
String refKey = ref.getKey(); // slaveId:N.identifier
int slaveId;
try {
slaveId = Integer.parseInt(refKey.split("\\.")[0].replace("slaveId:", ""));
} catch (Exception e) {
continue;
}
SubDeviceListVO sub = subDeviceBySlaveId != null ? subDeviceBySlaveId.get(slaveId) : null;
String subSn = sub != null ? sub.getSubDeviceNo() : null;
Long subProductId = sub != null ? sub.getSubProductId() : null;
String subIdentifier = ref.getValue();
String val = "0";
if (subProductId != null && StringUtils.isNotEmpty(subSn) && StringUtils.isNotEmpty(subIdentifier)) {
try {
ValueItem v = thingsModelService.getCacheIdentifierValue(subProductId, subSn, subIdentifier);
if (v != null && StringUtils.isNotEmpty(v.getValue())) {
val = v.getValue();
}
if (v != null && v.getTs() != null) {
// if (maxTs == null || v.getTs().after(maxTs)) {
// maxTs = v.getTs();
// }
maxTs = v.getTs();
}
} catch (Exception ignored) {
}
}
replaceMap.put("{" + refKey + "}", val);
}
try {
Object computed = CalculateUtils.execute(model.getFormula(), replaceMap);
String computedStr = computed != null ? computed.toString() : null;
return new ComputedValue(computedStr, maxTs);
} catch (Exception e) {
log.warn("[Push] configId={} sn={} identifier={} 公式计算失败 formula={} err={}",
config.getId(), gwSerialNumber, identifier, model.getFormula(), e.getMessage());
return null;
}
}
private Map<String, String> parseCrossDeviceRefs(String formula) {
Map<String, String> refs = new LinkedHashMap<>();
if (StringUtils.isEmpty(formula)) {
return refs;
}
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;
return new ComputedValue(result.getValue(), result.getTs());
}
/**

View File

@ -163,7 +163,7 @@ public class ToolController extends BaseController {
@ApiOperation("mqtt认证")
@PostMapping("/mqtt/authv5")
public ResponseEntity mqttAuthv5(@RequestBody JSONObject json) throws Exception {
log.info("-----------auth-json:" + json + "---------------");
log.info("-----------auth-json:{}---------------", json);
String clientid = json.getString("clientid");
String username = json.getString("username");
String password = json.getString("password");
@ -173,7 +173,7 @@ public class ToolController extends BaseController {
if (clientid.startsWith("server")) {
// 服务端认证:配置的账号密码认证
if (mqttConfig.getUsername().equals(username) && mqttConfig.getPassword().equals(password)) {
log.info("-----------服务端mqtt认证成功,clientId:" + clientid + "---------------");
log.info("-----------服务端mqtt认证成功,clientId:{}---------------", clientid);
ret.put("result", "allow");
return ResponseEntity.ok().body(ret);
} else {
@ -188,7 +188,7 @@ public class ToolController extends BaseController {
}
try {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
log.info("-----------移动端/Web端mqtt认证成功,clientId:" + clientid + "---------------");
log.info("-----------移动端/Web端mqtt认证成功,clientId:{}---------------", clientid);
ret.put("result", "allow");
return ResponseEntity.ok().body(ret);
} catch (Exception ex) {
@ -255,7 +255,7 @@ public class ToolController extends BaseController {
} catch (Exception ex) {
ex.printStackTrace();
log.error("发生错误:" + ex.getMessage());
log.error("发生错误:{}", ex.getMessage(), ex);
}
}
@ -270,7 +270,7 @@ public class ToolController extends BaseController {
if (peername.indexOf(":") != -1) {
ipAddress = peername.substring(0, peername.indexOf(":"));
}
log.info("webhook:" + event + ",clientid:" + clientid + ",peername:" + peername);
log.info("webhook:{},clientid:{},peername:{}", event, clientid, peername);
// 过滤服务端、web端和手机端
if (clientid.startsWith("server") || clientid.startsWith("web") || clientid.startsWith("phone")) {
return;

View File

@ -111,6 +111,11 @@ public class ThingsModelDTO extends BaseEntity {
private Integer modelOrder;
/**
* 物模型来源类型(0-设备上报,1-纯计算,2-其他预留)
*/
private Integer isCalculated;
private String value = "";
private String ts;

View File

@ -7,6 +7,7 @@ import com.fastbee.iot.model.ThingsModelPerm;
import com.fastbee.iot.model.ThingsModelSimVO;
import com.fastbee.iot.model.ThingsModels.*;
import com.fastbee.iot.model.dto.ThingsModelDTO;
import com.fastbee.iot.model.gateWay.SubDeviceListVO;
import com.fastbee.iot.model.modbus.ModbusAndThingsVO;
import java.util.List;
@ -202,4 +203,14 @@ public interface IThingsModelService {
* @return
*/
List<ThingsModel> selectThingsModelBySerialNumber(Long deviceId);
/**
* 计算跨设备计算物模型当前值
* 从子设备 Redis 缓存获取各引用变量的值,按公式实时计算
*
* @param formula 物模型公式,含 {slaveId:N.identifier} 占位符
* @param subDevices 网关下的子设备列表
* @return 计算结果(value + ts),公式无跨设备引用或计算失败时返回 null
*/
ValueItem computeCrossDeviceCurrentValue(String formula, List<SubDeviceListVO> subDevices);
}

View File

@ -21,8 +21,6 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@ -58,8 +56,6 @@ public class DataCenterServiceImpl implements DataCenterService {
@Resource
private com.fastbee.iot.cache.ITSLCache itslCache;
/** 公式中跨设备引用:{slaveId:N.identifier} */
private static final Pattern CROSS_DEVICE_REF = Pattern.compile("\\{slaveId:(\\d+)\\.([^}]+)\\}");
@Override
public List<JSONObject> deviceHistory(DeviceHistoryParam deviceHistoryParam) {
@ -515,13 +511,7 @@ public class DataCenterServiceImpl implements DataCenterService {
}
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;
return CalculateUtils.parseCrossDeviceRefs(formula);
}
private List<Date> buildMinuteSlots(String beginTime, String endTime) {

View File

@ -2030,11 +2030,38 @@ public class DeviceServiceImpl implements IDeviceService {
}
list.addAll(subList);
}
// 对跨设备计算物模型(isCalculated=1,公式含 slaveId 引用)实时计算当前值
computeCrossDeviceValuesForList(list, subDeviceListVOList);
// 指令控制
jurisdictionThingsModel(list, deviceId);
return list;
}
/**
* 对列表中所有跨设备计算物模型实时计算当前值(从子设备缓存取值,按公式计算)
*/
private void computeCrossDeviceValuesForList(List<ThingsModelDTO> list, List<SubDeviceListVO> subDevices) {
if (org.springframework.util.CollectionUtils.isEmpty(subDevices)) {
return;
}
for (ThingsModelDTO dto : list) {
if (!Integer.valueOf(1).equals(dto.getIsCalculated())) {
continue;
}
String formula = dto.getFormula();
if (StringUtils.isEmpty(formula) || !formula.contains("slaveId:")) {
continue;
}
ValueItem result = thingsModelService.computeCrossDeviceCurrentValue(formula, subDevices);
if (result != null && StringUtils.isNotEmpty(result.getValue())) {
dto.setValue(result.getValue());
if (result.getTs() != null) {
dto.setTs(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, result.getTs()));
}
}
}
}
private void jurisdictionThingsModel(List<ThingsModelDTO> list, Long deviceId) {
Long userId = SecurityUtils.getUserId();
// 查询是否有指令控制

View File

@ -9,9 +9,11 @@ import com.fastbee.common.core.redis.RedisCache;
import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.enums.scenemodel.SceneModelVariableTypeEnum;
import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.model.gateWay.SubDeviceListVO;
import com.fastbee.iot.cache.ITSLCache;
import com.fastbee.iot.domain.*;
import com.fastbee.iot.mapper.*;
@ -646,6 +648,8 @@ public class ThingsModelServiceImpl implements IThingsModelService {
otherThingsModel.setDatatype(datatype);
otherThingsModel.setModelOrder(thingsModel.getModelOrder());
otherThingsModel.setCreateTime(thingsModel.getCreateTime());
otherThingsModel.setFormula(thingsModel.getFormula());
otherThingsModel.setIsCalculated(thingsModel.getIsCalculated());
resultList.add(otherThingsModel);
}
return resultList;
@ -766,6 +770,50 @@ public class ThingsModelServiceImpl implements IThingsModelService {
return thingsModelMapper.selectThingsModelBySerialNumber(deviceId, SecurityUtils.getLanguage());
}
@Override
public ValueItem computeCrossDeviceCurrentValue(String formula, List<SubDeviceListVO> subDevices) {
Map<String, String> refs = CalculateUtils.parseCrossDeviceRefs(formula);
if (refs.isEmpty()) {
return null;
}
Map<String, String> replaceMap = new HashMap<>();
Date maxTs = null;
for (Map.Entry<String, String> ref : refs.entrySet()) {
String refKey = ref.getKey(); // slaveId:N.identifier
int slaveId;
try {
slaveId = Integer.parseInt(refKey.split("\\.")[0].replace("slaveId:", ""));
} catch (Exception e) {
replaceMap.put("{" + refKey + "}", "0");
continue;
}
SubDeviceListVO sub = subDevices.stream()
.filter(s -> s.getSlaveId() != null && s.getSlaveId() == slaveId)
.findFirst().orElse(null);
if (sub == null || sub.getSubProductId() == null || StringUtils.isEmpty(sub.getSubDeviceNo())) {
replaceMap.put("{" + refKey + "}", "0");
continue;
}
String subIdentifier = ref.getValue();
ValueItem v = this.getCacheIdentifierValue(sub.getSubProductId(), sub.getSubDeviceNo(), subIdentifier);
String val = (v != null && StringUtils.isNotEmpty(v.getValue())) ? v.getValue() : "0";
replaceMap.put("{" + refKey + "}", val);
if (v != null && v.getTs() != null) {
maxTs = v.getTs();
}
}
try {
Object computed = CalculateUtils.execute(formula, replaceMap);
ValueItem result = new ValueItem();
result.setValue(computed != null ? computed.toString() : "");
result.setTs(maxTs);
return result;
} catch (Exception e) {
log.warn("[ThingsModel] 跨设备公式计算失败 formula={} err={}", formula, e.getMessage());
return null;
}
}
private ThingsModel propertyDtoChangeThingsModel(ThingsModelValueItem valueItem) {
ThingsModel thingsModel = new ThingsModel();
thingsModel.setIdentifier(valueItem.getId());
@ -779,6 +827,8 @@ public class ThingsModelServiceImpl implements IThingsModelService {
thingsModel.setType(valueItem.getType());
thingsModel.setModelId(valueItem.getModelId());
thingsModel.setCreateTime(valueItem.getCreateTime());
thingsModel.setFormula(valueItem.getFormula());
thingsModel.setIsCalculated(valueItem.getIsCalculated());
return thingsModel;
}