20260102:

1、合并纯计算物模型与设备上报物模型数据存储(方法在解析数据之后将纯计算物模型填数据0,用到ConcurrentHashMap key:productId value:纯计算物模型标识符集合)
This commit is contained in:
George 2026-01-02 17:15:51 +08:00
parent 44df912fc2
commit d4403f5d42
18 changed files with 683 additions and 241 deletions

View File

@ -8,6 +8,12 @@ fastbee:
profile: /uploadPath profile: /uploadPath
addressEnabled: true # 获取ip地址开关 addressEnabled: true # 获取ip地址开关
captchaType: math # 验证码类型 math 数组计算 char 字符验证 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: server:

View File

@ -182,4 +182,20 @@ public class RedisKeyBuilder {
return FastBeeConstant.REDIS.POWER_ON_TRIGGER_JOB_IDS + serialNumber.toUpperCase(); 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;
}
} }

View File

@ -101,7 +101,7 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
switch (bo.getServerType()) { switch (bo.getServerType()) {
case MQTT: case MQTT:
log.debug("=>MQ*收到设备主题[{}],消息:[{}]", bo.getTopicName(), bo.getData()); log.debug("{}=>MQ*收到设备主题[{}],消息:[{}]", serialNumber, bo.getTopicName(), bo.getData());
//构建消息 //构建消息
DeviceStatusVO deviceStatusVO = buildReport(bo); DeviceStatusVO deviceStatusVO = buildReport(bo);
/*获取协议处理器*/ /*获取协议处理器*/
@ -131,7 +131,7 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
processNoSub(reportMessage, bo.getTopicName()); processNoSub(reportMessage, bo.getTopicName());
break; break;
case TCP: case TCP:
log.debug("*MQ收到TCP推送消息[{}]", JSON.toJSON(bo.getThingsModelSimpleItem())); log.debug("{}*MQ收到TCP推送消息[{}]", serialNumber, JSON.toJSON(bo.getThingsModelSimpleItem()));
DeviceStatusVO deviceStatusVO1 = deviceService.selectDeviceStatusAndTransportStatus(serialNumber); DeviceStatusVO deviceStatusVO1 = deviceService.selectDeviceStatusAndTransportStatus(serialNumber);
Optional.ofNullable(deviceStatusVO1).orElseThrow(() -> new ServiceException("设备不存在")); Optional.ofNullable(deviceStatusVO1).orElseThrow(() -> new ServiceException("设备不存在"));

View File

@ -19,6 +19,7 @@ import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.gateway.CRC16Utils; import com.fastbee.common.utils.gateway.CRC16Utils;
import com.fastbee.common.utils.modbus.BitUtils; import com.fastbee.common.utils.modbus.BitUtils;
import com.fastbee.helper.ModbusProtocolHelper; import com.fastbee.helper.ModbusProtocolHelper;
import com.fastbee.iot.cache.ICalculatedThingsModelCache;
import com.fastbee.iot.cache.IModbusConfigCache; import com.fastbee.iot.cache.IModbusConfigCache;
import com.fastbee.iot.domain.Device; import com.fastbee.iot.domain.Device;
import com.fastbee.iot.domain.ModbusConfig; import com.fastbee.iot.domain.ModbusConfig;
@ -39,6 +40,7 @@ import javax.annotation.Resource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -67,6 +69,9 @@ public class ModbusProtocol implements IProtocol {
@Resource @Resource
private ModbusConfigMapper modbusConfigMapper; private ModbusConfigMapper modbusConfigMapper;
@Resource
private ICalculatedThingsModelCache calculatedThingsModelCache;
@Override @Override
public DeviceReport decode(DeviceData deviceData, String clientId) { public DeviceReport decode(DeviceData deviceData, String clientId) {
try { try {
@ -311,6 +316,52 @@ public class ModbusProtocol implements IProtocol {
simpleItemList.add(simpleItem); 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);
} }
} }

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}
*/

View File

@ -317,6 +317,8 @@ public class TSLCacheImpl implements ITSLCache {
.setName_en_US(thingsModel.getModelName_en_US()) .setName_en_US(thingsModel.getModelName_en_US())
.setOrder(thingsModel.getModelOrder()) .setOrder(thingsModel.getModelOrder())
.setModelId(thingsModel.getModelId()) .setModelId(thingsModel.getModelId())
.setFormula(thingsModel.getFormula())
.setIsCalculated(thingsModel.getIsCalculated())
.setConfig(thingsModel.getModbusConfig()); .setConfig(thingsModel.getModbusConfig());
return JSONObject.toJSONString(dto); return JSONObject.toJSONString(dto);
})); }));

