fix(app): 修复 AppController 安全与查询问题
- 加强异常处理、类型安全和图片上传校验 - 优化设备相关查询,避免重复访问数据源 - 补充并记录并发测试与审查修复实施计划
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,17 @@
|
||||
package org.dromara.app.domain.vo;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import org.dromara.app.domain.AppSchedule;
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.dromara.app.domain.AppSchedule;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
@@ -47,7 +49,7 @@ public class AppScheduleVo implements Serializable {
|
||||
/**
|
||||
* 设备ids
|
||||
*/
|
||||
private List deviceNos;
|
||||
private List<AppDeviceVo> deviceNos;
|
||||
/**
|
||||
* 状态 0-关闭 1 开启
|
||||
*/
|
||||
@@ -55,7 +57,7 @@ public class AppScheduleVo implements Serializable {
|
||||
private String status;
|
||||
|
||||
@NotEmpty(message = "至少需要配置一天")
|
||||
private List<AppScheduleDetailVo> details;
|
||||
private List<Map<String, Object>> details;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 设备数据上报处理器 — 匹配 /{deviceNo}/publish/power
|
||||
* 设备电量及在线心跳处理器,匹配 /{deviceNo}/publish/power。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -23,6 +23,7 @@ public class DeviceDataHandler implements MqttTopicHandler {
|
||||
private static final Pattern PATTERN = Pattern.compile("^/([^/]+)/publish/power$");
|
||||
private final IAppDeviceService appDeviceService;
|
||||
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||
private final MqttDeviceStatusService deviceStatusService;
|
||||
|
||||
@Override
|
||||
public Pattern topicPattern() {
|
||||
@@ -44,13 +45,15 @@ public class DeviceDataHandler implements MqttTopicHandler {
|
||||
log.warn("[MQTT] 设备电量上报缺少电量字段 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||
return;
|
||||
}
|
||||
// 更新设备电量 + 同步在线状态到数据库
|
||||
// 有效电量上报同时作为设备在线心跳。
|
||||
AppDeviceBo appDeviceBo = new AppDeviceBo();
|
||||
appDeviceBo.setDeviceNo(deviceNo);
|
||||
appDeviceBo.setPowerLevel(dto.get("powerLevel").toString());
|
||||
appDeviceBo.setPowerLevelUpdatatime(new Date());
|
||||
appDeviceService.updateByBo(appDeviceBo);
|
||||
log.info("[MQTT] 设备电量更新 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||
deviceStatusService.markOnline(deviceNo);
|
||||
log.info("[MQTT] 设备电量更新并刷新在线心跳 时间={} 设备编号={} 消息体={}",
|
||||
HandlerLogTime.now(), deviceNo, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("[MQTT] 设备电量更新失败 时间={} 设备标识={} 设备编号={} 消息体={}",
|
||||
HandlerLogTime.now(), deviceIdentity, deviceNo, payload, e);
|
||||
|
||||
@@ -33,6 +33,7 @@ DeviceRegisterHandler implements MqttTopicHandler {
|
||||
private final AppDeviceMapper appDeviceMapper;
|
||||
private final ObjectProvider<IDeviceCommandPublisher> commandPublisherProvider;
|
||||
private final DeviceIdentityResolver deviceIdentityResolver;
|
||||
private final MqttDeviceStatusService deviceStatusService;
|
||||
|
||||
@Override
|
||||
public Pattern topicPattern() {
|
||||
@@ -60,8 +61,9 @@ DeviceRegisterHandler implements MqttTopicHandler {
|
||||
}
|
||||
AppDeviceBo device = buildRegisterDevice(deviceNo, normalizedDeviceMac, dto);
|
||||
appDeviceService.registerByMqtt(device);
|
||||
deviceStatusService.markOnline(deviceNo);
|
||||
sendDeviceNoToDevice(deviceNo, normalizedDeviceMac);
|
||||
log.info("[MQTT] 设备注册 时间={} MAC={} 设备编号={} 消息体={}",
|
||||
log.info("[MQTT] 设备注册并上线 时间={} MAC={} 设备编号={} 消息体={}",
|
||||
HandlerLogTime.now(), normalizedDeviceMac, deviceNo, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("[MQTT] 设备注册失败 时间={} 设备标识={} 消息体={}", HandlerLogTime.now(), deviceIdentity, payload, e);
|
||||
|
||||
@@ -13,10 +13,10 @@ import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 设备在线/离线状态处理器,匹配 /{deviceIdentity}/publish/status。
|
||||
* 设备遗嘱离线状态处理器,匹配 /{deviceIdentity}/publish/status。
|
||||
* <p>
|
||||
* deviceIdentity 支持设备编号;设备遗嘱消息允许使用 MAC 地址,处理前统一解析为设备编号。
|
||||
* 设备通过 status=online 标记上线,通过 LWT status=offline 标记异常离线。
|
||||
* 在线状态由电量上报刷新;本处理器仅通过 LWT status=offline 标记异常离线。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -42,17 +42,18 @@ public class DeviceStatusHandler implements MqttTopicHandler {
|
||||
|
||||
@Override
|
||||
public void handle(String deviceIdentity, String payload, boolean retained) {
|
||||
String deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
|
||||
Map<String, Object> body = parsePayload(payload);
|
||||
String deviceNo = resolveDeviceNo(deviceIdentity, body);
|
||||
if (deviceNo == null) {
|
||||
log.warn("[MQTT] 设备状态上报未找到设备 时间={} 设备标识={} 消息体={}",
|
||||
HandlerLogTime.now(), deviceIdentity, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
String status = parseStatus(payload);
|
||||
String status = parseStatus(payload, body);
|
||||
if ("online".equals(status) || "1".equals(status)) {
|
||||
deviceStatusService.markOnline(deviceNo);
|
||||
log.info("[MQTT] 设备状态在线 时间={} 设备编号={} 消息体={}", HandlerLogTime.now(), deviceNo, payload);
|
||||
log.debug("[MQTT] 忽略设备主动在线状态,在线状态由电量心跳维护 时间={} 设备编号={} 消息体={}",
|
||||
HandlerLogTime.now(), deviceNo, payload);
|
||||
return;
|
||||
}
|
||||
if ("offline".equals(status) || "0".equals(status)) {
|
||||
@@ -69,22 +70,52 @@ public class DeviceStatusHandler implements MqttTopicHandler {
|
||||
HandlerLogTime.now(), deviceNo, payload);
|
||||
}
|
||||
|
||||
private String parseStatus(String payload) {
|
||||
private String resolveDeviceNo(String deviceIdentity, Map<String, Object> body) {
|
||||
String deviceNo = deviceIdentityResolver.resolveDeviceNo(deviceIdentity);
|
||||
if (deviceNo != null || body == null) {
|
||||
return deviceNo;
|
||||
}
|
||||
Object deviceMac = body.get("deviceMac");
|
||||
if (deviceMac == null || StringUtils.isBlank(String.valueOf(deviceMac))) {
|
||||
return null;
|
||||
}
|
||||
return deviceIdentityResolver.resolveDeviceNo(String.valueOf(deviceMac));
|
||||
}
|
||||
|
||||
private Map<String, Object> parsePayload(String payload) {
|
||||
if (StringUtils.isBlank(payload)) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = payload.trim();
|
||||
if (!trimmed.startsWith("{")) {
|
||||
return trimmed.toLowerCase(Locale.ROOT);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> body = objectMapper.readValue(trimmed, new TypeReference<Map<String, Object>>() {
|
||||
return objectMapper.readValue(trimmed, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
Object status = body.get("status");
|
||||
return status == null ? null : String.valueOf(status).trim().toLowerCase(Locale.ROOT);
|
||||
} catch (Exception e) {
|
||||
log.warn("[MQTT] 设备状态上报 JSON 格式错误 时间={} 消息体={}", HandlerLogTime.now(), payload, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String parseStatus(String payload, Map<String, Object> body) {
|
||||
if (body == null) {
|
||||
if (StringUtils.isBlank(payload)) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = payload.trim();
|
||||
return trimmed.startsWith("{") ? null : trimmed.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
Object status = body.get("status");
|
||||
if (status != null) {
|
||||
return String.valueOf(status).trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
Object offline = body.get("offline");
|
||||
if (offline != null && "true".equalsIgnoreCase(String.valueOf(offline).trim())) {
|
||||
return "offline";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class MqttDeviceStatusService {
|
||||
@Value("${mqtt.command-ack.device-status-cache-prefix}")
|
||||
private String deviceStatusCachePrefix;
|
||||
|
||||
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds:900}")
|
||||
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds:600}")
|
||||
private long deviceStatusCacheTtlSeconds;
|
||||
|
||||
public void markOnline(String deviceNo) {
|
||||
|
||||
@@ -29,6 +29,14 @@ public interface IAppDeviceService {
|
||||
*/
|
||||
AppDeviceVo queryById(String deviceNo);
|
||||
|
||||
/**
|
||||
* 按设备编号批量查询设备。
|
||||
*
|
||||
* @param deviceNos 设备编号集合
|
||||
* @return 设备列表
|
||||
*/
|
||||
List<AppDeviceVo> queryByDeviceNos(Collection<String> deviceNos);
|
||||
|
||||
/**
|
||||
* 分页查询设备信息
|
||||
列表
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface IAppSchedulingDeviceService {
|
||||
|
||||
@@ -25,6 +26,14 @@ public interface IAppSchedulingDeviceService {
|
||||
|
||||
List<AppSchedulingDeviceVo> findByDeviceNo(String deviceNo);
|
||||
|
||||
/**
|
||||
* 批量查询已绑定排程的设备编号。
|
||||
*
|
||||
* @param deviceNos 待检查的设备编号
|
||||
* @return 已绑定排程的设备编号
|
||||
*/
|
||||
Set<String> findBoundDeviceNos(Collection<String> deviceNos);
|
||||
|
||||
Boolean deleteWithValidByScheduleIdAndDeviceNo(@NotEmpty(message = "主键不能为空") Long scheduleId, String deviceNo);
|
||||
|
||||
AppSchedulingDeviceVo queryByScheduleIdAndDeviceNo(Long scheduleId, String deviceNo);
|
||||
|
||||
@@ -73,6 +73,14 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
return baseMapper.selectVoById(deviceNo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AppDeviceVo> queryByDeviceNos(Collection<String> deviceNos) {
|
||||
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return baseMapper.selectVoByIds(deviceNos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<AppDeviceVo> queryPageList(AppDeviceBo bo, PageQuery pageQuery) {
|
||||
Page<AppDeviceVo> page = pageQuery.build();
|
||||
@@ -307,7 +315,7 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
|
||||
return params;
|
||||
}
|
||||
params.put("bindDevice", exists);
|
||||
if (exists.getUserId() == null && exists.getWorkStatus().equals("2") && exists.getStatus().equals("0") ) {
|
||||
if (exists.getUserId() == null && "2".equals(exists.getWorkStatus()) && "0".equals(exists.getStatus())) {
|
||||
params.put("bindDeviceStatus", 305);
|
||||
params.put("bindDeviceStatusName", "设备未配网,请先去配置网络");
|
||||
return params;
|
||||
|
||||
@@ -165,10 +165,10 @@ public class AppScheduleServiceImpl implements IAppScheduleService {
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
private List saveDetails(Long scheduleId, List<AppScheduleBo.AppScheduleDetail> details) {
|
||||
List list = new ArrayList<>();
|
||||
private List<Map<String, Object>> saveDetails(Long scheduleId, List<AppScheduleBo.AppScheduleDetail> details) {
|
||||
List<Map<String, Object>> detailList = new ArrayList<>();
|
||||
if (details == null) {
|
||||
return list;
|
||||
return detailList;
|
||||
}
|
||||
for (AppScheduleBo.AppScheduleDetail item : details) {
|
||||
AppScheduleDetail detail = new AppScheduleDetail();
|
||||
@@ -181,13 +181,13 @@ public class AppScheduleServiceImpl implements IAppScheduleService {
|
||||
detail.setTriggerType(item.getTriggerType());
|
||||
detail.setStatus(item.getStatus());
|
||||
scheduleDetailMapper.insert(detail);
|
||||
list.add(toDetailMap(detail, item.getTimeData()));
|
||||
detailList.add(toDetailMap(detail, item.getTimeData()));
|
||||
}
|
||||
return list;
|
||||
return detailList;
|
||||
}
|
||||
|
||||
private List updateDetails(List<AppScheduleBo.AppScheduleDetail> details) {
|
||||
List detailList = new ArrayList<>();
|
||||
private List<Map<String, Object>> updateDetails(List<AppScheduleBo.AppScheduleDetail> details) {
|
||||
List<Map<String, Object>> detailList = new ArrayList<>();
|
||||
if (details == null) {
|
||||
return detailList;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import org.springframework.stereotype.Service;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -106,4 +108,19 @@ public class AppSchedulingDeviceServiceImpl implements IAppSchedulingDeviceServi
|
||||
public List<AppSchedulingDeviceVo> findByDeviceNo(String deviceNo) {
|
||||
return baseMapper.selectVoList(new QueryWrapper<AppSchedulingDevice>().eq("device_no", deviceNo));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> findBoundDeviceNos(Collection<String> deviceNos) {
|
||||
if (deviceNos == null || deviceNos.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
return baseMapper.selectList(
|
||||
new QueryWrapper<AppSchedulingDevice>()
|
||||
.select("device_no")
|
||||
.in("device_no", deviceNos))
|
||||
.stream()
|
||||
.map(AppSchedulingDevice::getDeviceNo)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import java.util.List;
|
||||
/**
|
||||
* 设备离线检测定时任务
|
||||
* <p>
|
||||
* 逻辑:默认通过设备 LWT 离线消息更新状态。
|
||||
* 兼容旧设备时,可打开 ttl-compat-enabled,继续使用 Redis 在线 Key 过期兜底离线。
|
||||
* 逻辑:电量上报刷新 Redis 在线 Key,Key 超时后判定设备离线。
|
||||
* 设备 LWT 离线消息仍可立即更新离线状态,本任务用于处理未收到遗嘱的异常断线。
|
||||
* 本任务定期扫描数据库中非离线状态的设备,如果对应 Redis Key 已不存在,
|
||||
* 则将数据库 status 更新为 0(离线)。
|
||||
* </p>
|
||||
@@ -37,19 +37,19 @@ public class DeviceOfflineCheckTask {
|
||||
@Value("${mqtt.command-ack.device-status-cache-prefix}")
|
||||
private String deviceStatusCachePrefix;
|
||||
|
||||
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds:900}")
|
||||
@Value("${mqtt.command-ack.device-status-cache-ttl-seconds:600}")
|
||||
private long deviceStatusCacheTtlSeconds;
|
||||
|
||||
@Value("${mqtt.offline-check.enabled:true}")
|
||||
private boolean enabled;
|
||||
@Value("${mqtt.offline-check.ttl-compat-enabled:false}")
|
||||
@Value("${mqtt.offline-check.ttl-compat-enabled:true}")
|
||||
private boolean ttlCompatEnabled;
|
||||
|
||||
/**
|
||||
* 每 90 秒检查一次。默认不开启 TTL 兼容离线检测,避免低功耗长连接设备被误判离线。
|
||||
* 每 30 秒检查一次电量心跳是否超时。
|
||||
* 可通过 mqtt.offline-check.interval-ms 覆盖(单位 ms)
|
||||
*/
|
||||
@Scheduled(fixedDelayString = "${mqtt.offline-check.interval-ms:90000}")
|
||||
@Scheduled(fixedDelayString = "${mqtt.offline-check.interval-ms:30000}")
|
||||
public void checkOfflineDevices() {
|
||||
if (!enabled || !ttlCompatEnabled) {
|
||||
return;
|
||||
@@ -96,7 +96,7 @@ public class DeviceOfflineCheckTask {
|
||||
|
||||
List<String> updatedDeviceNos = new ArrayList<>();
|
||||
for (String deviceNo : offlineDeviceNos) {
|
||||
if (deviceStatusService.markOfflineIfCacheMissing(deviceNo, "设备掉线")) {
|
||||
if (deviceStatusService.markOfflineIfCacheMissing(deviceNo, "设备电量心跳超时")) {
|
||||
updatedDeviceNos.add(deviceNo);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user