From d4403f5d42f1ec15868b0ff5f3fdf2faf0523e62 Mon Sep 17 00:00:00 2001 From: George Date: Fri, 2 Jan 2026 17:15:51 +0800 Subject: [PATCH] =?UTF-8?q?20260102:=201=E3=80=81=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E7=BA=AF=E8=AE=A1=E7=AE=97=E7=89=A9=E6=A8=A1=E5=9E=8B=E4=B8=8E?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E4=B8=8A=E6=8A=A5=E7=89=A9=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=AD=98=E5=82=A8=EF=BC=88=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=9C=A8=E8=A7=A3=E6=9E=90=E6=95=B0=E6=8D=AE=E4=B9=8B=E5=90=8E?= =?UTF-8?q?=E5=B0=86=E7=BA=AF=E8=AE=A1=E7=AE=97=E7=89=A9=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=A1=AB=E6=95=B0=E6=8D=AE0=EF=BC=8C=E7=94=A8=E5=88=B0Concurre?= =?UTF-8?q?ntHashMap=20key:productId=20value:=E7=BA=AF=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E7=89=A9=E6=A8=A1=E5=9E=8B=E6=A0=87=E8=AF=86=E7=AC=A6=E9=9B=86?= =?UTF-8?q?=E5=90=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 6 + .../common/core/redis/RedisKeyBuilder.java | 16 ++ .../impl/DeviceReportMessageServiceImpl.java | 4 +- .../fastbee/modbus/codec/ModbusProtocol.java | 51 +++++ .../cache/ICalculatedThingsModelCache.java | 56 ++++++ .../impl/CalculatedThingsModelCacheImpl.java | 149 ++++++++++++++ .../CalculatedThingsModelRedisCacheImpl.java | 163 ++++++++++++++++ .../fastbee/iot/cache/impl/TSLCacheImpl.java | 2 + .../com/fastbee/iot/domain/ModbusConfig.java | 1 - .../com/fastbee/iot/domain/ThingsModel.java | 7 + .../ThingsModels/ThingsModelValueItem.java | 6 + .../iot/service/impl/DeviceServiceImpl.java | 181 +----------------- .../service/impl/ThingsModelServiceImpl.java | 50 +++++ .../mapper/iot/ThingsModelMapper.xml | 22 ++- sql/add_execute_time_to_modbus_job.sql | 6 - sql/add_field_extension_file.sql | 35 ++++ vue/src/views/index.vue | 126 ++++++++++-- vue/src/views/system/notice/index.vue | 43 ++--- 18 files changed, 683 insertions(+), 241 deletions(-) create mode 100644 fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/ICalculatedThingsModelCache.java create mode 100644 fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/CalculatedThingsModelCacheImpl.java create mode 100644 fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/CalculatedThingsModelRedisCacheImpl.java delete mode 100644 sql/add_execute_time_to_modbus_job.sql create mode 100644 sql/add_field_extension_file.sql diff --git a/fastbee-admin/src/main/resources/application.yml b/fastbee-admin/src/main/resources/application.yml index 33216ad7..35f649a9 100644 --- a/fastbee-admin/src/main/resources/application.yml +++ b/fastbee-admin/src/main/resources/application.yml @@ -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: diff --git a/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisKeyBuilder.java b/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisKeyBuilder.java index 8981507a..067576fd 100644 --- a/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisKeyBuilder.java +++ b/fastbee-common/src/main/java/com/fastbee/common/core/redis/RedisKeyBuilder.java @@ -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; + } + } diff --git a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceReportMessageServiceImpl.java b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceReportMessageServiceImpl.java index b9d36416..6d52e298 100644 --- a/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceReportMessageServiceImpl.java +++ b/fastbee-iot-data/src/main/java/com/fastbee/iot/data/service/impl/DeviceReportMessageServiceImpl.java @@ -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("设备不存在")); diff --git a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/codec/ModbusProtocol.java b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/codec/ModbusProtocol.java index 57d599ce..d82b03e6 100644 --- a/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/codec/ModbusProtocol.java +++ b/fastbee-protocol/fastbee-protocol-collect/src/main/java/com/fastbee/modbus/codec/ModbusProtocol.java @@ -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 simpleItemList, int isReadOnly) { + try { + // 从缓存获取纯计算物模型标识符集合(支持产品级别和设备级别) + Set calculatedIdentifiers = calculatedThingsModelCache.getCalculatedIdentifiers(productId, null); + + if (calculatedIdentifiers.isEmpty()) { + return; + } + + // 获取已上报的物模型标识符集合(避免重复) + Set 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); } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/ICalculatedThingsModelCache.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/ICalculatedThingsModelCache.java new file mode 100644 index 00000000..b785bb0c --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/ICalculatedThingsModelCache.java @@ -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 getCalculatedIdentifiersByProduct(Long productId); + + /** + * 获取设备下的纯计算物模型标识符集合 + * 如果设备有自定义物模型,返回设备级别的;否则返回产品级别的 + * @param productId 产品ID + * @param deviceId 设备ID(可选,未来扩展) + * @return 纯计算物模型标识符集合 + */ + Set getCalculatedIdentifiers(Long productId, Long deviceId); + + /** + * 设置产品下的纯计算物模型标识符集合 + * @param productId 产品ID + * @param identifiers 纯计算物模型标识符集合 + */ + void setCalculatedIdentifiersByProduct(Long productId, Set identifiers); + + /** + * 刷新产品下的纯计算物模型缓存 + * @param productId 产品ID + */ + void refreshCalculatedCache(Long productId); + + /** + * 清除产品下的纯计算物模型缓存 + * @param productId 产品ID + */ + void clearCalculatedCache(Long productId); + + /** + * 判断是否使用 Redis 缓存 + * @return true-使用Redis,false-使用本地缓存 + */ + boolean isRedisCacheEnabled(); +} + diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/CalculatedThingsModelCacheImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/CalculatedThingsModelCacheImpl.java new file mode 100644 index 00000000..23858419 --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/CalculatedThingsModelCacheImpl.java @@ -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(纯计算物模型标识符集合) + */ + private final ConcurrentHashMap> calculatedCache = new ConcurrentHashMap<>(); + + /** + * 设备级别缓存:deviceId -> Set(未来扩展) + * 当设备有自定义物模型选择时使用 + */ + private final ConcurrentHashMap> 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 getCalculatedIdentifiersByProduct(Long productId) { + if (productId == null) { + return Collections.emptySet(); + } + + // 从本地缓存获取 + Set identifiers = calculatedCache.get(productId); + if (identifiers != null) { + return identifiers; + } + + // 缓存未命中,从数据库加载 + refreshCalculatedCache(productId); + return calculatedCache.getOrDefault(productId, Collections.emptySet()); + } + + @Override + public Set getCalculatedIdentifiers(Long productId, Long deviceId) { + // 如果设备有自定义物模型选择,优先返回设备级别的 + if (deviceId != null) { + Set deviceIdentifiers = deviceCalculatedCache.get(deviceId); + if (deviceIdentifiers != null) { + return deviceIdentifiers; + } + } + + // 否则返回产品级别的 + return getCalculatedIdentifiersByProduct(productId); + } + + @Override + public void setCalculatedIdentifiersByProduct(Long productId, Set 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 allThingsModels = thingsModelService.selectThingsModelList(queryModel); + + if (CollectionUtils.isEmpty(allThingsModels)) { + calculatedCache.put(productId, Collections.emptySet()); + return; + } + + // 筛选出纯计算物模型(isCalculated == 1) + Set 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; + } +} + diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/CalculatedThingsModelRedisCacheImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/CalculatedThingsModelRedisCacheImpl.java new file mode 100644 index 00000000..91e435de --- /dev/null +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/CalculatedThingsModelRedisCacheImpl.java @@ -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 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 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 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 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 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 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 allThingsModels = thingsModelService.selectThingsModelList(queryModel); + + if (CollectionUtils.isEmpty(allThingsModels)) { + setCalculatedIdentifiersByProduct(productId, Collections.emptySet()); + return; + } + + // 筛选出纯计算物模型(isCalculated == 1) + Set 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; + } +} + +*/ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/TSLCacheImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/TSLCacheImpl.java index 229c36b3..36a9c596 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/TSLCacheImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/cache/impl/TSLCacheImpl.java @@ -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); })); diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ModbusConfig.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ModbusConfig.java index 66fb898d..947e81cd 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ModbusConfig.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ModbusConfig.java @@ -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; diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ThingsModel.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ThingsModel.java index 6e12da96..6ad5372d 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ThingsModel.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/ThingsModel.java @@ -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; + /** * 数据定义 */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ThingsModels/ThingsModelValueItem.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ThingsModels/ThingsModelValueItem.java index 3c5e06ea..e68948c7 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ThingsModels/ThingsModelValueItem.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ThingsModels/ThingsModelValueItem.java @@ -79,6 +79,12 @@ public class ThingsModelValueItem { * 计算公式 */ private String formula; + + /** + * 物模型来源类型(0-设备上报,1-纯计算,2-其他预留) + */ + private Integer isCalculated; + /** * modbus参数配置 */ diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java index 2e9a7e3c..5b1717e5 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/DeviceServiceImpl.java @@ -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 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 calculatedMaps = new HashMap<>(); - List 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 updatedIdentifiers, - Date nowDate, - Map maps, - List deviceLogList, - int type) { - try { - // 查询产品下所有物模型 - com.fastbee.iot.domain.ThingsModel queryModel = new com.fastbee.iot.domain.ThingsModel(); - queryModel.setProductId(input.getProductId()); - List 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 referencedIdentifiers = parseFormulaPlaceholders(formula); - - // 检查是否至少引用了一个已更新的物模型 - boolean hasDependency = false; - for (String refIdentifier : referencedIdentifiers) { - if (updatedIdentifiers.contains(refIdentifier)) { - hasDependency = true; - break; - } - } - - // 如果没有依赖已更新的物模型,跳过 - if (!hasDependency) { - continue; - } - - // 检查所有引用的物模型是否都有值(从缓存中获取) - Map 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); - } - } } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ThingsModelServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ThingsModelServiceImpl.java index 4856e4b9..f67abc62 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ThingsModelServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ThingsModelServiceImpl.java @@ -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 getSceneModelData(SceneModelDevice sceneModelDevice, List thingsModelDTOList) { List 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 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 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; // 设备上报 + } + } diff --git a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelMapper.xml b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelMapper.xml index edba1589..5d3dae58 100644 --- a/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelMapper.xml +++ b/fastbee-service/fastbee-iot-service/src/main/resources/mapper/iot/ThingsModelMapper.xml @@ -16,6 +16,7 @@ + @@ -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 @@ formula, + + is_calculated, + specs, @@ -287,6 +292,9 @@ #{formula}, + + #{isCalculated}, + #{specs}, @@ -352,7 +360,8 @@ is_readonly, create_by, create_time, - formula + formula, + is_calculated ) VALUES @@ -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} @@ -396,6 +405,9 @@ formula = #{formula}, + + is_calculated = #{isCalculated}, + specs = #{specs}, diff --git a/sql/add_execute_time_to_modbus_job.sql b/sql/add_execute_time_to_modbus_job.sql deleted file mode 100644 index 62fab32b..00000000 --- a/sql/add_execute_time_to_modbus_job.sql +++ /dev/null @@ -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`; - diff --git a/sql/add_field_extension_file.sql b/sql/add_field_extension_file.sql new file mode 100644 index 00000000..69e6cacb --- /dev/null +++ b/sql/add_field_extension_file.sql @@ -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'; \ No newline at end of file diff --git a/vue/src/views/index.vue b/vue/src/views/index.vue index 2bfbd6e0..ae1af5ad 100644 --- a/vue/src/views/index.vue +++ b/vue/src/views/index.vue @@ -175,14 +175,21 @@ + - - +

{{ $t('home.information') }}

-
+
- - + + @@ -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() { - this.intervalId = setInterval(() => { - const firstNotice = this.noticeList.shift(); - this.noticeList.push(firstNotice); - }, this.interval); + // 只有在没有手动滚动且没有鼠标悬停时才启动自动滚动 + 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 { diff --git a/vue/src/views/system/notice/index.vue b/vue/src/views/system/notice/index.vue index 7e4df157..b17bad68 100644 --- a/vue/src/views/system/notice/index.vue +++ b/vue/src/views/system/notice/index.vue @@ -91,19 +91,19 @@ - + - - + @@ -164,30 +164,20 @@ export default { noticeTitle: [{ required: true, message: this.$t('system.notice.670989-9'), trigger: 'blur' }], noticeType: [{ required: true, message: this.$t('system.notice.670989-10'), trigger: 'change' }], filePath: [ - { + { validator: (rule, value, callback) => { - if (this.isDocumentType && !value) { - callback(new Error('文档类型必须上传文件')); + if (this.form.noticeType == '3' && !value) { + callback(new Error('文档类型必须上传文件')); } else { callback(); } - }, - trigger: 'blur' + }, + trigger: 'blur' } ], }, }; }, - 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'; - } }, };