View File

@ -1,6 +1,5 @@
package com.fastbee.iot.domain; package com.fastbee.iot.domain;
import com.fastbee.common.annotation.Excel;
import com.fastbee.common.core.domain.BaseEntity; import com.fastbee.common.core.domain.BaseEntity;
import com.fastbee.common.core.protocol.modbus.ModbusCode; import com.fastbee.common.core.protocol.modbus.ModbusCode;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;

View File

@ -96,6 +96,13 @@ public class ThingsModel extends BaseEntity {
@Excel(name = "计算公式", prompt = "选填,例如:%s*10,%s是占位符") @Excel(name = "计算公式", prompt = "选填,例如:%s*10,%s是占位符")
private String formula; private String formula;
/**
* 物模型来源类型(0-设备上报,1-纯计算,2-其他预留)
* 纯计算物模型:计算公式包含{},依赖其他物模型计算值
*/
@ApiModelProperty(value = "物模型来源类型", notes = "(0-设备上报,1-纯计算,2-其他预留)")
private Integer isCalculated;
/** /**
* 数据定义 * 数据定义
*/ */

View File

@ -79,6 +79,12 @@ public class ThingsModelValueItem {
* 计算公式 * 计算公式
*/ */
private String formula; private String formula;
/**
* 物模型来源类型(0-设备上报,1-纯计算,2-其他预留)
*/
private Integer isCalculated;
/** /**
* modbus参数配置 * modbus参数配置
*/ */

View File

@ -570,6 +570,7 @@ public class DeviceServiceImpl implements IDeviceService {
// 执行计算 // 执行计算
value = String.valueOf(CaculateUtils.execute(formula, params)); value = String.valueOf(CaculateUtils.execute(formula, params));
log.info("=>纯计算物模型结果[{}],标识符:[{}],结果:{}", input.getProductId(), identity, value);
item.setValue(value); item.setValue(value);
} }
/* ★★★★★★★★★★★★★★★★★★★★★★ 数据计算 -结束 ★★★★★★★★★★★★★★★★★★★★★★*/ /* ★★★★★★★★★★★★★★★★★★★★★★ 数据计算 -结束 ★★★★★★★★★★★★★★★★★★★★★★*/
@ -700,40 +701,7 @@ public class DeviceServiceImpl implements IDeviceService {
} }
/* ★★★★★★★★★★★★★★★★★★★★★★ 存储数据 - 结束 ★★★★★★★★★★★★★★★★★★★★★★*/ /* ★★★★★★★★★★★★★★★★★★★★★★ 存储数据 - 结束 ★★★★★★★★★★★★★★★★★★★★★★*/
/* ★★★★★★★★★★★★★★★★★★★★★★ 自动计算依赖物模型 - 开始 ★★★★★★★★★★★★★★★★★★★★★★*/ // 注意:纯计算物模型已在 ModbusProtocol 中构造 ThingsModelSimpleItem(值为0)
// 记录所有已更新的物模型标识符(设备上报的)
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());
}
}
/* ★★★★★★★★★★★★★★★★★★★★★★ 自动计算依赖物模型 - 结束 ★★★★★★★★★★★★★★★★★★★★★★*/
return list; 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);
}
}
} }

View File

