diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java index c8678aff..f8a9b702 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java @@ -125,7 +125,9 @@ public class DeviceController { return new Result().error("设备不存在"); } BeanUtils.copyProperties(deviceUpdateDTO, entity); - deviceService.updateById(entity); + if (!deviceService.updateById(entity)) { + return new Result().error(ErrorCode.UPDATE_DATA_FAILED); + } return new Result(); } @@ -211,4 +213,4 @@ public class DeviceController { deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission()); return new Result(); } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index b61ede2f..2ed817b1 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -202,8 +202,8 @@ public class DeviceServiceImpl extends BaseServiceImpl firmware.setUrl(Constant.INVALID_FIRMWARE_URL); response.setFirmware(firmware); } else { - // 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息 - if (deviceById.getAutoUpdate() != 0) { + // 只有在设备已绑定且明确开启自动升级时才返回固件升级信息 + if (Integer.valueOf(1).equals(deviceById.getAutoUpdate())) { String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType(); DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type, deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion()); @@ -299,6 +299,7 @@ public class DeviceServiceImpl extends BaseServiceImpl UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class); vo.setDeviceType(device.getBoard()); vo.setBoard(device.getBoard()); + vo.setAutoUpdate(device.getAutoUpdate()); vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate())); vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt())); return vo; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/vo/UserShowDeviceListVO.java b/main/manager-api/src/main/java/xiaozhi/modules/device/vo/UserShowDeviceListVO.java index 857d08cd..b00a1686 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/vo/UserShowDeviceListVO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/vo/UserShowDeviceListVO.java @@ -32,8 +32,8 @@ public class UserShowDeviceListVO { @Schema(description = "设备别名") private String alias; - @Schema(description = "开启OTA") - private Integer otaUpgrade; + @Schema(description = "自动更新开关(0关闭/1开启)") + private Integer autoUpdate; @Schema(description = "最近对话时间") private String recentChatTime; diff --git a/main/manager-api/src/test/java/xiaozhi/modules/device/controller/DeviceControllerTest.java b/main/manager-api/src/test/java/xiaozhi/modules/device/controller/DeviceControllerTest.java new file mode 100644 index 00000000..a3fa0ad4 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/device/controller/DeviceControllerTest.java @@ -0,0 +1,78 @@ +package xiaozhi.modules.device.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.redis.RedisUtils; +import xiaozhi.common.user.UserDetail; +import xiaozhi.common.utils.MessageUtils; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.device.dto.DeviceUpdateDTO; +import xiaozhi.modules.device.entity.DeviceEntity; +import xiaozhi.modules.device.service.DeviceAddressBookService; +import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.security.user.SecurityUser; +import xiaozhi.modules.sys.service.SysParamsService; + +@DisplayName("设备更新接口回归测试") +class DeviceControllerTest { + + private static final String DEVICE_ID = "device-id"; + private static final long USER_ID = 1L; + + @Test + @DisplayName("数据库未更新时不误报自动升级状态修改成功") + void updateFailureIsReturnedToCaller() { + DeviceService deviceService = mock(DeviceService.class); + DeviceEntity entity = ownedDevice(); + when(deviceService.selectById(DEVICE_ID)).thenReturn(entity); + when(deviceService.updateById(entity)).thenReturn(false); + DeviceController controller = controller(deviceService); + + DeviceUpdateDTO update = new DeviceUpdateDTO(); + update.setAutoUpdate(0); + + try (MockedStatic securityUser = mockStatic(SecurityUser.class); + MockedStatic messageUtils = mockStatic(MessageUtils.class)) { + securityUser.when(SecurityUser::getUser).thenReturn(currentUser()); + messageUtils.when(() -> MessageUtils.getMessage(ErrorCode.UPDATE_DATA_FAILED)) + .thenReturn("Failed to update data"); + + Result result = controller.updateDeviceInfo(DEVICE_ID, update); + + assertEquals(ErrorCode.UPDATE_DATA_FAILED, result.getCode()); + assertEquals(0, entity.getAutoUpdate()); + verify(deviceService).updateById(entity); + } + } + + private DeviceController controller(DeviceService deviceService) { + return new DeviceController( + deviceService, + mock(DeviceAddressBookService.class), + mock(RedisUtils.class), + mock(SysParamsService.class)); + } + + private DeviceEntity ownedDevice() { + DeviceEntity entity = new DeviceEntity(); + entity.setId(DEVICE_ID); + entity.setUserId(USER_ID); + entity.setAutoUpdate(1); + return entity; + } + + private UserDetail currentUser() { + UserDetail user = new UserDetail(); + user.setId(USER_ID); + return user; + } +} diff --git a/main/manager-api/src/test/java/xiaozhi/modules/device/service/impl/DeviceAutoUpdateTest.java b/main/manager-api/src/test/java/xiaozhi/modules/device/service/impl/DeviceAutoUpdateTest.java new file mode 100644 index 00000000..67a29a6c --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/device/service/impl/DeviceAutoUpdateTest.java @@ -0,0 +1,104 @@ +package xiaozhi.modules.device.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.aop.framework.ProxyFactory; + +import xiaozhi.common.constant.Constant; +import xiaozhi.modules.device.dto.DeviceReportReqDTO; +import xiaozhi.modules.device.dto.DeviceReportRespDTO; +import xiaozhi.modules.device.entity.DeviceEntity; +import xiaozhi.modules.device.service.OtaService; +import xiaozhi.modules.sys.service.SysParamsService; + +@DisplayName("设备自动升级回归测试") +class DeviceAutoUpdateTest { + + private static final String MAC_ADDRESS = "00:11:22:33:44:55"; + private static final String BOARD_TYPE = "test-board"; + private static final String CURRENT_VERSION = "1.0.0"; + + @Test + @DisplayName("#3299 自动升级关闭时 OTA 响应不包含固件") + void disabledAutoUpdateOmitsFirmware() { + OtaService otaService = mock(OtaService.class); + DeviceServiceImpl service = proxiedService(deviceWithAutoUpdate(0), otaService); + + DeviceReportRespDTO response = service.checkDeviceActive( + MAC_ADDRESS, MAC_ADDRESS, deviceReport()); + + assertNull(response.getFirmware()); + verifyNoInteractions(otaService); + } + + @Test + @DisplayName("自动升级开启时继续执行固件查询") + void enabledAutoUpdateChecksFirmware() { + OtaService otaService = mock(OtaService.class); + when(otaService.getLatestOta(BOARD_TYPE)).thenReturn(null); + DeviceServiceImpl service = proxiedService(deviceWithAutoUpdate(1), otaService); + + DeviceReportRespDTO response = service.checkDeviceActive( + MAC_ADDRESS, MAC_ADDRESS, deviceReport()); + + assertNotNull(response.getFirmware()); + assertEquals(CURRENT_VERSION, response.getFirmware().getVersion()); + assertEquals(Constant.INVALID_FIRMWARE_URL, response.getFirmware().getUrl()); + verify(otaService).getLatestOta(BOARD_TYPE); + } + + private DeviceServiceImpl proxiedService(DeviceEntity device, OtaService otaService) { + SysParamsService sysParamsService = mock(SysParamsService.class); + when(sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true)) + .thenReturn("ws://127.0.0.1:8000/xiaozhi/v1/"); + when(sysParamsService.getValue(Constant.SERVER_AUTH_ENABLED, true)).thenReturn("false"); + when(sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, true)).thenReturn(null); + + DeviceServiceImpl target = new DeviceServiceImpl( + null, null, sysParamsService, null, otaService, null) { + @Override + public DeviceEntity getDeviceByMacAddress(String macAddress) { + return device; + } + + @Override + public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) { + // No-op: connection timestamps are outside this OTA decision test. + } + }; + + ProxyFactory proxyFactory = new ProxyFactory(target); + proxyFactory.setProxyTargetClass(true); + proxyFactory.setExposeProxy(true); + return (DeviceServiceImpl) proxyFactory.getProxy(); + } + + private DeviceEntity deviceWithAutoUpdate(int autoUpdate) { + DeviceEntity device = new DeviceEntity(); + device.setId(MAC_ADDRESS); + device.setMacAddress(MAC_ADDRESS); + device.setBoard(BOARD_TYPE); + device.setAutoUpdate(autoUpdate); + return device; + } + + private DeviceReportReqDTO deviceReport() { + DeviceReportReqDTO.Application application = new DeviceReportReqDTO.Application(); + application.setVersion(CURRENT_VERSION); + DeviceReportReqDTO.BoardInfo board = new DeviceReportReqDTO.BoardInfo(); + board.setType(BOARD_TYPE); + + DeviceReportReqDTO report = new DeviceReportReqDTO(); + report.setApplication(application); + report.setBoard(board); + return report; + } +} diff --git a/main/manager-api/src/test/java/xiaozhi/modules/device/service/impl/DeviceTimeSerializationTest.java b/main/manager-api/src/test/java/xiaozhi/modules/device/service/impl/DeviceTimeSerializationTest.java index f58013f6..bf2e1483 100644 --- a/main/manager-api/src/test/java/xiaozhi/modules/device/service/impl/DeviceTimeSerializationTest.java +++ b/main/manager-api/src/test/java/xiaozhi/modules/device/service/impl/DeviceTimeSerializationTest.java @@ -76,6 +76,25 @@ class DeviceTimeSerializationTest { () -> assertTrue(payload.path("createDate").isNull())); } + @ParameterizedTest(name = "自动升级状态 {0}") + @ValueSource(ints = { 0, 1 }) + @DisplayName("#3299 设备列表按 autoUpdate 契约返回真实开关状态") + void serializedDeviceContainsAutoUpdateState(int autoUpdate) { + DeviceEntity entity = new DeviceEntity(); + entity.setAutoUpdate(autoUpdate); + DeviceServiceImpl deviceService = serviceReturning(entity); + + UserShowDeviceListVO device = deviceService.getUserDeviceList(1L, "agent-id").getFirst(); + ObjectMapper objectMapper = new WebMvcConfig().jackson2HttpMessageConverter().getObjectMapper(); + JsonNode payload = objectMapper.valueToTree(device); + + assertAll( + () -> assertEquals(autoUpdate, device.getAutoUpdate()), + () -> assertEquals(autoUpdate, payload.path("autoUpdate").asInt()), + () -> assertTrue(payload.path("otaUpgrade").isMissingNode(), + "设备列表不应继续暴露未映射的旧字段 otaUpgrade")); + } + private DeviceServiceImpl serviceReturning(DeviceEntity entity) { return new DeviceServiceImpl(null, null, null, null, null, null) { @Override