fix(app): 修复 AppController 安全与查询问题

- 加强异常处理、类型安全和图片上传校验
- 优化设备相关查询,避免重复访问数据源
- 补充并记录并发测试与审查修复实施计划
This commit is contained in:
yuhaiming
2026-07-17 08:20:44 +08:00
parent 4a4aabf19e
commit 70e2356f22
82 changed files with 6070 additions and 583 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,14 @@ public interface IAppDeviceService {
*/
AppDeviceVo queryById(String deviceNo);
/**
* 按设备编号批量查询设备。
*
* @param deviceNos 设备编号集合
* @return 设备列表
*/
List<AppDeviceVo> queryByDeviceNos(Collection<String> deviceNos);
/**
* 分页查询设备信息
列表

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,8 +18,8 @@ import java.util.List;
/**
* 设备离线检测定时任务
* <p>
* 逻辑:默认通过设备 LWT 离线消息更新状态
* 兼容旧设备时,可打开 ttl-compat-enabled继续使用 Redis 在线 Key 过期兜底离线。
* 逻辑:电量上报刷新 Redis 在线 KeyKey 超时后判定设备离线
* 设备 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);
}
}