@ -12,6 +12,7 @@ import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.SecurityUtils; import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.iot.cache.ICalculatedThingsModelCache;
import com.fastbee.iot.cache.ITSLCache; import com.fastbee.iot.cache.ITSLCache;
import com.fastbee.iot.domain.*; import com.fastbee.iot.domain.*;
import com.fastbee.iot.mapper.*; import com.fastbee.iot.mapper.*;
@ -67,6 +68,8 @@ public class ThingsModelServiceImpl implements IThingsModelService {
private ITSLValueCache thingModelCache; private ITSLValueCache thingModelCache;
@Resource @Resource
private ITSLCache itslCache; private ITSLCache itslCache;
@Resource
private ICalculatedThingsModelCache calculatedThingsModelCache;
private static List<SceneModelData> getSceneModelData(SceneModelDevice sceneModelDevice, List<ThingsModelDTO> thingsModelDTOList) { private static List<SceneModelData> getSceneModelData(SceneModelDevice sceneModelDevice, List<ThingsModelDTO> thingsModelDTOList) {
List<SceneModelData> modelDataList = new ArrayList<>(); List<SceneModelData> modelDataList = new ArrayList<>();
@ -154,9 +157,17 @@ public class ThingsModelServiceImpl implements IThingsModelService {
thingsModel.setTenantName(user.getUserName()); thingsModel.setTenantName(user.getUserName());
} }
thingsModel.setCreateTime(DateUtils.getNowDate()); thingsModel.setCreateTime(DateUtils.getNowDate());
// 自动判断是否为纯计算物模型:如果公式包含{},则设置为1(纯计算)
if (thingsModel.getIsCalculated() == null) {
thingsModel.setIsCalculated(determineIsCalculated(thingsModel.getFormula()));
}
int result = thingsModelMapper.insertThingsModel(thingsModel); int result = thingsModelMapper.insertThingsModel(thingsModel);
// 更新redis缓存 // 更新redis缓存
itslCache.setCacheThingsModelByProductId(thingsModel.getProductId()); itslCache.setCacheThingsModelByProductId(thingsModel.getProductId());
// 刷新纯计算物模型缓存
if (result > 0) {
calculatedThingsModelCache.refreshCalculatedCache(thingsModel.getProductId());
}
if (result > 0) { if (result > 0) {
// 更新场景管理变量 // 更新场景管理变量
List<SceneModelDevice> sceneModelDeviceList = sceneModelDeviceMapper.listDeviceByProductId(thingsModel.getProductId()); List<SceneModelDevice> sceneModelDeviceList = sceneModelDeviceMapper.listDeviceByProductId(thingsModel.getProductId());
@ -233,6 +244,8 @@ public class ThingsModelServiceImpl implements IThingsModelService {
} }
//更新redis缓存 //更新redis缓存
itslCache.setCacheThingsModelByProductId(input.getProductId()); itslCache.setCacheThingsModelByProductId(input.getProductId());
// 刷新纯计算物模型缓存
calculatedThingsModelCache.refreshCalculatedCache(input.getProductId());
} }
return repeatCount; 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()); Boolean isRepeat = list.stream().anyMatch(x -> x.getIdentifier().equals(thingsModel.getIdentifier()) && x.getModelId().longValue() != thingsModel.getModelId());
if (!isRepeat) { if (!isRepeat) {
thingsModel.setUpdateTime(DateUtils.getNowDate()); thingsModel.setUpdateTime(DateUtils.getNowDate());
// 自动判断是否为纯计算物模型:如果公式包含{},则设置为1(纯计算)
if (thingsModel.getIsCalculated() == null) {
thingsModel.setIsCalculated(determineIsCalculated(thingsModel.getFormula()));
}
int result = thingsModelMapper.updateThingsModel(thingsModel); int result = thingsModelMapper.updateThingsModel(thingsModel);
// 更新redis缓存 // 更新redis缓存
itslCache.setCacheThingsModelByProductId(thingsModel.getProductId()); itslCache.setCacheThingsModelByProductId(thingsModel.getProductId());
// 刷新纯计算物模型缓存
if (result > 0) {
calculatedThingsModelCache.refreshCalculatedCache(thingsModel.getProductId());
}
if (result > 0) { if (result > 0) {
List<SceneModelData> sceneModelDataList = sceneModelDataMapper.selectSceneDeviceThingsModelList(thingsModel.getModelId()); List<SceneModelData> sceneModelDataList = sceneModelDataMapper.selectSceneDeviceThingsModelList(thingsModel.getModelId());
if (org.apache.commons.collections4.CollectionUtils.isEmpty(sceneModelDataList)) { if (org.apache.commons.collections4.CollectionUtils.isEmpty(sceneModelDataList)) {
@ -300,6 +321,10 @@ public class ThingsModelServiceImpl implements IThingsModelService {
} }
// 更新redis缓存 // 更新redis缓存
itslCache.setCacheThingsModelByProductId(thingsModel.getProductId()); itslCache.setCacheThingsModelByProductId(thingsModel.getProductId());
// 刷新纯计算物模型缓存
if (result > 0 && thingsModel != null && thingsModel.getProductId() != null) {
calculatedThingsModelCache.refreshCalculatedCache(thingsModel.getProductId());
}
return result; return result;
} }
@ -315,7 +340,13 @@ public class ThingsModelServiceImpl implements IThingsModelService {
ThingsModel thingsModel = thingsModelMapper.selectThingsModelByModelId(modelId, SecurityUtils.getLanguage()); ThingsModel thingsModel = thingsModelMapper.selectThingsModelByModelId(modelId, SecurityUtils.getLanguage());
int result = thingsModelMapper.deleteThingsModelByModelId(modelId); int result = thingsModelMapper.deleteThingsModelByModelId(modelId);
// 更新redis缓存 // 更新redis缓存
if (thingsModel != null && thingsModel.getProductId() != null) {
itslCache.setCacheThingsModelByProductId(thingsModel.getProductId()); itslCache.setCacheThingsModelByProductId(thingsModel.getProductId());
// 刷新纯计算物模型缓存
if (result > 0) {
calculatedThingsModelCache.refreshCalculatedCache(thingsModel.getProductId());
}
}
return result; return result;
} }
@ -343,6 +374,7 @@ public class ThingsModelServiceImpl implements IThingsModelService {
item.setDatatype(JSONObject.parseObject(selectModel.getSpecs(), Datatype.class)); item.setDatatype(JSONObject.parseObject(selectModel.getSpecs(), Datatype.class));
item.setOrder(selectModel.getModelOrder()); item.setOrder(selectModel.getModelOrder());
item.setFormula(selectModel.getFormula()); item.setFormula(selectModel.getFormula());
item.setIsCalculated(selectModel.getIsCalculated());
itslCache.setCacheThingsModelByProductId(productId); itslCache.setCacheThingsModelByProductId(productId);
return item; return item;
} }
@ -736,4 +768,22 @@ public class ThingsModelServiceImpl implements IThingsModelService {
return thingsModel; 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; // 设备上报
}
} }

