Compare commits

..
Author SHA1 Message Date
CGDandGitHub f5ed1aaec8 Merge pull request #3305 from xinnan-tech/update_version
Bump to 0.9.6
2026-07-24 11:32:33 +08:00
hrz 9da58e254c Bump to 0.9.6 2026-07-24 09:53:03 +08:00
CGDandGitHub 27e57631a7 Merge pull request #3300 from xinnan-tech/test/manager-api-runtime-warnings
test(manager-api): 清理测试运行告警
2026-07-23 17:46:04 +08:00
CGDandGitHub b1e39da9b8 Merge pull request #3301 from chentyke/fix/issue-3299-auto-update-state
fix: 修复设备自动升级开关状态不生效
2026-07-22 17:38:13 +08:00
Tyke Chen e87fc766d2 fix: restore device auto-update switch behavior 2026-07-22 17:10:21 +08:00
9 changed files with 213 additions and 9 deletions
@@ -324,7 +324,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.9.5"; public static final String VERSION = "0.9.6";
/** /**
* 无效固件URL * 无效固件URL
@@ -125,7 +125,9 @@ public class DeviceController {
return new Result<Void>().error("设备不存在"); return new Result<Void>().error("设备不存在");
} }
BeanUtils.copyProperties(deviceUpdateDTO, entity); BeanUtils.copyProperties(deviceUpdateDTO, entity);
deviceService.updateById(entity); if (!deviceService.updateById(entity)) {
return new Result<Void>().error(ErrorCode.UPDATE_DATA_FAILED);
}
return new Result<Void>(); return new Result<Void>();
} }
@@ -202,8 +202,8 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
firmware.setUrl(Constant.INVALID_FIRMWARE_URL); firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
response.setFirmware(firmware); response.setFirmware(firmware);
} else { } else {
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息 // 只有在设备已绑定且明确开启自动升级时才返回固件升级信息
if (deviceById.getAutoUpdate() != 0) { if (Integer.valueOf(1).equals(deviceById.getAutoUpdate())) {
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType(); String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type, DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion()); deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
@@ -299,6 +299,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class); UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
vo.setDeviceType(device.getBoard()); vo.setDeviceType(device.getBoard());
vo.setBoard(device.getBoard()); vo.setBoard(device.getBoard());
vo.setAutoUpdate(device.getAutoUpdate());
vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate())); vo.setCreateDateTimestamp(toTimestamp(device.getCreateDate()));
vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt())); vo.setLastConnectedAtTimestamp(toTimestamp(device.getLastConnectedAt()));
return vo; return vo;
@@ -32,8 +32,8 @@ public class UserShowDeviceListVO {
@Schema(description = "设备别名") @Schema(description = "设备别名")
private String alias; private String alias;
@Schema(description = "开启OTA") @Schema(description = "自动更新开关(0关闭/1开启)")
private Integer otaUpgrade; private Integer autoUpdate;
@Schema(description = "最近对话时间") @Schema(description = "最近对话时间")
private String recentChatTime; private String recentChatTime;
@@ -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> securityUser = mockStatic(SecurityUser.class);
MockedStatic<MessageUtils> messageUtils = mockStatic(MessageUtils.class)) {
securityUser.when(SecurityUser::getUser).thenReturn(currentUser());
messageUtils.when(() -> MessageUtils.getMessage(ErrorCode.UPDATE_DATA_FAILED))
.thenReturn("Failed to update data");
Result<Void> 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;
}
}
@@ -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;
}
}
@@ -76,6 +76,25 @@ class DeviceTimeSerializationTest {
() -> assertTrue(payload.path("createDate").isNull())); () -> 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) { private DeviceServiceImpl serviceReturning(DeviceEntity entity) {
return new DeviceServiceImpl(null, null, null, null, null, null) { return new DeviceServiceImpl(null, null, null, null, null, null) {
@Override @Override
@@ -265,7 +265,7 @@ function showAbout() {
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }), title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
content: t('settings.aboutContent', { content: t('settings.aboutContent', {
appName: import.meta.env.VITE_APP_TITLE, appName: import.meta.env.VITE_APP_TITLE,
version: '0.9.5', version: '0.9.6',
}), }),
showCancel: false, showCancel: false,
confirmText: t('common.confirm'), confirmText: t('common.confirm'),
+1 -1
View File
@@ -6,7 +6,7 @@ from config.config_loader import load_config
from config.settings import check_config_file from config.settings import check_config_file
from core.utils.cache.manager import cache_manager, CacheType from core.utils.cache.manager import cache_manager, CacheType
SERVER_VERSION = "0.9.5" SERVER_VERSION = "0.9.6"
_logger_initialized = False _logger_initialized = False