设备bug修改 新增设备解绑功能

This commit is contained in:
yuhaiming
2026-09-01 17:24:12 +08:00
parent 509e760f3d
commit 04c83305ad
36 changed files with 796 additions and 214 deletions

View File

@@ -365,6 +365,11 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
// 其他线程正在刷新,本次跳过;状态写入幂等,下条 ACK 仍会兜底刷新
return;
}
appDeviceMapper.update(null,
new LambdaUpdateWrapper<AppDevice>()
.set(AppDevice::getStatus, "1")
.eq(AppDevice::getDeviceNo, deviceNo)
);
Map<String, Object> statusCache = new HashMap<>();
statusCache.put("deviceNo", deviceNo);
statusCache.put("status", "1");
@@ -374,11 +379,6 @@ public class MqttCommandAckService implements IDeviceCommandAckHandler {
statusCache,
Duration.ofSeconds(mqttProperties.getCommandAck().getDeviceStatusCacheTtlSeconds())
);
appDeviceMapper.update(null,
new LambdaUpdateWrapper<AppDevice>()
.set(AppDevice::getStatus, "1")
.eq(AppDevice::getDeviceNo, deviceNo)
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("[MQTT] 刷新设备在线状态被中断 设备编号={}", deviceNo);

View File

@@ -30,7 +30,6 @@ public class MqttDeviceUnbindResultPublisher implements IDeviceUnbindResultPubli
throw new ServiceException("解绑回执缺少设备编号");
}
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("commandId", result.commandId());
payload.put("bindingId", result.bindingId());
payload.put("status", result.status());
payload.put("message", result.message());

View File

@@ -418,10 +418,23 @@ class MqttCommandAckServiceTest {
when(redissonClient.getLock("lock:mqtt:device:status:D01")).thenReturn(lock);
when(lock.tryLock(0, 10, TimeUnit.SECONDS)).thenReturn(true);
when(lock.isHeldByCurrentThread()).thenReturn(true);
AtomicBoolean databaseUpdated = new AtomicBoolean();
doAnswer(invocation -> {
databaseUpdated.set(true);
return 1;
}).when(appDeviceMapper).update(eq(null), any());
MqttCommandAckService service = new MqttCommandAckService(properties, appDeviceMapper);
try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
redis.when(RedisUtils::getClient).thenReturn(redissonClient);
redis.when(() -> RedisUtils.setCacheObject(
eq("mqtt:device:status:D01"), any(Map.class), eq(Duration.ofSeconds(900))
)).thenAnswer(invocation -> {
if (!databaseUpdated.get()) {
throw new AssertionError("online cache was written before database status update");
}
return null;
});
ReflectionTestUtils.invokeMethod(service, "refreshDeviceOnline", "D01");
@@ -434,4 +447,30 @@ class MqttCommandAckServiceTest {
verify(lock).unlock();
}
}
@Test
void refreshDeviceOnlineDoesNotWriteCacheWhenDatabaseUpdateFails() throws Exception {
MqttProperties properties = new MqttProperties();
AppDeviceMapper appDeviceMapper = mock(AppDeviceMapper.class);
RedissonClient redissonClient = mock(RedissonClient.class);
RLock lock = mock(RLock.class);
when(redissonClient.getLock("lock:mqtt:device:status:D01")).thenReturn(lock);
when(lock.tryLock(0, 10, TimeUnit.SECONDS)).thenReturn(true);
when(lock.isHeldByCurrentThread()).thenReturn(true);
doThrow(new IllegalStateException("database failure")).when(appDeviceMapper).update(eq(null), any());
MqttCommandAckService service = new MqttCommandAckService(properties, appDeviceMapper);
try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
redis.when(RedisUtils::getClient).thenReturn(redissonClient);
assertThatThrownBy(() -> ReflectionTestUtils.invokeMethod(service, "refreshDeviceOnline", "D01"))
.isInstanceOf(IllegalStateException.class)
.hasMessage("database failure");
redis.verify(() -> RedisUtils.setCacheObject(
eq("mqtt:device:status:D01"), any(Map.class), eq(Duration.ofSeconds(600))
), never());
verify(lock).unlock();
}
}
}

View File