View File

@ -16,6 +16,7 @@
<result property="type" column="type"/> <result property="type" column="type"/>
<result property="datatype" column="datatype"/> <result property="datatype" column="datatype"/>
<result property="formula" column="formula"/> <result property="formula" column="formula"/>
<result property="isCalculated" column="is_calculated"/>
<result property="specs" column="specs"/> <result property="specs" column="specs"/>
<result property="isChart" column="is_chart"/> <result property="isChart" column="is_chart"/>
<result property="isSharePerm" column="is_share_perm"/> <result property="isSharePerm" column="is_share_perm"/>
@ -57,6 +58,7 @@
type, type,
datatype, datatype,
formula, formula,
is_calculated,
specs, specs,
is_chart, is_chart,
is_share_perm, is_share_perm,
@ -83,7 +85,7 @@
end as model_name, end as model_name,
m.model_name as model_name_zh_cn, t.en_us as model_name_en_us, 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.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 m.create_time, m.update_by, m.update_time, m.remark, m.is_readonly, m.model_order
from iot_things_model m from iot_things_model m
left join iot_things_model_translate t on m.model_id = t.id left join iot_things_model_translate t on m.model_id = t.id
@ -138,7 +140,7 @@
else m.model_name else m.model_name
end as 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.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 m.create_time, m.update_by, m.update_time, m.remark, m.is_readonly, m.model_order
from iot_things_model m from iot_things_model m
left join iot_things_model_translate t on m.model_id = t.id left join iot_things_model_translate t on m.model_id = t.id
@ -171,7 +173,7 @@
else m.model_name else m.model_name
end as 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.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 m.create_time, m.update_by, m.update_time, m.remark, m.is_readonly, m.model_order
from iot_things_model m from iot_things_model m
left join iot_things_model_translate t on m.model_id = t.id left join iot_things_model_translate t on m.model_id = t.id
@ -216,6 +218,9 @@
<if test="formula != null"> <if test="formula != null">
formula, formula,
</if> </if>
<if test="isCalculated != null">
is_calculated,
</if>
<if test="specs != null and specs != ''"> <if test="specs != null and specs != ''">
specs, specs,
</if> </if>
@ -287,6 +292,9 @@
<if test="formula != null"> <if test="formula != null">
#{formula}, #{formula},
</if> </if>
<if test="isCalculated != null">
#{isCalculated},
</if>
<if test="specs != null and specs != ''"> <if test="specs != null and specs != ''">
#{specs}, #{specs},
</if> </if>
@ -352,7 +360,8 @@
is_readonly, is_readonly,
create_by, create_by,
create_time, create_time,
formula formula,
is_calculated
) )
VALUES VALUES
<foreach item="model" collection="list" separator=","> <foreach item="model" collection="list" separator=",">
@ -361,7 +370,7 @@
#{model.tenantName},#{model.identifier},#{model.type},#{model.datatype}, #{model.tenantName},#{model.identifier},#{model.type},#{model.datatype},
#{model.specs},#{model.isChart},#{model.isSharePerm},#{model.isHistory}, #{model.specs},#{model.isChart},#{model.isSharePerm},#{model.isHistory},
#{model.isMonitor},#{model.isApp},#{model.isReadonly},#{model.createBy},#{model.createTime}, #{model.isMonitor},#{model.isApp},#{model.isReadonly},#{model.createBy},#{model.createTime},
#{model.formula} #{model.formula},#{model.isCalculated}
</trim> </trim>
</foreach> </foreach>
</insert> </insert>
@ -396,6 +405,9 @@
<if test="formula != null"> <if test="formula != null">
formula = #{formula}, formula = #{formula},
</if> </if>
<if test="isCalculated != null">
is_calculated = #{isCalculated},
</if>
<if test="specs != null and specs != ''"> <if test="specs != null and specs != ''">
specs = #{specs}, specs = #{specs},
</if> </if>

