20260102:
1、合并纯计算物模型与设备上报物模型数据存储(方法在解析数据之后将纯计算物模型填数据0,用到ConcurrentHashMap key:productId value:纯计算物模型标识符集合)
This commit is contained in:
parent
44df912fc2
commit
d4403f5d42
@ -8,6 +8,12 @@ fastbee:
|
||||
profile: /uploadPath
|
||||
addressEnabled: true # 获取ip地址开关
|
||||
captchaType: math # 验证码类型 math 数组计算 char 字符验证
|
||||
# 纯计算物模型缓存配置
|
||||
# cache:
|
||||
# calculated-things-model:
|
||||
# use-redis: false # 是否使用 Redis 缓存(false=本地缓存,true=Redis缓存)
|
||||
# local-cache-threshold: 1000 # 本地缓存大小阈值(超过此值建议切换到Redis)
|
||||
# redis-ttl: 86400 # Redis 缓存过期时间(秒),默认 24 小时
|
||||
|
||||
# 开发环境配置
|
||||
server:
|
||||
|
||||
@ -182,4 +182,20 @@ public class RedisKeyBuilder {
|
||||
return FastBeeConstant.REDIS.POWER_ON_TRIGGER_JOB_IDS + serialNumber.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯计算物模型缓存key(产品级别)
|
||||
* 用于缓存产品下的纯计算物模型标识符集合
|
||||
*/
|
||||
public static String buildCalculatedThingsModelKey(Long productId) {
|
||||
return "CALCULATED_TM:PRODUCT:" + productId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯计算物模型缓存key(设备级别,未来扩展)
|
||||
* 用于缓存设备下的纯计算物模型标识符集合(当设备有自定义物模型选择时使用)
|
||||
*/
|
||||
public static String buildCalculatedThingsModelDeviceKey(Long deviceId) {
|
||||
return "CALCULATED_TM:DEVICE:" + deviceId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
|
||||
|
||||
switch (bo.getServerType()) {
|
||||
case MQTT:
|
||||
log.debug("=>MQ*收到设备主题[{}],消息:[{}]", bo.getTopicName(), bo.getData());
|
||||
log.debug("{}=>MQ*收到设备主题[{}],消息:[{}]", serialNumber, bo.getTopicName(), bo.getData());
|
||||
//构建消息
|
||||
DeviceStatusVO deviceStatusVO = buildReport(bo);
|
||||
/*获取协议处理器*/
|
||||
@ -131,7 +131,7 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
|
||||
processNoSub(reportMessage, bo.getTopicName());
|
||||
break;
|
||||
case TCP:
|
||||
log.debug("*MQ收到TCP推送消息[{}]", JSON.toJSON(bo.getThingsModelSimpleItem()));
|
||||
log.debug("{}*MQ收到TCP推送消息[{}]", serialNumber, JSON.toJSON(bo.getThingsModelSimpleItem()));
|
||||
DeviceStatusVO deviceStatusVO1 = deviceService.selectDeviceStatusAndTransportStatus(serialNumber);
|
||||
Optional.ofNullable(deviceStatusVO1).orElseThrow(() -> new ServiceException("设备不存在"));
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.common.utils.gateway.CRC16Utils;
|
||||
import com.fastbee.common.utils.modbus.BitUtils;
|
||||
import com.fastbee.helper.ModbusProtocolHelper;
|
||||
import com.fastbee.iot.cache.ICalculatedThingsModelCache;
|
||||
import com.fastbee.iot.cache.IModbusConfigCache;
|
||||
import com.fastbee.iot.domain.Device;
|
||||
import com.fastbee.iot.domain.ModbusConfig;
|
||||
@ -39,6 +40,7 @@ import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -67,6 +69,9 @@ public class ModbusProtocol implements IProtocol {
|
||||
@Resource
|
||||
private ModbusConfigMapper modbusConfigMapper;
|
||||
|
||||
@Resource
|
||||
private ICalculatedThingsModelCache calculatedThingsModelCache;
|
||||
|
||||
@Override
|
||||
public DeviceReport decode(DeviceData deviceData, String clientId) {
|
||||
try {
|
||||
@ -311,6 +316,52 @@ public class ModbusProtocol implements IProtocol {
|
||||
|
||||
simpleItemList.add(simpleItem);
|
||||
}
|
||||
|
||||
// 处理纯计算物模型:查询产品下所有纯计算物模型,构造 ThingsModelSimpleItem,值为 "0"
|
||||
handleCalculatedThingsModels(productId, simpleItemList, isReadOnly);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理纯计算物模型
|
||||
* 从缓存获取纯计算物模型列表,构造 ThingsModelSimpleItem,值为 "0"
|
||||
* 后续在计算环节会统一处理所有物模型(包括纯计算物模型)
|
||||
*
|
||||
* @param productId 产品ID
|
||||
* @param simpleItemList 物模型值列表
|
||||
* @param isReadOnly 是否只读
|
||||
*/
|
||||
private void handleCalculatedThingsModels(Long productId, List<ThingsModelSimpleItem> simpleItemList, int isReadOnly) {
|
||||
try {
|
||||
// 从缓存获取纯计算物模型标识符集合(支持产品级别和设备级别)
|
||||
Set<String> calculatedIdentifiers = calculatedThingsModelCache.getCalculatedIdentifiers(productId, null);
|
||||
|
||||
if (calculatedIdentifiers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取已上报的物模型标识符集合(避免重复)
|
||||
Set<String> reportedIdentifiers = simpleItemList.stream()
|
||||
.map(ThingsModelSimpleItem::getId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 为纯计算物模型构造 ThingsModelSimpleItem
|
||||
for (String identifier : calculatedIdentifiers) {
|
||||
// 跳过已上报的物模型
|
||||
if (reportedIdentifiers.contains(identifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 构造 ThingsModelSimpleItem,值为 "0"
|
||||
ThingsModelSimpleItem simpleItem = new ThingsModelSimpleItem();
|
||||
simpleItem.setId(identifier);
|
||||
simpleItem.setValue("0"); // 初始值设为0,后续在计算环节会重新计算
|
||||
simpleItem.setTs(DateUtils.getNowDate());
|
||||
simpleItemList.add(simpleItem);
|
||||
log.debug("=>添加纯计算物模型到上报列表,产品ID:[{}],标识符:[{}]", productId, identifier);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("=>处理纯计算物模型时发生错误,产品ID:[{}],错误:{}", productId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
package com.fastbee.iot.cache;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 纯计算物模型缓存接口
|
||||
* 支持产品级别和设备级别的缓存
|
||||
* 支持本地缓存和 Redis 缓存的切换
|
||||
*
|
||||
* @author Sumuxi
|
||||
* @date 2026-01-02
|
||||
*/
|
||||
public interface ICalculatedThingsModelCache {
|
||||
|
||||
/**
|
||||
* 获取产品下的纯计算物模型标识符集合
|
||||
* @param productId 产品ID
|
||||
* @return 纯计算物模型标识符集合
|
||||
*/
|
||||
Set<String> getCalculatedIdentifiersByProduct(Long productId);
|
||||
|
||||
/**
|
||||
* 获取设备下的纯计算物模型标识符集合
|
||||
* 如果设备有自定义物模型,返回设备级别的;否则返回产品级别的
|
||||
* @param productId 产品ID
|
||||
* @param deviceId 设备ID(可选,未来扩展)
|
||||
* @return 纯计算物模型标识符集合
|
||||
*/
|
||||
Set<String> getCalculatedIdentifiers(Long productId, Long deviceId);
|
||||
|
||||
/**
|
||||
* 设置产品下的纯计算物模型标识符集合
|
||||
* @param productId 产品ID
|
||||
* @param identifiers 纯计算物模型标识符集合
|
||||
*/
|
||||
void setCalculatedIdentifiersByProduct(Long productId, Set<String> identifiers);
|
||||
|
||||
/**
|
||||
* 刷新产品下的纯计算物模型缓存
|
||||
* @param productId 产品ID
|
||||
*/
|
||||
void refreshCalculatedCache(Long productId);
|
||||
|
||||
/**
|
||||
* 清除产品下的纯计算物模型缓存
|
||||
* @param productId 产品ID
|
||||
*/
|
||||
void clearCalculatedCache(Long productId);
|
||||
|
||||
/**
|
||||
* 判断是否使用 Redis 缓存
|
||||
* @return true-使用Redis,false-使用本地缓存
|
||||
*/
|
||||
boolean isRedisCacheEnabled();
|
||||
}
|
||||
|
||||
@ -0,0 +1,149 @@
|
||||
package com.fastbee.iot.cache.impl;
|
||||
|
||||
import com.fastbee.iot.cache.ICalculatedThingsModelCache;
|
||||
import com.fastbee.iot.domain.ThingsModel;
|
||||
import com.fastbee.iot.service.IThingsModelService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 纯计算物模型缓存实现(本地缓存)
|
||||
* 使用 ConcurrentHashMap 维护本地缓存
|
||||
* 适用于计算型物模型数量较少、单机部署的场景
|
||||
*
|
||||
* @author sumuxi
|
||||
* @date 2026/01/02
|
||||
*/
|
||||
@Service("calculatedThingsModelCache")
|
||||
@ConditionalOnProperty(name = "fastbee.cache.calculated-things-model.use-redis", havingValue = "false", matchIfMissing = true)
|
||||
@Slf4j
|
||||
public class CalculatedThingsModelCacheImpl implements ICalculatedThingsModelCache {
|
||||
|
||||
/**
|
||||
* 本地缓存:productId -> Set<String>(纯计算物模型标识符集合)
|
||||
*/
|
||||
private final ConcurrentHashMap<Long, Set<String>> calculatedCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 设备级别缓存:deviceId -> Set<String>(未来扩展)
|
||||
* 当设备有自定义物模型选择时使用
|
||||
*/
|
||||
private final ConcurrentHashMap<Long, Set<String>> deviceCalculatedCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
private IThingsModelService thingsModelService;
|
||||
|
||||
/**
|
||||
* 是否启用 Redis 缓存(通过配置控制)
|
||||
* 默认 false,使用本地缓存
|
||||
* 当计算型物模型数量较多或需要集群部署时,设置为 true
|
||||
*/
|
||||
@Value("${fastbee.cache.calculated-things-model.use-redis:false}")
|
||||
private boolean useRedisCache;
|
||||
|
||||
/**
|
||||
* 本地缓存大小阈值
|
||||
* 超过此阈值建议切换到 Redis 缓存
|
||||
*/
|
||||
@Value("${fastbee.cache.calculated-things-model.local-cache-threshold:1000}")
|
||||
private int localCacheThreshold;
|
||||
|
||||
@Override
|
||||
public Set<String> getCalculatedIdentifiersByProduct(Long productId) {
|
||||
if (productId == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
// 从本地缓存获取
|
||||
Set<String> identifiers = calculatedCache.get(productId);
|
||||
if (identifiers != null) {
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
// 缓存未命中,从数据库加载
|
||||
refreshCalculatedCache(productId);
|
||||
return calculatedCache.getOrDefault(productId, Collections.emptySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getCalculatedIdentifiers(Long productId, Long deviceId) {
|
||||
// 如果设备有自定义物模型选择,优先返回设备级别的
|
||||
if (deviceId != null) {
|
||||
Set<String> deviceIdentifiers = deviceCalculatedCache.get(deviceId);
|
||||
if (deviceIdentifiers != null) {
|
||||
return deviceIdentifiers;
|
||||
}
|
||||
}
|
||||
|
||||
// 否则返回产品级别的
|
||||
return getCalculatedIdentifiersByProduct(productId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCalculatedIdentifiersByProduct(Long productId, Set<String> identifiers) {
|
||||
if (productId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查缓存大小,超过阈值时记录警告
|
||||
if (calculatedCache.size() > localCacheThreshold) {
|
||||
log.warn("=>纯计算物模型本地缓存大小超过阈值[{}],建议切换到Redis缓存", localCacheThreshold);
|
||||
}
|
||||
|
||||
calculatedCache.put(productId, identifiers != null ? identifiers : Collections.emptySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshCalculatedCache(Long productId) {
|
||||
if (productId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询产品下所有物模型
|
||||
ThingsModel queryModel = new ThingsModel();
|
||||
queryModel.setProductId(productId);
|
||||
List<ThingsModel> allThingsModels = thingsModelService.selectThingsModelList(queryModel);
|
||||
|
||||
if (CollectionUtils.isEmpty(allThingsModels)) {
|
||||
calculatedCache.put(productId, Collections.emptySet());
|
||||
return;
|
||||
}
|
||||
|
||||
// 筛选出纯计算物模型(isCalculated == 1)
|
||||
Set<String> calculatedIdentifiers = allThingsModels.stream()
|
||||
.filter(model -> model.getIsCalculated() != null && model.getIsCalculated() == 1)
|
||||
.map(ThingsModel::getIdentifier)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
calculatedCache.put(productId, calculatedIdentifiers);
|
||||
log.debug("=>刷新纯计算物模型本地缓存成功,产品ID:[{}],数量:[{}]", productId, calculatedIdentifiers.size());
|
||||
} catch (Exception e) {
|
||||
log.error("=>刷新纯计算物模型本地缓存失败,产品ID:[{}],错误:{}", productId, e.getMessage(), e);
|
||||
calculatedCache.put(productId, Collections.emptySet());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCalculatedCache(Long productId) {
|
||||
if (productId != null) {
|
||||
calculatedCache.remove(productId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRedisCacheEnabled() {
|
||||
return useRedisCache;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,163 @@
|
||||
/*
|
||||
package com.fastbee.iot.cache.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.fastbee.common.core.redis.RedisCache;
|
||||
import com.fastbee.common.core.redis.RedisKeyBuilder;
|
||||
import com.fastbee.iot.cache.ICalculatedThingsModelCache;
|
||||
import com.fastbee.iot.domain.ThingsModel;
|
||||
import com.fastbee.iot.service.IThingsModelService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
*/
|
||||
/**
|
||||
* 纯计算物模型缓存实现(Redis缓存)
|
||||
* 使用 Redis 维护缓存,支持集群部署
|
||||
* 适用于计算型物模型数量较多、集群部署的场景
|
||||
*
|
||||
* @author Sumuxi
|
||||
* @date 2026-01-02
|
||||
*//*
|
||||
|
||||
@Service("calculatedThingsModelRedisCache")
|
||||
@ConditionalOnProperty(name = "fastbee.cache.calculated-things-model.use-redis", havingValue = "true")
|
||||
@Slf4j
|
||||
public class CalculatedThingsModelRedisCacheImpl implements ICalculatedThingsModelCache {
|
||||
|
||||
*/
|
||||
/**
|
||||
* 缓存过期时间(秒),默认 24 小时
|
||||
*//*
|
||||
|
||||
@Value("${fastbee.cache.calculated-things-model.redis-ttl:86400}")
|
||||
private long redisTtl;
|
||||
|
||||
@Resource
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Resource
|
||||
private IThingsModelService thingsModelService;
|
||||
|
||||
@Override
|
||||
public Set<String> getCalculatedIdentifiersByProduct(Long productId) {
|
||||
if (productId == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
String cacheKey = RedisKeyBuilder.buildCalculatedThingsModelKey(productId);
|
||||
String cacheValue = redisCache.getCacheObject(cacheKey);
|
||||
|
||||
if (cacheValue != null) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<String> result = JSON.parseObject(cacheValue, Set.class);
|
||||
return result != null ? result : Collections.emptySet();
|
||||
} catch (Exception e) {
|
||||
log.warn("=>解析纯计算物模型Redis缓存失败,产品ID:[{}],错误:{}", productId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存未命中,从数据库加载
|
||||
refreshCalculatedCache(productId);
|
||||
cacheValue = redisCache.getCacheObject(cacheKey);
|
||||
if (cacheValue != null) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<String> result = JSON.parseObject(cacheValue, Set.class);
|
||||
return result != null ? result : Collections.emptySet();
|
||||
} catch (Exception e) {
|
||||
log.warn("=>解析纯计算物模型Redis缓存失败,产品ID:[{}],错误:{}", productId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getCalculatedIdentifiers(Long productId, Long deviceId) {
|
||||
// 如果设备有自定义物模型选择,优先返回设备级别的
|
||||
if (deviceId != null) {
|
||||
String deviceCacheKey = RedisKeyBuilder.buildCalculatedThingsModelDeviceKey(deviceId);
|
||||
String deviceCacheValue = redisCache.getCacheObject(deviceCacheKey);
|
||||
if (deviceCacheValue != null) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<String> result = JSON.parseObject(deviceCacheValue, Set.class);
|
||||
return result != null ? result : Collections.emptySet();
|
||||
} catch (Exception e) {
|
||||
log.warn("=>解析设备纯计算物模型Redis缓存失败,设备ID:[{}],错误:{}", deviceId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 否则返回产品级别的
|
||||
return getCalculatedIdentifiersByProduct(productId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCalculatedIdentifiersByProduct(Long productId, Set<String> identifiers) {
|
||||
if (productId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = RedisKeyBuilder.buildCalculatedThingsModelKey(productId);
|
||||
String cacheValue = JSON.toJSONString(identifiers != null ? identifiers : Collections.emptySet());
|
||||
redisCache.setCacheObject(cacheKey, cacheValue, (int) redisTtl, java.util.concurrent.TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshCalculatedCache(Long productId) {
|
||||
if (productId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 查询产品下所有物模型
|
||||
ThingsModel queryModel = new ThingsModel();
|
||||
queryModel.setProductId(productId);
|
||||
List<ThingsModel> allThingsModels = thingsModelService.selectThingsModelList(queryModel);
|
||||
|
||||
if (CollectionUtils.isEmpty(allThingsModels)) {
|
||||
setCalculatedIdentifiersByProduct(productId, Collections.emptySet());
|
||||
return;
|
||||
}
|
||||
|
||||
// 筛选出纯计算物模型(isCalculated == 1)
|
||||
Set<String> calculatedIdentifiers = allThingsModels.stream()
|
||||
.filter(model -> model.getIsCalculated() != null && model.getIsCalculated() == 1)
|
||||
.map(ThingsModel::getIdentifier)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
setCalculatedIdentifiersByProduct(productId, calculatedIdentifiers);
|
||||
log.debug("=>刷新纯计算物模型Redis缓存成功,产品ID:[{}],数量:[{}]", productId, calculatedIdentifiers.size());
|
||||
} catch (Exception e) {
|
||||
log.error("=>刷新纯计算物模型Redis缓存失败,产品ID:[{}],错误:{}", productId, e.getMessage(), e);
|
||||
setCalculatedIdentifiersByProduct(productId, Collections.emptySet());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCalculatedCache(Long productId) {
|
||||
if (productId != null) {
|
||||
String cacheKey = RedisKeyBuilder.buildCalculatedThingsModelKey(productId);
|
||||
redisCache.deleteObject(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRedisCacheEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@ -317,6 +317,8 @@ public class TSLCacheImpl implements ITSLCache {
|
||||
.setName_en_US(thingsModel.getModelName_en_US())
|
||||
.setOrder(thingsModel.getModelOrder())
|
||||
.setModelId(thingsModel.getModelId())
|
||||
.setFormula(thingsModel.getFormula())
|
||||
.setIsCalculated(thingsModel.getIsCalculated())
|
||||
.setConfig(thingsModel.getModbusConfig());
|
||||
return JSONObject.toJSONString(dto);
|
||||
}));
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.fastbee.iot.domain;
|
||||
|
||||
import com.fastbee.common.annotation.Excel;
|
||||
import com.fastbee.common.core.domain.BaseEntity;
|
||||
import com.fastbee.common.core.protocol.modbus.ModbusCode;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
|
||||
@ -96,6 +96,13 @@ public class ThingsModel extends BaseEntity {
|
||||
@Excel(name = "计算公式", prompt = "选填,例如:%s*10,%s是占位符")
|
||||
private String formula;
|
||||
|
||||
/**
|
||||
* 物模型来源类型(0-设备上报,1-纯计算,2-其他预留)
|
||||
* 纯计算物模型:计算公式包含{},依赖其他物模型计算值
|
||||
*/
|
||||
@ApiModelProperty(value = "物模型来源类型", notes = "(0-设备上报,1-纯计算,2-其他预留)")
|
||||
private Integer isCalculated;
|
||||
|
||||
/**
|
||||
* 数据定义
|
||||
*/
|
||||
|
||||
@ -79,6 +79,12 @@ public class ThingsModelValueItem {
|
||||
* 计算公式
|
||||
*/
|
||||
private String formula;
|
||||
|
||||
/**
|
||||
* 物模型来源类型(0-设备上报,1-纯计算,2-其他预留)
|
||||
*/
|
||||
private Integer isCalculated;
|
||||
|
||||
/**
|
||||
* modbus参数配置
|
||||
*/
|
||||
|
||||
@ -570,6 +570,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
|
||||
// 执行计算
|
||||
value = String.valueOf(CaculateUtils.execute(formula, params));
|
||||
log.info("=>纯计算物模型结果[{}],标识符:[{}],结果:{}", input.getProductId(), identity, value);
|
||||
item.setValue(value);
|
||||
}
|
||||
/* ★★★★★★★★★★★★★★★★★★★★★★ 数据计算 -结束 ★★★★★★★★★★★★★★★★★★★★★★*/
|
||||
@ -700,40 +701,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
}
|
||||
/* ★★★★★★★★★★★★★★★★★★★★★★ 存储数据 - 结束 ★★★★★★★★★★★★★★★★★★★★★★*/
|
||||
|
||||
/* ★★★★★★★★★★★★★★★★★★★★★★ 自动计算依赖物模型 - 开始 ★★★★★★★★★★★★★★★★★★★★★★*/
|
||||
// 记录所有已更新的物模型标识符(设备上报的)
|
||||
Set<String> updatedIdentifiers = new HashSet<>();
|
||||
for (ThingsModelSimpleItem item : input.getThingsModelValueRemarkItem()) {
|
||||
String identity = item.getId();
|
||||
if (identity.startsWith("array_")) {
|
||||
identity = identity.substring(9);
|
||||
}
|
||||
updatedIdentifiers.add(identity);
|
||||
}
|
||||
|
||||
// 自动计算依赖已更新物模型的其他物模型(纯计算型物模型)
|
||||
if (!updatedIdentifiers.isEmpty() && !isShadow) {
|
||||
// 用于存储计算型物模型的数据
|
||||
Map<String, String> calculatedMaps = new HashMap<>();
|
||||
List<DeviceLog> calculatedDeviceLogList = new ArrayList<>();
|
||||
|
||||
calculateDependentThingsModels(input, updatedIdentifiers, nowDate, calculatedMaps, calculatedDeviceLogList, type);
|
||||
|
||||
// 将计算后的值更新到缓存
|
||||
if (!calculatedMaps.isEmpty()) {
|
||||
redisCache.hashPutAll(key, calculatedMaps);
|
||||
log.debug("=>自动计算依赖物模型,更新缓存成功,设备编号:[{}],计算型物模型数量:[{}]",
|
||||
input.getDeviceNumber(), calculatedMaps.size());
|
||||
}
|
||||
|
||||
// 保存计算型物模型的历史数据
|
||||
if (!CollectionUtils.isEmpty(calculatedDeviceLogList)) {
|
||||
logService.saveDeviceLogBatch(calculatedDeviceLogList);
|
||||
log.debug("=>自动计算依赖物模型,保存历史数据成功,设备编号:[{}],历史数据数量:[{}]",
|
||||
input.getDeviceNumber(), calculatedDeviceLogList.size());
|
||||
}
|
||||
}
|
||||
/* ★★★★★★★★★★★★★★★★★★★★★★ 自动计算依赖物模型 - 结束 ★★★★★★★★★★★★★★★★★★★★★★*/
|
||||
// 注意:纯计算物模型已在 ModbusProtocol 中构造 ThingsModelSimpleItem(值为0)
|
||||
|
||||
return list;
|
||||
}
|
||||
@ -1956,151 +1924,6 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 自动计算依赖已更新物模型的其他物模型(纯计算型物模型)
|
||||
* 当设备只上报频率时,自动计算依赖频率的压力等物模型
|
||||
*
|
||||
* @param input 设备上报数据输入
|
||||
* @param updatedIdentifiers 已更新的物模型标识符集合(设备上报的)
|
||||
* @param nowDate 数据记录时间
|
||||
* @param maps 物模型值映射(用于批量更新缓存)
|
||||
* @param deviceLogList 设备日志列表(用于存储历史数据)
|
||||
* @param type 物模型类型(1=属性,2=功能,3=事件)
|
||||
*/
|
||||
private void calculateDependentThingsModels(ThingsModelValuesInput input,
|
||||
Set<String> updatedIdentifiers,
|
||||
Date nowDate,
|
||||
Map<String, String> maps,
|
||||
List<DeviceLog> deviceLogList,
|
||||
int type) {
|
||||
try {
|
||||
// 查询产品下所有物模型
|
||||
com.fastbee.iot.domain.ThingsModel queryModel = new com.fastbee.iot.domain.ThingsModel();
|
||||
queryModel.setProductId(input.getProductId());
|
||||
List<com.fastbee.iot.domain.ThingsModel> allThingsModels = thingsModelService.selectThingsModelList(queryModel);
|
||||
|
||||
if (CollectionUtils.isEmpty(allThingsModels)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取设备信息(用于存储历史数据)
|
||||
Device device = this.selectDeviceBySerialNumber(input.getDeviceNumber());
|
||||
if (device == null) {
|
||||
log.warn("=>自动计算依赖物模型时,设备不存在[{}]", input.getDeviceNumber());
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历所有物模型,找出依赖已更新物模型的计算型物模型
|
||||
for (com.fastbee.iot.domain.ThingsModel thingsModel : allThingsModels) {
|
||||
String dependentIdentifier = thingsModel.getIdentifier();
|
||||
|
||||
// 跳过已更新的物模型(设备已上报的)
|
||||
if (updatedIdentifiers.contains(dependentIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 只处理有计算公式的物模型
|
||||
String formula = thingsModel.getFormula();
|
||||
if (StringUtils.isEmpty(formula)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查计算公式是否引用了已更新的物模型
|
||||
Set<String> referencedIdentifiers = parseFormulaPlaceholders(formula);
|
||||
|
||||
// 检查是否至少引用了一个已更新的物模型
|
||||
boolean hasDependency = false;
|
||||
for (String refIdentifier : referencedIdentifiers) {
|
||||
if (updatedIdentifiers.contains(refIdentifier)) {
|
||||
hasDependency = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有依赖已更新的物模型,跳过
|
||||
if (!hasDependency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查所有引用的物模型是否都有值(从缓存中获取)
|
||||
Map<String, String> params = new HashMap<>();
|
||||
|
||||
for (String refIdentifier : referencedIdentifiers) {
|
||||
try {
|
||||
ValueItem refValueItem = itslValueCache.getCacheIdentifier(
|
||||
input.getProductId(),
|
||||
input.getDeviceNumber(),
|
||||
refIdentifier
|
||||
);
|
||||
if (refValueItem != null && StringUtils.isNotEmpty(refValueItem.getValue())) {
|
||||
params.put("{" + refIdentifier + "}", refValueItem.getValue());
|
||||
} else {
|
||||
// 如果引用的物模型值不存在,使用0作为默认值
|
||||
params.put("{" + refIdentifier + "}", "0");
|
||||
log.warn("=>自动计算依赖物模型时,引用的物模型值不存在[{}],标识符:[{}],使用默认值0",
|
||||
input.getProductId(), refIdentifier);
|
||||
// 如果依赖的物模型没有值,仍然尝试计算(使用默认值0)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("=>自动计算依赖物模型时,获取引用的物模型值失败[{}],标识符:[{}],错误:{}",
|
||||
input.getProductId(), refIdentifier, e.getMessage());
|
||||
params.put("{" + refIdentifier + "}", "0");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果公式中有 %s,使用0作为默认值(因为这是纯计算型物模型,没有设备上报的原始值)
|
||||
if (formula.contains("%s")) {
|
||||
params.put("%s", "0");
|
||||
}
|
||||
|
||||
// 执行计算
|
||||
String calculatedValue;
|
||||
try {
|
||||
calculatedValue = String.valueOf(CaculateUtils.execute(formula, params));
|
||||
} catch (Exception e) {
|
||||
log.error("=>自动计算依赖物模型时,计算公式执行失败[{}],标识符:[{}],公式:[{}],错误:{}",
|
||||
input.getProductId(), dependentIdentifier, formula, e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
ValueItem valueItem = new ValueItem();
|
||||
valueItem.setId(dependentIdentifier);
|
||||
valueItem.setValue(calculatedValue);
|
||||
valueItem.setShadow(calculatedValue);
|
||||
valueItem.setTs(DateUtils.getNowDate());
|
||||
maps.put(dependentIdentifier, JSONObject.toJSONString(valueItem));
|
||||
|
||||
// 获取物模型详细信息(用于存储历史数据)
|
||||
ThingsModelValueItem thingModelValue = thingsModelService.getSingleThingModels(
|
||||
input.getProductId(), dependentIdentifier);
|
||||
if (thingModelValue != null && thingModelValue.getType() == 1 && thingModelValue.getIsHistory() == 1) {
|
||||
// 存储历史数据(仅属性类型且启用了历史存储)
|
||||
DeviceLog deviceLog = new DeviceLog();
|
||||
deviceLog.setSerialNumber(input.getDeviceNumber());
|
||||
deviceLog.setLogType(type);
|
||||
deviceLog.setMode(2); // 在线模式
|
||||
deviceLog.setLogValue(calculatedValue);
|
||||
deviceLog.setRemark("自动计算"); // 标记为自动计算
|
||||
deviceLog.setIdentity(dependentIdentifier);
|
||||
deviceLog.setCreateTime(nowDate);
|
||||
deviceLog.setUserId(device.getTenantId());
|
||||
deviceLog.setUserName(device.getTenantName());
|
||||
deviceLog.setTenantId(device.getTenantId());
|
||||
deviceLog.setTenantName(device.getTenantName());
|
||||
deviceLog.setModelName(thingModelValue.getName());
|
||||
deviceLog.setIsMonitor(thingModelValue.getIsMonitor());
|
||||
deviceLogList.add(deviceLog);
|
||||
}
|
||||
|
||||
log.debug("=>自动计算依赖物模型成功[{}],标识符:[{}],计算公式:[{}],计算结果:[{}]",
|
||||
input.getProductId(), dependentIdentifier, formula, calculatedValue);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("=>自动计算依赖物模型时发生异常[{}],设备编号:[{}],错误:{}",
|
||||
input.getProductId(), input.getDeviceNumber(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import com.fastbee.common.exception.ServiceException;
|
||||
import com.fastbee.common.utils.DateUtils;
|
||||
import com.fastbee.common.utils.SecurityUtils;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.iot.cache.ICalculatedThingsModelCache;
|
||||
import com.fastbee.iot.cache.ITSLCache;
|
||||
import com.fastbee.iot.domain.*;
|
||||
import com.fastbee.iot.mapper.*;
|
||||
@ -67,6 +68,8 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
private ITSLValueCache thingModelCache;
|
||||
@Resource
|
||||
private ITSLCache itslCache;
|
||||
@Resource
|
||||
private ICalculatedThingsModelCache calculatedThingsModelCache;
|
||||
|
||||
private static List<SceneModelData> getSceneModelData(SceneModelDevice sceneModelDevice, List<ThingsModelDTO> thingsModelDTOList) {
|
||||
List<SceneModelData> modelDataList = new ArrayList<>();
|
||||
@ -154,9 +157,17 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
thingsModel.setTenantName(user.getUserName());
|
||||
}
|
||||
thingsModel.setCreateTime(DateUtils.getNowDate());
|
||||
// 自动判断是否为纯计算物模型:如果公式包含{},则设置为1(纯计算)
|
||||
if (thingsModel.getIsCalculated() == null) {
|
||||
thingsModel.setIsCalculated(determineIsCalculated(thingsModel.getFormula()));
|
||||
}
|
||||
int result = thingsModelMapper.insertThingsModel(thingsModel);
|
||||
// 更新redis缓存
|
||||
itslCache.setCacheThingsModelByProductId(thingsModel.getProductId());
|
||||
// 刷新纯计算物模型缓存
|
||||
if (result > 0) {
|
||||
calculatedThingsModelCache.refreshCalculatedCache(thingsModel.getProductId());
|
||||
}
|
||||
if (result > 0) {
|
||||
// 更新场景管理变量
|
||||
List<SceneModelDevice> sceneModelDeviceList = sceneModelDeviceMapper.listDeviceByProductId(thingsModel.getProductId());
|
||||
@ -233,6 +244,8 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
}
|
||||
//更新redis缓存
|
||||
itslCache.setCacheThingsModelByProductId(input.getProductId());
|
||||
// 刷新纯计算物模型缓存
|
||||
calculatedThingsModelCache.refreshCalculatedCache(input.getProductId());
|
||||
}
|
||||
return repeatCount;
|
||||
}
|
||||
@ -257,9 +270,17 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
Boolean isRepeat = list.stream().anyMatch(x -> x.getIdentifier().equals(thingsModel.getIdentifier()) && x.getModelId().longValue() != thingsModel.getModelId());
|
||||
if (!isRepeat) {
|
||||
thingsModel.setUpdateTime(DateUtils.getNowDate());
|
||||
// 自动判断是否为纯计算物模型:如果公式包含{},则设置为1(纯计算)
|
||||
if (thingsModel.getIsCalculated() == null) {
|
||||
thingsModel.setIsCalculated(determineIsCalculated(thingsModel.getFormula()));
|
||||
}
|
||||
int result = thingsModelMapper.updateThingsModel(thingsModel);
|
||||
// 更新redis缓存
|
||||
itslCache.setCacheThingsModelByProductId(thingsModel.getProductId());
|
||||
// 刷新纯计算物模型缓存
|
||||
if (result > 0) {
|
||||
calculatedThingsModelCache.refreshCalculatedCache(thingsModel.getProductId());
|
||||
}
|
||||
if (result > 0) {
|
||||
List<SceneModelData> sceneModelDataList = sceneModelDataMapper.selectSceneDeviceThingsModelList(thingsModel.getModelId());
|
||||
if (org.apache.commons.collections4.CollectionUtils.isEmpty(sceneModelDataList)) {
|
||||
@ -300,6 +321,10 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
}
|
||||
// 更新redis缓存
|
||||
itslCache.setCacheThingsModelByProductId(thingsModel.getProductId());
|
||||
// 刷新纯计算物模型缓存
|
||||
if (result > 0 && thingsModel != null && thingsModel.getProductId() != null) {
|
||||
calculatedThingsModelCache.refreshCalculatedCache(thingsModel.getProductId());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -315,7 +340,13 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
ThingsModel thingsModel = thingsModelMapper.selectThingsModelByModelId(modelId, SecurityUtils.getLanguage());
|
||||
int result = thingsModelMapper.deleteThingsModelByModelId(modelId);
|
||||
// 更新redis缓存
|
||||
if (thingsModel != null && thingsModel.getProductId() != null) {
|
||||
itslCache.setCacheThingsModelByProductId(thingsModel.getProductId());
|
||||
// 刷新纯计算物模型缓存
|
||||
if (result > 0) {
|
||||
calculatedThingsModelCache.refreshCalculatedCache(thingsModel.getProductId());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -343,6 +374,7 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
item.setDatatype(JSONObject.parseObject(selectModel.getSpecs(), Datatype.class));
|
||||
item.setOrder(selectModel.getModelOrder());
|
||||
item.setFormula(selectModel.getFormula());
|
||||
item.setIsCalculated(selectModel.getIsCalculated());
|
||||
itslCache.setCacheThingsModelByProductId(productId);
|
||||
return item;
|
||||
}
|
||||
@ -736,4 +768,22 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
return thingsModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为纯计算物模型
|
||||
* 如果公式包含{},则认为是纯计算物模型(依赖其他物模型计算值)
|
||||
*
|
||||
* @param formula 计算公式
|
||||
* @return 0=设备上报(默认),1=纯计算,2=其他(预留)
|
||||
*/
|
||||
private Integer determineIsCalculated(String formula) {
|
||||
if (StringUtils.isEmpty(formula)) {
|
||||
return 0; // 默认设备上报
|
||||
}
|
||||
// 如果公式包含{},则认为是纯计算物模型
|
||||
if (formula.contains("{") && formula.contains("}")) {
|
||||
return 1; // 纯计算
|
||||
}
|
||||
return 0; // 设备上报
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
<result property="type" column="type"/>
|
||||
<result property="datatype" column="datatype"/>
|
||||
<result property="formula" column="formula"/>
|
||||
<result property="isCalculated" column="is_calculated"/>
|
||||
<result property="specs" column="specs"/>
|
||||
<result property="isChart" column="is_chart"/>
|
||||
<result property="isSharePerm" column="is_share_perm"/>
|
||||
@ -57,6 +58,7 @@
|
||||
type,
|
||||
datatype,
|
||||
formula,
|
||||
is_calculated,
|
||||
specs,
|
||||
is_chart,
|
||||
is_share_perm,
|
||||
@ -83,7 +85,7 @@
|
||||
end as model_name,
|
||||
m.model_name as model_name_zh_cn, t.en_us as model_name_en_us,
|
||||
m.product_id, m.product_name, m.tenant_id, m.tenant_name, m.identifier, m.type, m.datatype, m.formula,
|
||||
m.specs, m.is_chart, m.is_share_perm, m.is_history, m.is_monitor, m.is_app, m.del_flag, m.create_by,
|
||||
m.is_calculated, m.specs, m.is_chart, m.is_share_perm, m.is_history, m.is_monitor, m.is_app, m.del_flag, m.create_by,
|
||||
m.create_time, m.update_by, m.update_time, m.remark, m.is_readonly, m.model_order
|
||||
from iot_things_model m
|
||||
left join iot_things_model_translate t on m.model_id = t.id
|
||||
@ -138,7 +140,7 @@
|
||||
else m.model_name
|
||||
end as model_name,
|
||||
m.product_id, m.product_name, m.tenant_id, m.tenant_name, m.identifier, m.type, m.datatype, m.formula,
|
||||
m.specs, m.is_chart, m.is_share_perm, m.is_history, m.is_monitor, m.is_app, m.del_flag, m.create_by,
|
||||
m.is_calculated, m.specs, m.is_chart, m.is_share_perm, m.is_history, m.is_monitor, m.is_app, m.del_flag, m.create_by,
|
||||
m.create_time, m.update_by, m.update_time, m.remark, m.is_readonly, m.model_order
|
||||
from iot_things_model m
|
||||
left join iot_things_model_translate t on m.model_id = t.id
|
||||
@ -171,7 +173,7 @@
|
||||
else m.model_name
|
||||
end as model_name,
|
||||
m.product_id, m.product_name, m.tenant_id, m.tenant_name, m.identifier, m.type, m.datatype, m.formula,
|
||||
m.specs, m.is_chart, m.is_share_perm, m.is_history, m.is_monitor, m.is_app, m.del_flag, m.create_by,
|
||||
m.is_calculated, m.specs, m.is_chart, m.is_share_perm, m.is_history, m.is_monitor, m.is_app, m.del_flag, m.create_by,
|
||||
m.create_time, m.update_by, m.update_time, m.remark, m.is_readonly, m.model_order
|
||||
from iot_things_model m
|
||||
left join iot_things_model_translate t on m.model_id = t.id
|
||||
@ -216,6 +218,9 @@
|
||||
<if test="formula != null">
|
||||
formula,
|
||||
</if>
|
||||
<if test="isCalculated != null">
|
||||
is_calculated,
|
||||
</if>
|
||||
<if test="specs != null and specs != ''">
|
||||
specs,
|
||||
</if>
|
||||
@ -287,6 +292,9 @@
|
||||
<if test="formula != null">
|
||||
#{formula},
|
||||
</if>
|
||||
<if test="isCalculated != null">
|
||||
#{isCalculated},
|
||||
</if>
|
||||
<if test="specs != null and specs != ''">
|
||||
#{specs},
|
||||
</if>
|
||||
@ -352,7 +360,8 @@
|
||||
is_readonly,
|
||||
create_by,
|
||||
create_time,
|
||||
formula
|
||||
formula,
|
||||
is_calculated
|
||||
)
|
||||
VALUES
|
||||
<foreach item="model" collection="list" separator=",">
|
||||
@ -361,7 +370,7 @@
|
||||
#{model.tenantName},#{model.identifier},#{model.type},#{model.datatype},
|
||||
#{model.specs},#{model.isChart},#{model.isSharePerm},#{model.isHistory},
|
||||
#{model.isMonitor},#{model.isApp},#{model.isReadonly},#{model.createBy},#{model.createTime},
|
||||
#{model.formula}
|
||||
#{model.formula},#{model.isCalculated}
|
||||
</trim>
|
||||
</foreach>
|
||||
</insert>
|
||||
@ -396,6 +405,9 @@
|
||||
<if test="formula != null">
|
||||
formula = #{formula},
|
||||
</if>
|
||||
<if test="isCalculated != null">
|
||||
is_calculated = #{isCalculated},
|
||||
</if>
|
||||
<if test="specs != null and specs != ''">
|
||||
specs = #{specs},
|
||||
</if>
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
-- 为iot_modbus_job表添加execute_time字段
|
||||
-- 用于记录设备上电触发任务的执行时间
|
||||
|
||||
ALTER TABLE `iot_modbus_job`
|
||||
ADD COLUMN `execute_time` datetime NULL DEFAULT NULL COMMENT '执行时间(仅设备上电触发任务使用,记录任务执行的时间)' AFTER `remark`;
|
||||
|
||||
35
sql/add_field_extension_file.sql
Normal file
35
sql/add_field_extension_file.sql
Normal file
@ -0,0 +1,35 @@
|
||||
-- 一、为iot_modbus_job表添加execute_time字段
|
||||
-- 用于记录设备上电触发任务的执行时间
|
||||
|
||||
ALTER TABLE `iot_modbus_job`
|
||||
ADD COLUMN `execute_time` datetime NULL DEFAULT NULL COMMENT '执行时间(仅设备上电触发任务使用,记录任务执行的时间)' AFTER `remark`;
|
||||
|
||||
-- 二. 在字典表中添加"文档"类型,用于首页公告下载pdf等文档
|
||||
INSERT INTO `sys_dict_data` VALUES (NULL, 3, '文档', '3', 'sys_notice_type', '', 'info', 'N', '0', 'admin', NOW(), '', NULL, '文档类型');
|
||||
|
||||
-- 2. 在 sys_notice 表中添加文件路径字段
|
||||
ALTER TABLE `sys_notice` ADD COLUMN `file_path` VARCHAR(500) NULL COMMENT '文件路径(文档类型使用)' AFTER `notice_content`;
|
||||
|
||||
-- 检查表结构
|
||||
SELECT * FROM sys_dict_data_translate WHERE id = 738;
|
||||
|
||||
-- 如果没有记录,插入翻译数据
|
||||
INSERT INTO sys_dict_data_translate (id, zh_cn, en_us)
|
||||
VALUES (738, '文档', 'Document')
|
||||
ON DUPLICATE KEY UPDATE zh_cn = '文档', en_us = 'Document';
|
||||
|
||||
|
||||
-- 三、物模型改造:添加纯计算物模型标识字段
|
||||
-- is_calculated: 0=设备上报(默认),1=纯计算(计算公式包含{}),2=其他(预留扩展)
|
||||
ALTER TABLE `iot_things_model`
|
||||
ADD COLUMN `is_calculated` tinyint(1) NOT NULL DEFAULT 0
|
||||
COMMENT '物模型来源类型(0-设备上报,1-纯计算,2-其他预留)'
|
||||
AFTER `formula`;
|
||||
|
||||
-- 为已有数据设置 is_calculated:如果公式包含{},则设置为1(纯计算)
|
||||
UPDATE `iot_things_model`
|
||||
SET `is_calculated` = 1
|
||||
WHERE `formula` IS NOT NULL
|
||||
AND `formula` != ''
|
||||
AND `formula` LIKE '%{%}%'
|
||||
AND `del_flag` = '0';
|
||||
@ -175,14 +175,21 @@
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||||
<el-card shadow="hover" class="message-card">
|
||||
<!-- 信息栏 -->
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||||
<el-card shadow="hover" class="message-card message-card-expanded">
|
||||
<h3 style="font-weight: bold; margin-top: 10px">
|
||||
<i class="el-icon-s-order"></i>
|
||||
{{ $t('home.information') }}
|
||||
</h3>
|
||||
<div class="notice-bar" :class="{ animating: animate }">
|
||||
<div
|
||||
class="notice-bar"
|
||||
:class="{ animating: animate && !isScrolling }"
|
||||
ref="noticeBar"
|
||||
@wheel="handleWheel"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div
|
||||
style="cursor: pointer; display: table; width: 100%; line-height: 36px"
|
||||
v-for="item in noticeList"
|
||||
@ -205,12 +212,13 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||||
<!-- mqtt状态数据 -->
|
||||
<!-- 应飞哥要求隐藏 2026-01-01 -->
|
||||
<!-- <el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||||
<!- mqtt状态数据 ->
|
||||
<div class="card-container">
|
||||
<div ref="statsChart" style="height: 240px; width: 100%"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-col> -->
|
||||
</el-row>
|
||||
|
||||
<el-card class="rate" shadow="hover" style="margin: 10px 10px 20px 10px">
|
||||
@ -407,7 +415,9 @@ export default {
|
||||
version: '3.8.0',
|
||||
animate: true,
|
||||
duration: 10, // 公告滚动一次的时长(单位:秒)
|
||||
interval: 5000, // 公告间隔时间(单位:毫秒)
|
||||
interval: 4000, // 公告间隔时间(单位:毫秒)
|
||||
isScrolling: false, // 是否正在使用鼠标滚轮滚动
|
||||
scrollResetTimer: null, // 滚动重置定时器
|
||||
// 服务器信息
|
||||
server: {
|
||||
jvm: {
|
||||
@ -458,6 +468,11 @@ export default {
|
||||
clearInterval(this.weatherTimer);
|
||||
this.weatherTimer = null;
|
||||
}
|
||||
// 清除滚动重置定时器
|
||||
if (this.scrollResetTimer) {
|
||||
clearTimeout(this.scrollResetTimer);
|
||||
this.scrollResetTimer = null;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.init();
|
||||
@ -467,8 +482,8 @@ export default {
|
||||
// 先尝试获取浏览器位置,然后获取天气
|
||||
this.getLocationAndWeather();
|
||||
|
||||
this.getMqttStats();
|
||||
this.statisticMqtt();
|
||||
// this.getMqttStats();
|
||||
// this.statisticMqtt();
|
||||
|
||||
// 设置天气自动刷新(每30分钟刷新一次)
|
||||
this.weatherTimer = setInterval(() => {
|
||||
@ -478,16 +493,57 @@ export default {
|
||||
methods: {
|
||||
// 公告栏滚动
|
||||
startScroll() {
|
||||
// 只有在没有手动滚动且没有鼠标悬停时才启动自动滚动
|
||||
if (!this.isScrolling) {
|
||||
this.stopScroll(); // 先清除可能存在的定时器
|
||||
this.intervalId = setInterval(() => {
|
||||
const firstNotice = this.noticeList.shift();
|
||||
this.noticeList.push(firstNotice);
|
||||
}, this.interval);
|
||||
}
|
||||
},
|
||||
stopScroll() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
},
|
||||
// 处理鼠标滚轮事件
|
||||
handleWheel(event) {
|
||||
const noticeBar = this.$refs.noticeBar;
|
||||
if (noticeBar) {
|
||||
// 标记用户正在手动滚动
|
||||
if (!this.isScrolling) {
|
||||
this.isScrolling = true;
|
||||
this.stopScroll();
|
||||
}
|
||||
// 重置滚动标志的定时器
|
||||
if (this.scrollResetTimer) {
|
||||
clearTimeout(this.scrollResetTimer);
|
||||
}
|
||||
// 滚动停止后2秒,重置标志
|
||||
this.scrollResetTimer = setTimeout(() => {
|
||||
this.isScrolling = false;
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
// 鼠标进入信息栏
|
||||
handleMouseEnter() {
|
||||
// 鼠标悬停时暂停自动滚动
|
||||
this.stopScroll();
|
||||
},
|
||||
// 鼠标离开信息栏
|
||||
handleMouseLeave() {
|
||||
// 清除滚动重置定时器
|
||||
if (this.scrollResetTimer) {
|
||||
clearTimeout(this.scrollResetTimer);
|
||||
this.scrollResetTimer = null;
|
||||
}
|
||||
// 重置滚动标志
|
||||
this.isScrolling = false;
|
||||
// 鼠标离开后,恢复自动滚动
|
||||
this.startScroll();
|
||||
},
|
||||
// 折线图
|
||||
getChartData() {
|
||||
searchUserCount().then((response) => {
|
||||
@ -1058,10 +1114,11 @@ export default {
|
||||
getNoticeList() {
|
||||
let queryParams = {
|
||||
pageNum: 1,
|
||||
pageSize: 6,
|
||||
pageSize: 20,
|
||||
};
|
||||
listNotice(queryParams).then((response) => {
|
||||
this.noticeList = response.rows.splice(0, 6);
|
||||
// 显示所有查询到的数据,不限制数量
|
||||
this.noticeList = response.rows || [];
|
||||
});
|
||||
},
|
||||
// 下载文件
|
||||
@ -2841,6 +2898,21 @@ $aqi-unhealthy-color: #f44336;
|
||||
box-shadow: 0 8px 30px $hover-shadow-color;
|
||||
}
|
||||
}
|
||||
// 扩大后的信息栏
|
||||
.message-card-expanded {
|
||||
height: 500px; // 与地图高度一致
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
// 柱状图
|
||||
.card-container {
|
||||
height: 240px;
|
||||
@ -2988,8 +3060,30 @@ $aqi-unhealthy-color: #f44336;
|
||||
|
||||
// 公告栏容器样式调整
|
||||
.notice-bar {
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
max-height: 450px;
|
||||
padding-right: 5px;
|
||||
|
||||
// 自定义滚动条样式
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover {
|
||||
background: #555;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保每个公告项有适当间距
|
||||
& > div {
|
||||
|
||||
@ -91,19 +91,19 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 文档类型时显示文件上传 -->
|
||||
<el-col :span="24" v-if="isDocumentType">
|
||||
<el-col :span="24" v-if="form.noticeType == '3'">
|
||||
<el-form-item label="文档上传" prop="filePath">
|
||||
<file-upload
|
||||
v-model="form.filePath"
|
||||
:file-type="['pdf', 'doc', 'docx']"
|
||||
:file-size="50"
|
||||
:limit="1"
|
||||
:upload-file-url="getUploadUrl()"
|
||||
:upload-file-url="process.env.VUE_APP_BASE_API + '/common/upload'"
|
||||
></file-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 非文档类型时显示内容编辑器 -->
|
||||
<el-col :span="24" v-if="!isDocumentType">
|
||||
<el-col :span="24" v-if="form.noticeType != '3'">
|
||||
<el-form-item :label="$t('system.notice.670989-7')">
|
||||
<editor v-model="form.noticeContent" :min-height="192" />
|
||||
</el-form-item>
|
||||
@ -166,7 +166,7 @@ export default {
|
||||
filePath: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (this.isDocumentType && !value) {
|
||||
if (this.form.noticeType == '3' && !value) {
|
||||
callback(new Error('文档类型必须上传文件'));
|
||||
} else {
|
||||
callback();
|
||||
@ -178,16 +178,6 @@ export default {
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 判断是否为文档类型
|
||||
isDocumentType() {
|
||||
if (!this.form || this.form.noticeType === undefined || this.form.noticeType === null) {
|
||||
return false;
|
||||
}
|
||||
// 兼容字符串和数字类型
|
||||
return this.form.noticeType == '3';
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
},
|
||||
@ -221,14 +211,9 @@ export default {
|
||||
// 处理公告类型变化
|
||||
handleNoticeTypeChange(value) {
|
||||
// 切换类型时清空不相关的字段
|
||||
const noticeType = String(value || '');
|
||||
if (noticeType === '3') {
|
||||
if (value == '3') {
|
||||
// 文档类型,清空内容
|
||||
this.form.noticeContent = undefined;
|
||||
// 触发文件路径验证
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form && this.$refs.form.validateField('filePath');
|
||||
});
|
||||
} else {
|
||||
// 非文档类型,清空文件路径
|
||||
this.form.filePath = undefined;
|
||||
@ -300,12 +285,6 @@ export default {
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
/** 获取上传URL */
|
||||
getUploadUrl() {
|
||||
// 使用 window.location.origin 作为备用方案
|
||||
const baseUrl = process.env.VUE_APP_BASE_API || '/prod-api';
|
||||
return baseUrl + '/common/upload';
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user