设备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");
}
}