View File

@ -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`;

View 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';

View File

@ -175,14 +175,21 @@
</div> </div>
</el-col> </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"> <h3 style="font-weight: bold; margin-top: 10px">
<i class="el-icon-s-order"></i> <i class="el-icon-s-order"></i>
{{ $t('home.information') }} {{ $t('home.information') }}
</h3> </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 <div
style="cursor: pointer; display: table; width: 100%; line-height: 36px" style="cursor: pointer; display: table; width: 100%; line-height: 36px"
v-for="item in noticeList" v-for="item in noticeList"
@ -205,12 +212,13 @@
</el-card> </el-card>
</el-col> </el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8"> <!-- 应飞哥要求隐藏 2026-01-01 -->
<!-- mqtt状态数据 --> <!-- <el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
<!- mqtt状态数据 ->
<div class="card-container"> <div class="card-container">
<div ref="statsChart" style="height: 240px; width: 100%"></div> <div ref="statsChart" style="height: 240px; width: 100%"></div>
</div> </div>
</el-col> </el-col> -->
</el-row> </el-row>
<el-card class="rate" shadow="hover" style="margin: 10px 10px 20px 10px"> <el-card class="rate" shadow="hover" style="margin: 10px 10px 20px 10px">
@ -407,7 +415,9 @@ export default {
version: '3.8.0', version: '3.8.0',
animate: true, animate: true,
duration: 10, // 公告滚动一次的时长(单位:秒) duration: 10, // 公告滚动一次的时长(单位:秒)
interval: 5000, // 公告间隔时间(单位:毫秒) interval: 4000, // 公告间隔时间(单位:毫秒)
isScrolling: false, // 是否正在使用鼠标滚轮滚动
scrollResetTimer: null, // 滚动重置定时器
// 服务器信息 // 服务器信息
server: { server: {
jvm: { jvm: {
@ -458,6 +468,11 @@ export default {
clearInterval(this.weatherTimer); clearInterval(this.weatherTimer);
this.weatherTimer = null; this.weatherTimer = null;
} }
// 清除滚动重置定时器
if (this.scrollResetTimer) {
clearTimeout(this.scrollResetTimer);
this.scrollResetTimer = null;
}
}, },
created() { created() {
this.init(); this.init();
@ -467,8 +482,8 @@ export default {
// 先尝试获取浏览器位置,然后获取天气 // 先尝试获取浏览器位置,然后获取天气
this.getLocationAndWeather(); this.getLocationAndWeather();
this.getMqttStats(); // this.getMqttStats();
this.statisticMqtt(); // this.statisticMqtt();
// 设置天气自动刷新(每30分钟刷新一次) // 设置天气自动刷新(每30分钟刷新一次)
this.weatherTimer = setInterval(() => { this.weatherTimer = setInterval(() => {
@ -478,16 +493,57 @@ export default {
methods: { methods: {
// 公告栏滚动 // 公告栏滚动
startScroll() { startScroll() {
// 只有在没有手动滚动且没有鼠标悬停时才启动自动滚动
if (!this.isScrolling) {
this.stopScroll(); // 先清除可能存在的定时器
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
const firstNotice = this.noticeList.shift(); const firstNotice = this.noticeList.shift();
this.noticeList.push(firstNotice); this.noticeList.push(firstNotice);
}, this.interval); }, this.interval);
}
}, },
stopScroll() { stopScroll() {
if (this.intervalId) { if (this.intervalId) {
clearInterval(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() { getChartData() {
searchUserCount().then((response) => { searchUserCount().then((response) => {
@ -1058,10 +1114,11 @@ export default {
getNoticeList() { getNoticeList() {
let queryParams = { let queryParams = {
pageNum: 1, pageNum: 1,
pageSize: 6, pageSize: 20,
}; };
listNotice(queryParams).then((response) => { 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; 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 { .card-container {
height: 240px; height: 240px;
@ -2988,8 +3060,30 @@ $aqi-unhealthy-color: #f44336;
// 公告栏容器样式调整 // 公告栏容器样式调整
.notice-bar { .notice-bar {
overflow: hidden; overflow-y: auto;
overflow-x: hidden;
position: relative; 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 { & > div {

View File

@ -91,19 +91,19 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 文档类型时显示文件上传 --> <!-- 文档类型时显示文件上传 -->
<el-col :span="24" v-if="isDocumentType"> <el-col :span="24" v-if="form.noticeType == '3'">
<el-form-item label="文档上传" prop="filePath"> <el-form-item label="文档上传" prop="filePath">
<file-upload <file-upload
v-model="form.filePath" v-model="form.filePath"
:file-type="['pdf', 'doc', 'docx']" :file-type="['pdf', 'doc', 'docx']"
:file-size="50" :file-size="50"
:limit="1" :limit="1"
:upload-file-url="getUploadUrl()" :upload-file-url="process.env.VUE_APP_BASE_API + '/common/upload'"
></file-upload> ></file-upload>
</el-form-item> </el-form-item>
</el-col> </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')"> <el-form-item :label="$t('system.notice.670989-7')">
<editor v-model="form.noticeContent" :min-height="192" /> <editor v-model="form.noticeContent" :min-height="192" />
</el-form-item> </el-form-item>
@ -166,7 +166,7 @@ export default {
filePath: [ filePath: [
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (this.isDocumentType && !value) { if (this.form.noticeType == '3' && !value) {
callback(new Error('文档类型必须上传文件')); callback(new Error('文档类型必须上传文件'));
} else { } else {
callback(); 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() { created() {
this.getList(); this.getList();
}, },
@ -221,14 +211,9 @@ export default {
// 处理公告类型变化 // 处理公告类型变化
handleNoticeTypeChange(value) { handleNoticeTypeChange(value) {
// 切换类型时清空不相关的字段 // 切换类型时清空不相关的字段
const noticeType = String(value || ''); if (value == '3') {
if (noticeType === '3') {
// 文档类型,清空内容 // 文档类型,清空内容
this.form.noticeContent = undefined; this.form.noticeContent = undefined;
// 触发文件路径验证
this.$nextTick(() => {
this.$refs.form && this.$refs.form.validateField('filePath');
});
} else { } else {
// 非文档类型,清空文件路径 // 非文档类型,清空文件路径
this.form.filePath = undefined; this.form.filePath = undefined;
@ -300,12 +285,6 @@ export default {
}) })
.catch(() => {}); .catch(() => {});
}, },
/** 获取上传URL */
getUploadUrl() {
// 使用 window.location.origin 作为备用方案
const baseUrl = process.env.VUE_APP_BASE_API || '/prod-api';
return baseUrl + '/common/upload';
}
}, },
}; };
</script> </script>