@@ -0,0 +1,58 @@
package org.dromara.mqtt;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.app.domain.mqtt.DeviceUnbindResult;
import org.dromara.mqtt.config.properties.MqttProperties;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@Tag("dev")
class MqttDeviceUnbindResultPublisherTest {
private static GenericApplicationContext applicationContext;
@BeforeAll
static void initializeJsonUtils() {
applicationContext = new GenericApplicationContext();
applicationContext.registerBean(ObjectMapper.class, (Supplier<ObjectMapper>) ObjectMapper::new);
applicationContext.refresh();
new SpringUtil().setApplicationContext(applicationContext);
}
@AfterAll
static void closeApplicationContext() {
applicationContext.close();
}
@Test
void publishUsesBindingIdWithoutCommandId() throws Exception {
MqttClientManager clientManager = mock(MqttClientManager.class);
MqttDeviceUnbindResultPublisher publisher = new MqttDeviceUnbindResultPublisher(new MqttProperties());
ReflectionTestUtils.setField(publisher, "mqttClientManager", clientManager);
publisher.publish(new DeviceUnbindResult(
"D01", "binding-1", DeviceUnbindResult.APPLIED, "done"));
ArgumentCaptor<String> payloadCaptor = ArgumentCaptor.forClass(String.class);
verify(clientManager).publish(org.mockito.ArgumentMatchers.eq("/d01/subscriber/unbind/ack"),
payloadCaptor.capture());
JsonNode payload = new ObjectMapper().readTree(payloadCaptor.getValue());
assertThat(payload.has("commandId")).isFalse();
assertThat(payload.get("bindingId").asText()).isEqualTo("binding-1");
assertThat(payload.get("status").asText()).isEqualTo(DeviceUnbindResult.APPLIED);
assertThat(payload.get("message").asText()).isEqualTo("done");
}
}

View File

@@ -47,6 +47,12 @@
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,27 @@
package org.dromara.common.mybatis.p6spy;
import com.baomidou.mybatisplus.extension.p6spy.StdoutLogger;
/**
* P6Spy SQL logger that suppresses SQL executed by scheduled tasks.
*/
public class ScheduledTaskAwareStdoutLogger extends StdoutLogger {
private static final String SCHEDULE_POOL_PREFIX = "schedule-pool-";
private static final String VIRTUAL_SCHEDULE_POOL_PREFIX = "virtual-schedule-pool-";
private static final String SPRING_SCHEDULING_PREFIX = "scheduling-";
static boolean isScheduledTaskThread(String threadName) {
return threadName != null
&& (threadName.startsWith(SCHEDULE_POOL_PREFIX)
|| threadName.startsWith(VIRTUAL_SCHEDULE_POOL_PREFIX)
|| threadName.startsWith(SPRING_SCHEDULING_PREFIX));
}
@Override
public void logText(String text) {
if (!isScheduledTaskThread(Thread.currentThread().getName())) {
super.logText(text);
}
}
}

View File

@@ -2,8 +2,8 @@
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 日志输出到控制台,定时任务线程不打印 SQL
appender=org.dromara.common.mybatis.p6spy.ScheduledTaskAwareStdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 取消JDBC URL前缀

View File

@@ -0,0 +1,24 @@
package org.dromara.common.mybatis.p6spy;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("dev")
public class ScheduledTaskAwareStdoutLoggerTest {
@Test
public void identifiesScheduledTaskThreads() {
assertThat(ScheduledTaskAwareStdoutLogger.isScheduledTaskThread("schedule-pool-1")).isTrue();
assertThat(ScheduledTaskAwareStdoutLogger.isScheduledTaskThread("virtual-schedule-pool-2")).isTrue();
assertThat(ScheduledTaskAwareStdoutLogger.isScheduledTaskThread("scheduling-3")).isTrue();
}
@Test
public void keepsSqlLoggingEnabledForOtherThreads() {
assertThat(ScheduledTaskAwareStdoutLogger.isScheduledTaskThread("XNIO-1 task-2")).isFalse();
assertThat(ScheduledTaskAwareStdoutLogger.isScheduledTaskThread("mqtt-consumer-1")).isFalse();
assertThat(ScheduledTaskAwareStdoutLogger.isScheduledTaskThread(null)).isFalse();
}
}