20260101:

1、设备调试页面钩子函数,写入redis缓存标识,可通过切面推送错误信息到前端页面,离开页面取消标识;
2、数据记录页面一笔数据统一创建日期;优化设备日志保存从for单个改为批量
3、信息栏添加pdf文档下载
This commit is contained in:
George 2026-01-01 22:31:56 +08:00
parent 8a87041e71
commit 44df912fc2
23 changed files with 538 additions and 179 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,12 +285,10 @@ 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;
}
}
}
ReportDataBo report = new ReportDataBo();
report.setSerialNumber(message.getSerialNumber());
report.setProductId(message.getProductId());

View File

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

View File

@ -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) {

View File

@ -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:

View File

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

View File

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

View File

@ -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 {

View File

@ -27,6 +27,10 @@ public interface ILogService {
* 保存设备日志
**/
int saveDeviceLog(DeviceLog deviceLog);
/**
* 批量保存设备日志
**/
void saveDeviceLogBatch(List<DeviceLog> deviceLogList);
/**
* 批量保存日志

View File

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

View File

@ -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);
}
}
/**
* 批量保存日志
*/

View File

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

View File

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

View File

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

View File

@ -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(() => {
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(() => {
if (isAllSelected) {
// 如果已经全部选中,则清空选择(反选)
this.sceneQueryParams.identifiers = [];
} else {
// 如果没有全部选中,则选中所有(勾选)
this.sceneQueryParams.identifiers = [...allIdentifiers];
}
});
}
},

View File

@ -182,10 +182,17 @@
<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 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>
@ -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,10 +2967,43 @@ $aqi-unhealthy-color: #f44336;
line-height: 20px;
}
}
// 文档类型公告的特殊样式
.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;
}

View File

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

View File

@ -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');

View File

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