20260101:
1、设备调试页面钩子函数,写入redis缓存标识,可通过切面推送错误信息到前端页面,离开页面取消标识; 2、数据记录页面一笔数据统一创建日期;优化设备日志保存从for单个改为批量 3、信息栏添加pdf文档下载
This commit is contained in:
parent
8a87041e71
commit
44df912fc2
@ -70,4 +70,10 @@ public class CacheConstants {
|
||||
* 设备OTA升级实时数据
|
||||
*/
|
||||
public static final String DEVICE_OTA_DATA = "device:ota:";
|
||||
|
||||
/**
|
||||
* 设备调试日志记录标志 redis key
|
||||
*/
|
||||
public static final String DEVICE_DEBUG_USER_KEY = "device:debug:user:";
|
||||
|
||||
}
|
||||
|
||||
@ -120,9 +120,10 @@ public class CaculateUtils {
|
||||
*/
|
||||
public static List<String> suffixHandle(String exeStr) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
Stack stack = new Stack();
|
||||
//Stack stack = new Stack();
|
||||
Deque<Character> stack = new ArrayDeque<>();
|
||||
char[] exeChars = exeStr.toCharArray();
|
||||
List<String> res = new ArrayList<String>();
|
||||
List<String> res = new ArrayList<>();
|
||||
for (char x : exeChars) {
|
||||
// 判断是不是操作符号
|
||||
if (symbol.indexOf(x) > -1) {
|
||||
@ -224,7 +225,7 @@ public class CaculateUtils {
|
||||
|| key.contains("[") || key.contains("]") || key.contains("*") || key.contains("+")
|
||||
|| key.contains("?") || key.contains("^") || key.contains("$") || key.contains("|")) {
|
||||
// 转义正则表达式特殊字符
|
||||
String escapedKey = java.util.regex.Pattern.quote(key);
|
||||
String escapedKey = Pattern.quote(key);
|
||||
str = str.replaceAll(escapedKey, value);
|
||||
} else {
|
||||
str = str.replaceAll(key, value);
|
||||
|
||||
@ -2,21 +2,22 @@ package com.fastbee.iot.data.aspect;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fastbee.common.constant.CacheConstants;
|
||||
import com.fastbee.common.core.device.DeviceAndProtocol;
|
||||
import com.fastbee.common.core.mq.DeviceReportBo;
|
||||
import com.fastbee.common.core.mq.message.DeviceData;
|
||||
import com.fastbee.common.core.mq.message.DeviceMessage;
|
||||
import com.fastbee.common.core.redis.RedisCache;
|
||||
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
|
||||
import com.fastbee.common.enums.DeviceLogTypeEnum;
|
||||
import com.fastbee.common.enums.TopicType;
|
||||
import com.fastbee.common.utils.DateUtils;
|
||||
import com.fastbee.common.utils.SecurityUtils;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.common.utils.gateway.mq.TopicsUtils;
|
||||
import com.fastbee.iot.domain.Device;
|
||||
import com.fastbee.iot.domain.DeviceLog;
|
||||
import com.fastbee.iot.domain.Product;
|
||||
import com.fastbee.iot.domain.ProductAuthorize;
|
||||
import com.fastbee.iot.domain.*;
|
||||
import com.fastbee.iot.service.IDeviceService;
|
||||
import com.fastbee.iot.service.IDeviceUserService;
|
||||
import com.fastbee.iot.service.IProductAuthorizeService;
|
||||
import com.fastbee.iot.service.IProductService;
|
||||
import com.fastbee.iot.tdengine.service.ILogService;
|
||||
@ -94,6 +95,9 @@ public class DeviceDebugLogAspect {
|
||||
@Resource
|
||||
private TopicsUtils topicsUtils;
|
||||
|
||||
@Resource
|
||||
private RedisCache redisCache;
|
||||
|
||||
/**
|
||||
* 拦截数据解析方法
|
||||
*/
|
||||
@ -184,36 +188,6 @@ public class DeviceDebugLogAspect {
|
||||
}, "记录协议解析错误日志失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备编号获取协议编码(有缓存)
|
||||
*/
|
||||
private String getProtocolCodeBySerialNumber(String serialNumber) {
|
||||
try {
|
||||
DeviceAndProtocol deviceAndProtocol =
|
||||
deviceService.selectProtocolBySerialNumber(serialNumber);
|
||||
if (deviceAndProtocol != null) {
|
||||
return deviceAndProtocol.getProtocolCode();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("获取协议编码失败: {}, 设备编号: {}", ex.getMessage(), serialNumber);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据产品ID获取协议编码(有缓存)
|
||||
*/
|
||||
private String getProtocolCodeByProductId(Long productId) {
|
||||
try {
|
||||
if (productId != null) {
|
||||
return productService.getProtocolByProductId(productId);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("根据产品ID获取协议编码失败: {}, 产品ID: {}", ex.getMessage(), productId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备编号获取产品ID(有缓存)
|
||||
*/
|
||||
@ -245,6 +219,12 @@ public class DeviceDebugLogAspect {
|
||||
log.warn("设备编号为空,跳过错误日志记录");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户是否在数据调试页面
|
||||
if (!isUserInDebugPage(context.getSerialNumber())) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveErrorLog(context);
|
||||
} catch (Exception ex) {
|
||||
log.error("{}: {}", errorLogPrefix, ex.getMessage(), ex);
|
||||
@ -318,6 +298,50 @@ public class DeviceDebugLogAspect {
|
||||
String errorMsg = e.getMessage();
|
||||
return StringUtils.isEmpty(errorMsg) ? e.getClass().getName() : errorMsg;
|
||||
}
|
||||
@Resource
|
||||
private IDeviceUserService deviceUserService;
|
||||
/**
|
||||
* 检查当前用户是否在数据调试页面
|
||||
* @return true-在调试页面,false-不在调试页面
|
||||
*/
|
||||
private boolean isUserInDebugPage(String serialNumber) {
|
||||
|
||||
// 1、从Redis中获取用户ID
|
||||
|
||||
// 2、从SecurityContext中获取用户ID
|
||||
try {
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
if (userId == null) {
|
||||
return false;
|
||||
}
|
||||
String cacheKey = CacheConstants.DEVICE_DEBUG_USER_KEY + userId;
|
||||
String flag = redisCache.getCacheObject(cacheKey);
|
||||
return "1".equals(flag);
|
||||
} catch (Exception e) {
|
||||
// 如果获取用户信息失败,默认不记录日志,避免影响正常功能
|
||||
log.debug("无法从SecurityContext获取用户ID(可能在异步线程中)");
|
||||
}
|
||||
|
||||
|
||||
// 3、从数据库中获取用户ID
|
||||
try {
|
||||
// 获取设备信息
|
||||
Device device = getDeviceBySerialNumber(serialNumber);
|
||||
if (device == null || device.getTenantId() == null) {
|
||||
return false;
|
||||
}
|
||||
String cacheKey = CacheConstants.DEVICE_DEBUG_USER_KEY + device.getTenantId();
|
||||
String flag = redisCache.getCacheObject(cacheKey);
|
||||
if ("1".equals(flag)) {
|
||||
log.debug("检测到设备[{}]的关联用户[{}]开启了调试标志", serialNumber, device.getTenantId());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.warn("检查设备用户调试状态失败: {}, 设备编号: {}", e.getMessage(), serialNumber);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查设备是否已授权
|
||||
@ -377,13 +401,13 @@ public class DeviceDebugLogAspect {
|
||||
}
|
||||
|
||||
// 构建并保存设备日志
|
||||
DeviceLog deviceLog = buildDeviceLog(device, context);
|
||||
logService.saveDeviceLog(deviceLog);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("已保存错误日志到数据库 - 设备: {}, 错误类型: {}, 错误信息: {}",
|
||||
serialNumber, context.getIdentity(),
|
||||
StringUtils.substring(context.getErrorMsg(), 0, MAX_ERROR_MSG_DISPLAY_LENGTH));
|
||||
}
|
||||
// DeviceLog deviceLog = buildDeviceLog(device, context);
|
||||
// logService.saveDeviceLog(deviceLog);
|
||||
// if (log.isDebugEnabled()) {
|
||||
// log.debug("已保存错误日志到数据库 - 设备: {}, 错误类型: {}, 错误信息: {}",
|
||||
// serialNumber, context.getIdentity(),
|
||||
// StringUtils.substring(context.getErrorMsg(), 0, MAX_ERROR_MSG_DISPLAY_LENGTH));
|
||||
// }
|
||||
|
||||
// 推送错误信息到前端页面
|
||||
pushErrorToFrontend(serialNumber, productId, context);
|
||||
|
||||
@ -11,7 +11,6 @@ import com.fastbee.common.enums.ServerType;
|
||||
import com.fastbee.common.enums.TopicType;
|
||||
import com.fastbee.common.constant.HttpStatus;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
import com.fastbee.common.utils.DateUtils;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.common.utils.gateway.mq.TopicsUtils;
|
||||
import com.fastbee.common.utils.gateway.protocol.ByteUtils;
|
||||
@ -134,6 +133,12 @@ public class PowerOnTriggerTaskExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(1000); // 延迟1秒,等待设备上线
|
||||
} catch (InterruptedException e) {
|
||||
log.error("设备上电触发任务执行失败", e);
|
||||
}
|
||||
|
||||
String pendingKey = RedisKeyBuilder.buildPowerOnTriggerPendingKey(serialNumber);
|
||||
// 关键:使用原子删除操作来防止竞态条件
|
||||
// 只有第一个成功删除 pendingKey 的线程才能继续执行任务
|
||||
@ -300,10 +305,6 @@ public class PowerOnTriggerTaskExecutor {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 记录指令到Redis缓存(用于轮询任务管理)
|
||||
String runtimeCacheKey = RedisKeyBuilder.buildModbusRuntimeCacheKey(serialNumber);
|
||||
redisCache.zSetAdd(runtimeCacheKey, command, DateUtils.getTimestampSeconds());
|
||||
|
||||
// 记录发送时间
|
||||
long sendTime = System.currentTimeMillis();
|
||||
boolean responseReceived;
|
||||
|
||||
@ -15,11 +15,11 @@ import org.quartz.JobDataMap;
|
||||
import org.quartz.JobKey;
|
||||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -30,13 +30,13 @@ import java.util.List;
|
||||
@Service
|
||||
@Slf4j
|
||||
public class DeviceJobServiceImpl implements IDeviceJobService {
|
||||
@Autowired
|
||||
@Resource
|
||||
private Scheduler scheduler;
|
||||
|
||||
@Autowired
|
||||
@Resource
|
||||
private DeviceJobMapper jobMapper;
|
||||
|
||||
@Autowired
|
||||
@Resource
|
||||
private SysJobMapper sysJobMapper;
|
||||
|
||||
|
||||
|
||||
@ -145,10 +145,10 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
|
||||
queryParam.setSerialNumber(serialNumber);
|
||||
List<ProductAuthorize> authorizeList = productAuthorizeService.selectProductAuthorizeList(queryParam);
|
||||
if (authorizeList == null || authorizeList.isEmpty()) {
|
||||
log.warn("TCP数据上报-授权码校验失败: 设备[{}]未绑定授权码,拒绝数据上报", serialNumber);
|
||||
//log.warn("TCP数据上报-授权码校验失败: 设备[{}]未绑定授权码,拒绝数据上报", serialNumber);
|
||||
return; // 拒绝数据上报
|
||||
}
|
||||
log.debug("TCP数据上报-授权码校验通过: 设备[{}]已绑定授权码", serialNumber);
|
||||
//log.debug("TCP数据上报-授权码校验通过: 设备[{}]已绑定授权码", serialNumber);
|
||||
}
|
||||
|
||||
//同步设备状态
|
||||
@ -285,10 +285,8 @@ public class DeviceReportMessageServiceImpl implements IDeviceReportMessageServi
|
||||
private void processNoSub(DeviceReport message, String topicName) {
|
||||
if (message.getServerType().equals(ServerType.MQTT)) {
|
||||
//处理topic以prop结尾上报的数据 (属性)
|
||||
if (message.getServerType().equals(ServerType.MQTT)) {
|
||||
if (!topicName.endsWith(TopicType.PROPERTY_POST.getTopicSuffix())) {
|
||||
return;
|
||||
}
|
||||
if (!topicName.endsWith(TopicType.PROPERTY_POST.getTopicSuffix())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ReportDataBo report = new ReportDataBo();
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
package com.fastbee.controller.device;
|
||||
|
||||
import com.fastbee.common.annotation.Log;
|
||||
import com.fastbee.common.constant.CacheConstants;
|
||||
import com.fastbee.common.constant.HttpStatus;
|
||||
import com.fastbee.common.core.controller.BaseController;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
import com.fastbee.common.core.domain.entity.SysRole;
|
||||
import com.fastbee.common.core.domain.model.LoginUser;
|
||||
import com.fastbee.common.core.page.TableDataInfo;
|
||||
import com.fastbee.common.core.redis.RedisCache;
|
||||
import com.fastbee.common.enums.BusinessType;
|
||||
import com.fastbee.common.exception.ServiceException;
|
||||
import com.fastbee.common.utils.MessageUtils;
|
||||
@ -34,6 +36,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -49,6 +52,8 @@ public class DeviceController extends BaseController {
|
||||
@Autowired
|
||||
private IDeviceService deviceService;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
/**
|
||||
* 查询设备列表
|
||||
@ -453,4 +458,39 @@ public class DeviceController extends BaseController {
|
||||
return rspData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启设备调试日志记录
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('iot:device:list')")
|
||||
@PostMapping("/debug/enable")
|
||||
@ApiOperation("开启设备调试日志记录")
|
||||
public AjaxResult enableDebugLog() {
|
||||
try {
|
||||
Long userId = getUserId();
|
||||
String cacheKey = CacheConstants.DEVICE_DEBUG_USER_KEY + userId;
|
||||
// 设置标志,过期时间30分钟(防止页面异常关闭导致标志未清除)
|
||||
redisCache.setCacheObject(cacheKey, "1", 30, TimeUnit.MINUTES);
|
||||
return AjaxResult.success("已开启调试日志记录");
|
||||
} catch (Exception e) {
|
||||
return AjaxResult.error("开启调试日志记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭设备调试日志记录
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('iot:device:list')")
|
||||
@PostMapping("/debug/disable")
|
||||
@ApiOperation("关闭设备调试日志记录")
|
||||
public AjaxResult disableDebugLog() {
|
||||
try {
|
||||
Long userId = getUserId();
|
||||
String cacheKey = CacheConstants.DEVICE_DEBUG_USER_KEY + userId;
|
||||
redisCache.deleteObject(cacheKey);
|
||||
return AjaxResult.success("已关闭调试日志记录");
|
||||
} catch (Exception e) {
|
||||
return AjaxResult.error("关闭调试日志记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ public class MessageAdapter implements MessageDecoder, MessageEncoder {
|
||||
Long productId = null;
|
||||
String devNum = new String(ByteBufUtil.getBytes(buf));
|
||||
String dump = ByteBufUtil.hexDump(buf);
|
||||
log.info("{}=>上报hex数据:{}", clientId, dump);
|
||||
log.info("{}上报hex数据:{}", clientId, dump);
|
||||
|
||||
//这里兼容一下TCP整包发送的数据(整包==设备编号,数据一起发送)
|
||||
if (clientId == null) {
|
||||
|
||||
@ -127,7 +127,6 @@ public class NettyConfig {
|
||||
case COAP:
|
||||
this.name = name != null ? name : ServerType.COAP.name();
|
||||
this.server = server;
|
||||
;
|
||||
this.server.config = this;
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -5,6 +5,7 @@ import com.fastbee.base.session.Session;
|
||||
import com.fastbee.base.session.SessionManager;
|
||||
import com.fastbee.base.util.AttributeUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@ -49,6 +50,15 @@ public class TCPMessageAdapter extends ChannelInboundHandlerAdapter {
|
||||
ByteBuf buf = (ByteBuf) msg;
|
||||
/*session存储*/
|
||||
Session session = getSession(ctx);
|
||||
// 临时记录设备原始数据日志
|
||||
// 针对特定设备记录原始数据日志
|
||||
String serialNumber = session.getClientId();
|
||||
if ("D3C611176213".equalsIgnoreCase(serialNumber) || "DEE3FBBF0132".equalsIgnoreCase(serialNumber)) {
|
||||
ByteBuf copy = buf.duplicate();
|
||||
byte[] source = new byte[buf.writerIndex()];
|
||||
copy.readBytes(source);
|
||||
log.warn("=>设备[{}]原始数据:{}", serialNumber, ByteBufUtil.hexDump(source));
|
||||
}
|
||||
session.access();
|
||||
/*将设备端消息传递给下一个处理器*/
|
||||
ctx.fireChannelRead(Packet.of(session, buf));
|
||||
|
||||
@ -71,7 +71,8 @@ import java.math.BigDecimal;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.fastbee.common.utils.SecurityUtils.getLoginUser;
|
||||
@ -498,7 +499,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
@DSTransactional
|
||||
public List<ThingsModelSimpleItem> reportDeviceThingsModelValue(ThingsModelValuesInput input, int type, boolean isShadow) {
|
||||
String key = RedisKeyBuilder.buildTSLVCacheKey(input.getProductId(), input.getDeviceNumber());
|
||||
Map<String, String> maps = new HashMap<String, String>();
|
||||
Map<String, String> maps = new HashMap<>();
|
||||
List<ThingsModelSimpleItem> list = new ArrayList<>();
|
||||
|
||||
//属性存储集合
|
||||
@ -507,6 +508,8 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
List<FunctionLog> functionLogList = new ArrayList<>();
|
||||
//事件存储集合
|
||||
List<EventLog> eventLogList = new ArrayList<>();
|
||||
// 获取当前时间
|
||||
Date nowDate = DateUtils.getNowDate();
|
||||
for (ThingsModelSimpleItem item : input.getThingsModelValueRemarkItem()) {
|
||||
String identity = item.getId();
|
||||
String serialNumber = input.getDeviceNumber();
|
||||
@ -538,13 +541,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
// 解析公式中的其他物模型标识符(格式:{identifier} 或直接使用标识符)
|
||||
String formula = thingModels.getFormula();
|
||||
// 匹配 {identifier} 格式的占位符
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(formula);
|
||||
Set<String> referencedIdentifiers = new HashSet<>();
|
||||
while (matcher.find()) {
|
||||
String identifier = matcher.group(1);
|
||||
referencedIdentifiers.add(identifier);
|
||||
}
|
||||
Set<String> referencedIdentifiers = parseFormulaPlaceholders(formula);
|
||||
|
||||
// 获取引用的其他物模型的值
|
||||
for (String refIdentifier : referencedIdentifiers) {
|
||||
@ -561,8 +558,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
} else {
|
||||
// 如果引用的物模型值不存在,使用0作为默认值
|
||||
params.put("{" + refIdentifier + "}", "0");
|
||||
log.warn("=>计算公式引用的物模型值不存在[{}],标识符:[{}],使用默认值0",
|
||||
input.getProductId(), refIdentifier);
|
||||
log.warn("=>计算公式引用的物模型值不存在[{}],标识符:[{}],使用默认值0", input.getProductId(), refIdentifier);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("=>获取计算公式引用的物模型值失败[{}],标识符:[{}],错误:{}",
|
||||
@ -641,7 +637,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
deviceLog.setLogValue(value);
|
||||
deviceLog.setRemark(item.getRemark());
|
||||
deviceLog.setIdentity(id);
|
||||
deviceLog.setCreateTime(DateUtils.getNowDate());
|
||||
deviceLog.setCreateTime(nowDate);
|
||||
deviceLog.setUserId(device.getTenantId());
|
||||
deviceLog.setUserName(device.getTenantName());
|
||||
deviceLog.setTenantId(device.getTenantId());
|
||||
@ -654,7 +650,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
case SERVICE:
|
||||
if (1 == thingModels.getIsHistory()) {
|
||||
FunctionLog function = new FunctionLog();
|
||||
function.setCreateTime(DateUtils.getNowDate());
|
||||
function.setCreateTime(nowDate);
|
||||
function.setFunValue(value);
|
||||
function.setSerialNumber(input.getDeviceNumber());
|
||||
function.setIdentify(id);
|
||||
@ -677,7 +673,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
event.setIsMonitor(0);
|
||||
event.setUserId(device.getTenantId());
|
||||
event.setUserName(device.getTenantName());
|
||||
event.setCreateTime(DateUtils.getNowDate());
|
||||
event.setCreateTime(nowDate);
|
||||
// 1=影子模式,2=在线模式,3=其他
|
||||
event.setMode(2);
|
||||
eventLogList.add(event);
|
||||
@ -687,11 +683,12 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
}
|
||||
// 缓存最新一条数据到redis
|
||||
redisCache.hashPutAll(key, maps);
|
||||
//存储历史数据
|
||||
// 存储历史数据——批量
|
||||
if (!CollectionUtils.isEmpty(deviceLogList) && !isShadow) {
|
||||
for (DeviceLog deviceLog : deviceLogList) {
|
||||
logService.saveDeviceLog(deviceLog);
|
||||
}
|
||||
// for (DeviceLog deviceLog : deviceLogList) {
|
||||
// logService.saveDeviceLog(deviceLog);
|
||||
// }
|
||||
logService.saveDeviceLogBatch(deviceLogList);
|
||||
}
|
||||
//指令存储,影子模式不存储
|
||||
if (!CollectionUtils.isEmpty(functionLogList) && !isShadow) {
|
||||
@ -720,7 +717,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
Map<String, String> calculatedMaps = new HashMap<>();
|
||||
List<DeviceLog> calculatedDeviceLogList = new ArrayList<>();
|
||||
|
||||
calculateDependentThingsModels(input, updatedIdentifiers, key, calculatedMaps, calculatedDeviceLogList, type);
|
||||
calculateDependentThingsModels(input, updatedIdentifiers, nowDate, calculatedMaps, calculatedDeviceLogList, type);
|
||||
|
||||
// 将计算后的值更新到缓存
|
||||
if (!calculatedMaps.isEmpty()) {
|
||||
@ -731,9 +728,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
|
||||
// 保存计算型物模型的历史数据
|
||||
if (!CollectionUtils.isEmpty(calculatedDeviceLogList)) {
|
||||
for (DeviceLog deviceLog : calculatedDeviceLogList) {
|
||||
logService.saveDeviceLog(deviceLog);
|
||||
}
|
||||
logService.saveDeviceLogBatch(calculatedDeviceLogList);
|
||||
log.debug("=>自动计算依赖物模型,保存历史数据成功,设备编号:[{}],历史数据数量:[{}]",
|
||||
input.getDeviceNumber(), calculatedDeviceLogList.size());
|
||||
}
|
||||
@ -743,6 +738,26 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析公式中的占位符标识符
|
||||
*
|
||||
* @param formula 计算公式
|
||||
* @return 占位符标识符集合
|
||||
*/
|
||||
private Set<String> parseFormulaPlaceholders(String formula) {
|
||||
Set<String> referencedIdentifiers = new HashSet<>();
|
||||
if (StringUtils.isEmpty(formula)) {
|
||||
return referencedIdentifiers;
|
||||
}
|
||||
Pattern pattern = Pattern.compile("\\{([^}]+)}");
|
||||
Matcher matcher = pattern.matcher(formula);
|
||||
while (matcher.find()) {
|
||||
String identifier = matcher.group(1);
|
||||
referencedIdentifiers.add(identifier);
|
||||
}
|
||||
return referencedIdentifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设备列表
|
||||
*
|
||||
@ -1947,14 +1962,14 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
*
|
||||
* @param input 设备上报数据输入
|
||||
* @param updatedIdentifiers 已更新的物模型标识符集合(设备上报的)
|
||||
* @param key Redis缓存key
|
||||
* @param nowDate 数据记录时间
|
||||
* @param maps 物模型值映射(用于批量更新缓存)
|
||||
* @param deviceLogList 设备日志列表(用于存储历史数据)
|
||||
* @param type 物模型类型(1=属性,2=功能,3=事件)
|
||||
*/
|
||||
private void calculateDependentThingsModels(ThingsModelValuesInput input,
|
||||
Set<String> updatedIdentifiers,
|
||||
String key,
|
||||
Date nowDate,
|
||||
Map<String, String> maps,
|
||||
List<DeviceLog> deviceLogList,
|
||||
int type) {
|
||||
@ -1991,13 +2006,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
}
|
||||
|
||||
// 检查计算公式是否引用了已更新的物模型
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(formula);
|
||||
Set<String> referencedIdentifiers = new HashSet<>();
|
||||
while (matcher.find()) {
|
||||
String identifier = matcher.group(1);
|
||||
referencedIdentifiers.add(identifier);
|
||||
}
|
||||
Set<String> referencedIdentifiers = parseFormulaPlaceholders(formula);
|
||||
|
||||
// 检查是否至少引用了一个已更新的物模型
|
||||
boolean hasDependency = false;
|
||||
@ -2074,7 +2083,7 @@ public class DeviceServiceImpl implements IDeviceService {
|
||||
deviceLog.setLogValue(calculatedValue);
|
||||
deviceLog.setRemark("自动计算"); // 标记为自动计算
|
||||
deviceLog.setIdentity(dependentIdentifier);
|
||||
deviceLog.setCreateTime(DateUtils.getNowDate());
|
||||
deviceLog.setCreateTime(nowDate);
|
||||
deviceLog.setUserId(device.getTenantId());
|
||||
deviceLog.setUserName(device.getTenantName());
|
||||
deviceLog.setTenantId(device.getTenantId());
|
||||
|
||||
@ -627,8 +627,8 @@ public class ThingsModelServiceImpl implements IThingsModelService {
|
||||
public IdentifierVO revertObjectOrArrayIdentifier(String identifier) {
|
||||
IdentifierVO identifierVO = new IdentifierVO();
|
||||
if (identifier.startsWith("array_")) {
|
||||
int i1 = org.apache.commons.lang3.StringUtils.ordinalIndexOf(identifier, "_", 1);
|
||||
int i2 = org.apache.commons.lang3.StringUtils.ordinalIndexOf(identifier, "_", 2);
|
||||
int i1 = StringUtils.ordinalIndexOf(identifier, "_", 1);
|
||||
int i2 = StringUtils.ordinalIndexOf(identifier, "_", 2);
|
||||
identifierVO.setIdentifier(identifier.substring(i2 + 1));
|
||||
identifierVO.setIndex(Integer.parseInt(identifier.substring(++i1, i2)));
|
||||
} else {
|
||||
|
||||
@ -27,6 +27,10 @@ public interface ILogService {
|
||||
* 保存设备日志
|
||||
**/
|
||||
int saveDeviceLog(DeviceLog deviceLog);
|
||||
/**
|
||||
* 批量保存设备日志
|
||||
**/
|
||||
void saveDeviceLogBatch(List<DeviceLog> deviceLogList);
|
||||
|
||||
/**
|
||||
* 批量保存日志
|
||||
|
||||
@ -14,10 +14,11 @@ import com.fastbee.iot.mapper.DeviceLogMapper;
|
||||
import com.fastbee.iot.model.MonitorModel;
|
||||
import com.fastbee.iot.tdengine.service.model.TdLogDto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@ -33,11 +34,11 @@ import java.util.stream.Collectors;
|
||||
@ConditionalOnProperty(name = "spring.datasource.dynamic.datasource.taos.enabled", havingValue = "false")
|
||||
@Service("MySql")
|
||||
public class MySqlLogServiceImpl implements ILogService {
|
||||
@Autowired
|
||||
@Resource
|
||||
private DeviceLogMapper deviceLogMapper;
|
||||
@Autowired
|
||||
@Resource
|
||||
private EventLogMapper eventLogMapper;
|
||||
@Autowired
|
||||
@Resource
|
||||
private FunctionLogMapper functionLogMapper;
|
||||
|
||||
|
||||
@ -55,6 +56,14 @@ public class MySqlLogServiceImpl implements ILogService {
|
||||
return deviceLogMapper.insertDeviceLog(deviceLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveDeviceLogBatch(List<DeviceLog> deviceLogList) {
|
||||
if (CollectionUtils.isEmpty(deviceLogList)) {
|
||||
return;
|
||||
}
|
||||
deviceLogMapper.saveBatch(deviceLogList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int saveBatch(TdLogDto dto) {
|
||||
return deviceLogMapper.saveBatch(dto.getList());
|
||||
|
||||
@ -23,6 +23,7 @@ import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* 类名: TdengineLogServiceImpl
|
||||
@ -62,6 +63,52 @@ public class TdengineLogServiceImpl implements ILogService {
|
||||
return tDDeviceLogMapper.save(dbName, deviceLog);
|
||||
}
|
||||
|
||||
/***
|
||||
* 批量保存设备日志
|
||||
* @param deviceLogList 设备日志列表
|
||||
*/
|
||||
@Override
|
||||
public void saveDeviceLogBatch(List<DeviceLog> deviceLogList) {
|
||||
if (CollectionUtils.isEmpty(deviceLogList)) {
|
||||
log.debug("=>批量保存设备日志到TDengine:日志列表为空,跳过保存");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 按设备编号分组
|
||||
Map<String, List<DeviceLog>> groupedBySerialNumber = deviceLogList.stream()
|
||||
.filter(log -> log.getSerialNumber() != null && !log.getSerialNumber().isEmpty())
|
||||
.collect(Collectors.groupingBy(DeviceLog::getSerialNumber));
|
||||
|
||||
if (groupedBySerialNumber.isEmpty()) {
|
||||
log.warn("=>批量保存设备日志到TDengine:没有有效的设备编号,跳过保存");
|
||||
return;
|
||||
}
|
||||
|
||||
// 为每个设备批量保存日志
|
||||
int totalSaved = 0;
|
||||
for (Map.Entry<String, List<DeviceLog>> entry : groupedBySerialNumber.entrySet()) {
|
||||
String serialNumber = entry.getKey();
|
||||
List<DeviceLog> logs = entry.getValue();
|
||||
|
||||
try {
|
||||
TdLogDto dto = new TdLogDto(serialNumber, logs);
|
||||
int saved = tDDeviceLogMapper.saveBatch(dbName, dto);
|
||||
totalSaved += saved;
|
||||
log.debug("=>批量保存设备日志到TDengine成功,设备编号:[{}],保存{}条记录", serialNumber, saved);
|
||||
} catch (Exception e) {
|
||||
log.error("=>批量保存设备日志到TDengine失败,设备编号:[{}],共{}条记录,错误:{}",
|
||||
serialNumber, logs.size(), e.getMessage(), e);
|
||||
// 继续处理其他设备的日志,不中断整个流程
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("=>批量保存设备日志到TDengine时发生异常,共{}条记录,错误:{}",
|
||||
deviceLogList.size(), e.getMessage(), e);
|
||||
throw new RuntimeException("批量保存设备日志到TDengine失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存日志
|
||||
*/
|
||||
|
||||
@ -32,9 +32,9 @@ public class SysNotice extends BaseEntity {
|
||||
private String noticeTitle;
|
||||
|
||||
/**
|
||||
* 公告类型(1通知 2公告)
|
||||
* 公告类型(1通知 2公告 3文档)
|
||||
*/
|
||||
@ApiModelProperty("公告类型(1通知 2公告)")
|
||||
@ApiModelProperty("公告类型(1通知 2公告 3文档)")
|
||||
private String noticeType;
|
||||
|
||||
/**
|
||||
@ -43,6 +43,12 @@ public class SysNotice extends BaseEntity {
|
||||
@ApiModelProperty("公告内容")
|
||||
private String noticeContent;
|
||||
|
||||
/**
|
||||
* 文件路径(文档类型使用)
|
||||
*/
|
||||
@ApiModelProperty("文件路径(文档类型使用)")
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 公告状态(0正常 1关闭)
|
||||
*/
|
||||
@ -84,6 +90,14 @@ public class SysNotice extends BaseEntity {
|
||||
this.noticeContent = noticeContent;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public void setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
@ -99,6 +113,7 @@ public class SysNotice extends BaseEntity {
|
||||
.append("noticeTitle", getNoticeTitle())
|
||||
.append("noticeType", getNoticeType())
|
||||
.append("noticeContent", getNoticeContent())
|
||||
.append("filePath", getFilePath())
|
||||
.append("status", getStatus())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
|
||||
@ -9,6 +9,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="noticeTitle" column="notice_title" />
|
||||
<result property="noticeType" column="notice_type" />
|
||||
<result property="noticeContent" column="notice_content" />
|
||||
<result property="filePath" column="file_path" />
|
||||
<result property="status" column="status" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
@ -18,7 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectNoticeVo">
|
||||
select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, status, create_by, create_time, update_by, update_time, remark
|
||||
select notice_id, notice_title, notice_type, cast(notice_content as char) as notice_content, file_path, status, create_by, create_time, update_by, update_time, remark
|
||||
from sys_notice
|
||||
</sql>
|
||||
|
||||
@ -47,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="noticeTitle != null and noticeTitle != '' ">notice_title, </if>
|
||||
<if test="noticeType != null and noticeType != '' ">notice_type, </if>
|
||||
<if test="noticeContent != null and noticeContent != '' ">notice_content, </if>
|
||||
<if test="filePath != null and filePath != '' ">file_path, </if>
|
||||
<if test="status != null and status != '' ">status, </if>
|
||||
<if test="remark != null and remark != ''">remark,</if>
|
||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||
@ -55,6 +57,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="noticeTitle != null and noticeTitle != ''">#{noticeTitle}, </if>
|
||||
<if test="noticeType != null and noticeType != ''">#{noticeType}, </if>
|
||||
<if test="noticeContent != null and noticeContent != ''">#{noticeContent}, </if>
|
||||
<if test="filePath != null and filePath != ''">#{filePath}, </if>
|
||||
<if test="status != null and status != ''">#{status}, </if>
|
||||
<if test="remark != null and remark != ''">#{remark},</if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||
@ -68,6 +71,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="noticeTitle != null and noticeTitle != ''">notice_title = #{noticeTitle}, </if>
|
||||
<if test="noticeType != null and noticeType != ''">notice_type = #{noticeType}, </if>
|
||||
<if test="noticeContent != null">notice_content = #{noticeContent}, </if>
|
||||
<if test="filePath != null">file_path = #{filePath}, </if>
|
||||
<if test="status != null and status != ''">status = #{status}, </if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
|
||||
update_time = sysdate()
|
||||
|
||||
@ -61,3 +61,19 @@ export function delPreferences(data) {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// 开启设备调试日志记录
|
||||
export function enableDebugLog() {
|
||||
return request({
|
||||
url: '/iot/device/debug/enable',
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭设备调试日志记录
|
||||
export function disableDebugLog() {
|
||||
return request({
|
||||
url: '/iot/device/debug/disable',
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dataCenter.history.384934-3')" prop="identifiers">
|
||||
<el-select style="width: 100%" v-model="devQueryParams.identifiers" :placeholder="$t('dataCenter.history.384934-4')" filterable multiple collapse-tags @change="handleDevIdentifierChange">
|
||||
<el-option label="全部勾选" value="__selectAll__">
|
||||
<span style="font-weight: bold; color: #409EFF;">全部勾选</span>
|
||||
<el-option label="全部勾选/反选" value="__selectAll__">
|
||||
<span style="font-weight: bold; color: #409EFF;">{{ devIsAllSelected ? '全部反选' : '全部勾选' }}</span>
|
||||
</el-option>
|
||||
<el-option v-for="(item, index) in devIdentifierList" :key="index" :label="item.modelName" :value="item.identifier"></el-option>
|
||||
</el-select>
|
||||
@ -80,8 +80,8 @@
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dataCenter.history.384934-3')" prop="identifiers">
|
||||
<el-select style="width: 100%" v-model="sceneQueryParams.identifiers" :placeholder="$t('dataCenter.history.384934-4')" multiple collapse-tags @change="handleSceneIdentifierChange">
|
||||
<el-option label="全部勾选" value="__selectAll__">
|
||||
<span style="font-weight: bold; color: #409EFF;">全部勾选</span>
|
||||
<el-option label="全部勾选/反选" value="__selectAll__">
|
||||
<span style="font-weight: bold; color: #409EFF;">{{ sceneIsAllSelected ? '全部反选' : '全部勾选' }}</span>
|
||||
</el-option>
|
||||
<el-option v-for="(item, index) in sceneIdentifierList" :key="index" :label="item.sourceName" :value="item.identifier"></el-option>
|
||||
</el-select>
|
||||
@ -270,6 +270,20 @@ export default {
|
||||
const end = start + this.scenePageSize;
|
||||
return this.sceneTableComTemp.slice(start, end);
|
||||
},
|
||||
// 判断设备变量是否全部选中
|
||||
devIsAllSelected() {
|
||||
if (this.devIdentifierList.length === 0) return false;
|
||||
const allIdentifiers = this.devIdentifierList.map(item => item.identifier);
|
||||
return allIdentifiers.length > 0 &&
|
||||
allIdentifiers.every(id => this.devQueryParams.identifiers.includes(id));
|
||||
},
|
||||
// 判断场景变量是否全部选中
|
||||
sceneIsAllSelected() {
|
||||
if (this.sceneIdentifierList.length === 0) return false;
|
||||
const allIdentifiers = this.sceneIdentifierList.map(item => item.identifier);
|
||||
return allIdentifiers.length > 0 &&
|
||||
allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 获取设备列表
|
||||
@ -292,12 +306,22 @@ export default {
|
||||
},
|
||||
// 设备变量选择变化
|
||||
handleDevIdentifierChange(val) {
|
||||
// 如果选择了"全部勾选",则选中所有变量
|
||||
// 如果选择了"全部勾选",则切换全选/全不选
|
||||
const index = val.indexOf('__selectAll__');
|
||||
if (index > -1) {
|
||||
const allIdentifiers = this.devIdentifierList.map(item => item.identifier);
|
||||
// 判断当前是否已经全部选中(基于当前值,不是新值)
|
||||
const isAllSelected = allIdentifiers.length > 0 &&
|
||||
allIdentifiers.every(id => this.devQueryParams.identifiers.includes(id));
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.devQueryParams.identifiers = [...allIdentifiers];
|
||||
if (isAllSelected) {
|
||||
// 如果已经全部选中,则清空选择(反选)
|
||||
this.devQueryParams.identifiers = [];
|
||||
} else {
|
||||
// 如果没有全部选中,则选中所有(勾选)
|
||||
this.devQueryParams.identifiers = [...allIdentifiers];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -398,12 +422,22 @@ export default {
|
||||
},
|
||||
// 场景变量选择变化
|
||||
handleSceneIdentifierChange(val) {
|
||||
// 如果选择了"全部勾选",则选中所有变量
|
||||
// 如果选择了"全部勾选",则切换全选/全不选
|
||||
const index = val.indexOf('__selectAll__');
|
||||
if (index > -1) {
|
||||
const allIdentifiers = this.sceneIdentifierList.map(item => item.identifier);
|
||||
// 判断当前是否已经全部选中(基于当前值,不是新值)
|
||||
const isAllSelected = allIdentifiers.length > 0 &&
|
||||
allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id));
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.sceneQueryParams.identifiers = [...allIdentifiers];
|
||||
if (isAllSelected) {
|
||||
// 如果已经全部选中,则清空选择(反选)
|
||||
this.sceneQueryParams.identifiers = [];
|
||||
} else {
|
||||
// 如果没有全部选中,则选中所有(勾选)
|
||||
this.sceneQueryParams.identifiers = [...allIdentifiers];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -177,24 +177,31 @@
|
||||
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="8">
|
||||
<el-card shadow="hover" class="message-card">
|
||||
<!-- 信息栏 -->
|
||||
<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 }" v-for="item in noticeList" :key="item.noticeId">
|
||||
<div style="cursor: pointer; display: table; width: 100%; line-height: 36px" @click="openDetail(item.noticeId)" v-for="item in noticeList" :key="item.noticeId">
|
||||
<div style="display: table-cell; padding-right: 10px">
|
||||
<el-tag size="mini" effect="dark" type="warning" v-if="item.noticeType == 2">公告</el-tag>
|
||||
<el-tag size="mini" effect="dark" v-else>信息</el-tag>
|
||||
{{ item.noticeTitle }}
|
||||
</div>
|
||||
<div style="display: table-cell; width: 90px; font-size: 14px">
|
||||
<i class="el-icon-time"></i>
|
||||
{{ parseTime(item.createTime, '{y}-{m}-{d}') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 信息栏 -->
|
||||
<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
|
||||
style="cursor: pointer; display: table; width: 100%; line-height: 36px"
|
||||
v-for="item in noticeList"
|
||||
:key="item.noticeId"
|
||||
@click="openDetail(item.noticeId)"
|
||||
:class="{ 'document-notice': item.noticeType == 3 }"
|
||||
>
|
||||
<div style="display: table-cell; padding-right: 10px">
|
||||
<el-tag size="mini" effect="dark" type="warning" v-if="item.noticeType == 2">公告</el-tag>
|
||||
<el-tag size="mini" effect="dark" type="danger" v-else-if="item.noticeType == 3">文档</el-tag>
|
||||
<el-tag size="mini" effect="dark" v-else>信息</el-tag>
|
||||
{{ item.noticeTitle }}
|
||||
</div>
|
||||
<div style="display: table-cell; width: 90px; font-size: 14px">
|
||||
<i class="el-icon-time"></i>
|
||||
{{ parseTime(item.createTime, '{y}-{m}-{d}') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
@ -238,7 +245,14 @@
|
||||
<span style="margin-left: 20px">{{ notice.createTime }}</span>
|
||||
</div>
|
||||
<div v-loading="loading" class="content">
|
||||
<div v-html="notice.noticeContent"></div>
|
||||
<!-- 文档类型显示下载链接 -->
|
||||
<div v-if="notice.noticeType == '3' && notice.filePath" style="text-align: center; padding: 40px;">
|
||||
<el-button type="primary" size="medium" @click="downloadFile(notice.filePath)">
|
||||
<i class="el-icon-download"></i> 下载文档
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 其他类型显示内容 -->
|
||||
<div v-else v-html="notice.noticeContent"></div>
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="closeDetail">{{ $t('home.close') }}</el-button>
|
||||
@ -393,7 +407,7 @@ export default {
|
||||
version: '3.8.0',
|
||||
animate: true,
|
||||
duration: 10, // 公告滚动一次的时长(单位:秒)
|
||||
interval: 2000, // 公告间隔时间(单位:毫秒)
|
||||
interval: 5000, // 公告间隔时间(单位:毫秒)
|
||||
// 服务器信息
|
||||
server: {
|
||||
jvm: {
|
||||
@ -1050,6 +1064,16 @@ export default {
|
||||
this.noticeList = response.rows.splice(0, 6);
|
||||
});
|
||||
},
|
||||
// 下载文件
|
||||
downloadFile(filePath) {
|
||||
const url = process.env.VUE_APP_BASE_API + filePath;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filePath.substring(filePath.lastIndexOf('/') + 1);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
// 打开信息详情
|
||||
openDetail(id) {
|
||||
this.open = true;
|
||||
@ -2943,11 +2967,44 @@ $aqi-unhealthy-color: #f44336;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
.notice-bar {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
// 文档类型公告的特殊样式
|
||||
.document-notice {
|
||||
background-color: #ffebee !important; // 浅红色背景
|
||||
border-left: 4px solid #f44336 !important; // 红色左边框
|
||||
margin: 5px 0;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #ffcdd2 !important; // 悬停时加深背景色
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
background-color: #f44336 !important;
|
||||
border-color: #f44336 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 公告栏容器样式调整
|
||||
.notice-bar {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
// 确保每个公告项有适当间距
|
||||
& > div {
|
||||
margin: 4px 0;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.animating {
|
||||
animation: scroll 10s linear infinite;
|
||||
animation: scroll 10s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -161,17 +161,19 @@
|
||||
<pagination style="margin-bottom: 20px" v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" :pageSizes="[12, 24, 36, 60]" @pagination="getList" />
|
||||
</div>
|
||||
<div v-if="showType == 'card'">
|
||||
<el-row :gutter="20" v-loading="loading">
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" v-for="(item, index) in deviceList" :key="index" style="margin-bottom: 20px; text-align: center">
|
||||
<el-card :body-style="{ padding: '20px' }" shadow="always">
|
||||
<el-row :gutter="20" v-loading="loading" style="display: flex; flex-wrap: wrap;">
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" v-for="(item, index) in deviceList" :key="index" style="margin-bottom: 20px; text-align: center; display: flex;">
|
||||
<el-card :body-style="{ padding: '20px', display: 'flex', flexDirection: 'column', height: '100%' }" shadow="always" style="width: 100%; display: flex; flex-direction: column;">
|
||||
<el-row type="flex" :gutter="10" justify="space-between">
|
||||
<el-col :span="20" style="text-align: left">
|
||||
<el-link type="" :underline="false" @click="handleDeviceDetail(item)" style="font-weight: bold; font-size: 16px; line-height: 32px">
|
||||
<el-link type="" :underline="false" @click="handleDeviceDetail(item)" style="font-weight: bold; font-size: 16px; line-height: 32px; display: flex; align-items: center; width: 100%;">
|
||||
<el-tooltip class="item" effect="dark" :content="$t('device.index.105953-38')" placement="top-start">
|
||||
<svg-icon icon-class="share" style="font-size: 20px" v-if="item.isOwner != 1" />
|
||||
<svg-icon icon-class="share" style="font-size: 20px; flex-shrink: 0;" v-if="item.isOwner != 1" />
|
||||
</el-tooltip>
|
||||
<svg-icon icon-class="device" style="flex-shrink: 0;" v-if="item.isOwner == 1" />
|
||||
<el-tooltip :content="item.deviceName" placement="top" :disabled="!item.deviceName || item.deviceName.length <= 20">
|
||||
<span style="margin: 0 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;">{{ item.deviceName }}</span>
|
||||
</el-tooltip>
|
||||
<svg-icon icon-class="device" v-if="item.isOwner == 1" />
|
||||
<span style="margin: 0 5px">{{ item.deviceName }}</span>
|
||||
<!-- <el-tag size="mini" type="info">Ver {{item.firmwareVersion}}</el-tag>-->
|
||||
<!-- <el-text v-if="item.protocolCode" type="info" size="mini" style="font-size: 14px; color: #ccc">{{ item.protocolCode }}</el-text> -->
|
||||
</el-link>
|
||||
@ -189,38 +191,38 @@
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="17">
|
||||
<div style="text-align: left; line-height: 40px; white-space: nowrap">
|
||||
<dict-tag :options="dict.type.iot_device_status" :value="item.status" size="mini" style="display: inline-block" />
|
||||
<span style="display: inline-block; margin: 0 10px">
|
||||
<!-- <el-tag type="success" size="small" v-if="item.isShadow == 1">影子</el-tag>
|
||||
<el-tag type="info" size="small" v-else>影子</el-tag> -->
|
||||
<el-tag type="primary" size="mini" v-if="item.protocolCode">{{ item.protocolCode }}</el-tag>
|
||||
<el-row :gutter="10" style="flex: 1; min-height: 0;">
|
||||
<el-col :span="17" style="display: flex; flex-direction: column;">
|
||||
<div style="text-align: left; line-height: 28px; min-height: 28px; word-wrap: break-word; overflow-wrap: break-word; flex-shrink: 0;">
|
||||
<dict-tag :options="dict.type.iot_device_status" :value="item.status" size="mini" style="display: inline-block; margin-bottom: 4px; vertical-align: middle;" />
|
||||
<span style="display: inline-block; margin: 0 5px 4px 5px; vertical-align: middle;">
|
||||
<el-tag type="primary" size="mini" v-if="item.protocolCode" style="margin-bottom: 4px;">{{ item.protocolCode }}</el-tag>
|
||||
</span>
|
||||
<el-tag type="primary" size="mini" v-if="item.transport">{{ item.transport }}</el-tag>
|
||||
<el-tag type="danger" size="mini" style="display: inline-block; margin: 0 0 0 10px" v-if="item.alertCount">
|
||||
<el-tag type="primary" size="mini" v-if="item.transport" style="margin-bottom: 4px;">{{ item.transport }}</el-tag>
|
||||
<el-tag type="danger" size="mini" style="display: inline-block; margin: 0 0 4px 5px;" v-if="item.alertCount">
|
||||
{{ $t('device.index.105953-53') }}{{ item.alertCount.unprocessedCount }}
|
||||
</el-tag>
|
||||
<!-- <dict-tag :options="dict.type.iot_location_way" :value="item.locationWay" size="small" style="display:inline-block;" /> -->
|
||||
<!-- <dict-tag :options="dict.type.iot_transport_type" :value="item.transport" size="small" style="display: inline-block" /> -->
|
||||
</div>
|
||||
<el-descriptions :column="1" size="mini" style="white-space: nowrap">
|
||||
<el-descriptions :column="1" size="mini" style="margin-top: 8px; flex: 1;">
|
||||
<el-descriptions-item :label="$t('device.index.105953-20')">
|
||||
{{ item.serialNumber }}
|
||||
<el-tooltip :content="item.serialNumber" placement="top" :disabled="!item.serialNumber || item.serialNumber.length <= 15">
|
||||
<span style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ item.serialNumber }}</span>
|
||||
</el-tooltip>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('device.index.105953-39')">
|
||||
{{ item.productName }}
|
||||
<el-tooltip :content="item.productName" placement="top" :disabled="!item.productName || item.productName.length <= 20">
|
||||
<span style="display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ item.productName }}</span>
|
||||
</el-tooltip>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('device.index.105953-32')">
|
||||
{{ parseTime(item.activeTime, '{y}-{m}-{d}') }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-col>
|
||||
<el-col :span="7">
|
||||
<el-col :span="7" style="flex-shrink: 0;">
|
||||
<div style="margin-top: 10px">
|
||||
<el-image
|
||||
style="width: 100%; height: 100px; border-radius: 10px"
|
||||
style="width: 100%; height: 100px; border-radius: 10px; object-fit: cover;"
|
||||
lazy
|
||||
:preview-src-list="[baseUrl + item.imgUrl]"
|
||||
:src="baseUrl + item.imgUrl"
|
||||
@ -228,21 +230,21 @@
|
||||
v-if="item.imgUrl != null && item.imgUrl != ''"
|
||||
></el-image>
|
||||
<el-image
|
||||
style="width: 100%; height: 100px; border-radius: 10px"
|
||||
style="width: 100%; height: 100px; border-radius: 10px; object-fit: cover;"
|
||||
:preview-src-list="[require('@/assets/images/gateway.png')]"
|
||||
:src="require('@/assets/images/gateway.png')"
|
||||
fit="cover"
|
||||
v-else-if="item.deviceType == 2"
|
||||
></el-image>
|
||||
<el-image
|
||||
style="width: 100%; height: 100px; border-radius: 10px"
|
||||
style="width: 100%; height: 100px; border-radius: 10px; object-fit: cover;"
|
||||
:preview-src-list="[require('@/assets/images/video.png')]"
|
||||
:src="require('@/assets/images/video.png')"
|
||||
fit="cover"
|
||||
v-else-if="item.deviceType == 3"
|
||||
></el-image>
|
||||
<el-image
|
||||
style="width: 100%; height: 100px; border-radius: 10px"
|
||||
style="width: 100%; height: 100px; border-radius: 10px; object-fit: cover;"
|
||||
:preview-src-list="[require('@/assets/images/product.png')]"
|
||||
:src="require('@/assets/images/product.png')"
|
||||
fit="cover"
|
||||
@ -251,14 +253,14 @@
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-button-group style="margin-top: 15px">
|
||||
<el-button type="danger" size="mini" style="padding: 5px 10px" icon="el-icon-delete" @click="handleDelete(item)" v-hasPermi="['iot:device:remove']">
|
||||
<el-button-group style="margin-top: 15px; width: 100%; display: flex; justify-content: center;">
|
||||
<el-button type="danger" size="mini" style="padding: 5px 10px; flex: 1;" icon="el-icon-delete" @click="handleDelete(item)" v-hasPermi="['iot:device:remove']">
|
||||
{{ $t('device.index.105953-35') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" style="padding: 5px 15px" icon="el-icon-view" @click="handleEditDevice(item, 'basic')" v-hasPermi="['iot:device:query']">
|
||||
<el-button type="primary" size="mini" style="padding: 5px 15px; flex: 1;" icon="el-icon-view" @click="handleEditDevice(item, 'basic')" v-hasPermi="['iot:device:query']">
|
||||
{{ $t('device.index.105953-36') }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" style="padding: 5px 15px" icon="el-icon-odometer" @click="handleRunDevice(item)" v-hasPermi="['iot:device:query']">
|
||||
<el-button type="success" size="mini" style="padding: 5px 15px; flex: 1;" icon="el-icon-odometer" @click="handleRunDevice(item)" v-hasPermi="['iot:device:query']">
|
||||
{{ $t('device.index.105953-40') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
|
||||
@ -375,7 +375,7 @@
|
||||
|
||||
<script>
|
||||
import { hex2int, int2hex, copyText, formatDate, deepClone } from '@/utils/common';
|
||||
import { encode, decode, messagePost, addPreferences, editPreferences, delPreferences, preferencesList } from '@/api/iot/mqttTest';
|
||||
import { encode, decode, messagePost, addPreferences, editPreferences, delPreferences, preferencesList, enableDebugLog, disableDebugLog } from '@/api/iot/mqttTest';
|
||||
export default {
|
||||
props: {
|
||||
device: {
|
||||
@ -910,6 +910,22 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
/**开启调试日志记录 */
|
||||
async enableDebugLogging() {
|
||||
try {
|
||||
await enableDebugLog();
|
||||
} catch (err) {
|
||||
console.warn('开启调试日志记录失败:', err);
|
||||
}
|
||||
},
|
||||
/**关闭调试日志记录 */
|
||||
async disableDebugLogging() {
|
||||
try {
|
||||
await disableDebugLog();
|
||||
} catch (err) {
|
||||
console.warn('关闭调试日志记录失败:', err);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.resetCreateForm();
|
||||
@ -917,8 +933,13 @@ export default {
|
||||
this.$busEvent.$off('updateData', this.handleUpdateData);
|
||||
// 绑定新的监听器
|
||||
this.$busEvent.$on('updateData', this.handleUpdateData);
|
||||
|
||||
// 开启调试日志记录
|
||||
this.enableDebugLogging();
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 关闭调试日志记录
|
||||
this.disableDebugLogging();
|
||||
// 移除所有事件监听器
|
||||
this.$busEvent.$off('updateData', this.handleUpdateData);
|
||||
this.$busEvent.$off('updateMqttMessage');
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('system.notice.670989-5')" prop="noticeType">
|
||||
<el-select v-model="form.noticeType" :placeholder="$t('system.notice.670989-6')" style="width: 100%">
|
||||
<el-select v-model="form.noticeType" :placeholder="$t('system.notice.670989-6')" style="width: 100%" @change="handleNoticeTypeChange">
|
||||
<el-option v-for="dict in dict.type.sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@ -90,7 +90,20 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<!-- 文档类型时显示文件上传 -->
|
||||
<el-col :span="24" v-if="isDocumentType">
|
||||
<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()"
|
||||
></file-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 非文档类型时显示内容编辑器 -->
|
||||
<el-col :span="24" v-if="!isDocumentType">
|
||||
<el-form-item :label="$t('system.notice.670989-7')">
|
||||
<editor v-model="form.noticeContent" :min-height="192" />
|
||||
</el-form-item>
|
||||
@ -108,9 +121,13 @@
|
||||
|
||||
<script>
|
||||
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from '@/api/system/notice';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
|
||||
export default {
|
||||
name: 'Notice',
|
||||
components: {
|
||||
FileUpload
|
||||
},
|
||||
dicts: ['sys_notice_status', 'sys_notice_type'],
|
||||
data() {
|
||||
return {
|
||||
@ -146,9 +163,31 @@ export default {
|
||||
rules: {
|
||||
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('文档类型必须上传文件'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
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();
|
||||
},
|
||||
@ -174,10 +213,27 @@ export default {
|
||||
noticeTitle: undefined,
|
||||
noticeType: undefined,
|
||||
noticeContent: undefined,
|
||||
filePath: undefined,
|
||||
status: '0',
|
||||
};
|
||||
this.resetForm('form');
|
||||
},
|
||||
// 处理公告类型变化
|
||||
handleNoticeTypeChange(value) {
|
||||
// 切换类型时清空不相关的字段
|
||||
const noticeType = String(value || '');
|
||||
if (noticeType === '3') {
|
||||
// 文档类型,清空内容
|
||||
this.form.noticeContent = undefined;
|
||||
// 触发文件路径验证
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form && this.$refs.form.validateField('filePath');
|
||||
});
|
||||
} else {
|
||||
// 非文档类型,清空文件路径
|
||||
this.form.filePath = undefined;
|
||||
}
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
@ -244,6 +300,12 @@ 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