diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/JsonUtils.java b/main/manager-api/src/main/java/xiaozhi/common/utils/JsonUtils.java index 288dae9a..dae0498b 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/utils/JsonUtils.java +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/JsonUtils.java @@ -1,7 +1,9 @@ package xiaozhi.common.utils; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -16,6 +18,10 @@ import cn.hutool.core.util.StrUtil; */ public class JsonUtils { private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final TypeReference> STRING_OBJECT_MAP = new TypeReference<>() { + }; + private static final TypeReference>> STRING_OBJECT_MAP_LIST = new TypeReference<>() { + }; public static String toJsonString(Object object) { try { @@ -67,4 +73,59 @@ public class JsonUtils { } } + public static Map parseMap(String text) { + if (StrUtil.isEmpty(text)) { + return null; + } + return parseObject(text, STRING_OBJECT_MAP); + } + + public static List> parseMapList(String text) { + if (StrUtil.isEmpty(text)) { + return null; + } + return parseObject(text, STRING_OBJECT_MAP_LIST); + } + + public static Map toStringObjectMap(Object value) { + if (value == null) { + return null; + } + if (!(value instanceof Map map)) { + throw new ClassCastException("Expected Map but got " + value.getClass().getName()); + } + + Map result = new LinkedHashMap<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + result.put(String.class.cast(entry.getKey()), entry.getValue()); + } + return result; + } + + public static List> toStringObjectMapList(Object value) { + if (value == null) { + return null; + } + + List list = List.class.cast(value); + List> result = new ArrayList<>(list.size()); + for (Object item : list) { + result.add(toStringObjectMap(item)); + } + return result; + } + + public static List toList(Object value, Class elementType) { + if (value == null) { + return null; + } + + List list = List.class.cast(value); + List result = new ArrayList<>(list.size()); + for (Object item : list) { + result.add(elementType.cast(item)); + } + return result; + } + } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index 407a3e45..0961fe7b 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -34,6 +34,7 @@ import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.user.UserDetail; +import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.Result; import xiaozhi.common.utils.ResultUtils; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; @@ -340,8 +341,8 @@ public class AgentController { @RequiresPermissions("sys:role:normal") public Result saveAgentTags(@PathVariable String id, @RequestBody Map params) { requireAgentPermission(id); - List tagIds = (List) params.get("tagIds"); - List tagNames = (List) params.get("tagNames"); + List tagIds = JsonUtils.toList(params.get("tagIds"), String.class); + List tagNames = JsonUtils.toList(params.get("tagNames"), String.class); AgentUpdateDTO dto = new AgentUpdateDTO(); dto.setTagIds(tagIds); dto.setTagNames(tagNames); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java index 2183f98d..8337afae 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java @@ -167,7 +167,7 @@ public class AgentChatHistoryServiceImpl extends CrudRepository jsonMap = JsonUtils.parseObject(content, Map.class); + Map jsonMap = JsonUtils.parseMap(content); if (jsonMap != null && jsonMap.containsKey("content")) { Object contentObj = jsonMap.get("content"); return contentObj != null ? contentObj.toString() : content; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java index f1cdf94c..6ff84995 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentMcpAccessPointServiceImpl.java @@ -76,7 +76,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic // 等待初始化响应 (id=1) - 移除固定延迟,改为响应驱动 List initResponses = client.listenerWithoutClose(response -> { try { - Map jsonMap = JsonUtils.parseObject(response, Map.class); + Map jsonMap = JsonUtils.parseMap(response); if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) { // 检查是否有result字段,表示初始化成功 return jsonMap.containsKey("result") && !jsonMap.containsKey("error"); @@ -92,7 +92,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic boolean initSucceeded = false; for (String response : initResponses) { try { - Map jsonMap = JsonUtils.parseObject(response, Map.class); + Map jsonMap = JsonUtils.parseMap(response); if (jsonMap != null && Integer.valueOf(1).equals(jsonMap.get("id"))) { if (jsonMap.containsKey("result")) { log.info("MCP初始化成功,智能体ID: {}", id); @@ -123,7 +123,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic // 等待工具列表响应 (id=2) List toolsResponses = client.listener(response -> { try { - Map jsonMap = JsonUtils.parseObject(response, Map.class); + Map jsonMap = JsonUtils.parseMap(response); return jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id")); } catch (Exception e) { log.warn("解析工具列表响应失败: {}", response, e); @@ -134,18 +134,18 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic // 处理工具列表响应 for (String response : toolsResponses) { try { - Map jsonMap = JsonUtils.parseObject(response, Map.class); + Map jsonMap = JsonUtils.parseMap(response); if (jsonMap != null && Integer.valueOf(2).equals(jsonMap.get("id"))) { // 检查是否有result字段 Object resultObj = jsonMap.get("result"); - if (resultObj instanceof Map) { - Map resultMap = (Map) resultObj; + if (resultObj instanceof Map) { + Map resultMap = JsonUtils.toStringObjectMap(resultObj); Object toolsObj = resultMap.get("tools"); - if (toolsObj instanceof List) { - List> toolsList = (List>) toolsObj; + if (toolsObj instanceof List) { + List> toolsList = JsonUtils.toStringObjectMapList(toolsObj); // 提取工具名称列表 List result = toolsList.stream() - .map(tool -> (String) tool.get("name")) + .map(tool -> String.class.cast(tool.get("name"))) .filter(name -> name != null) .sorted() .collect(Collectors.toList()); @@ -232,4 +232,4 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic // 加密后成token值 return AESUtils.encrypt(key, json); } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index cfeb620c..ecab5c09 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -677,7 +677,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp mapping.setPluginId(pluginId); Map paramInfo = new HashMap<>(); - List> fields = JsonUtils.parseObject(provider.getFields(), List.class); + List> fields = JsonUtils.parseMapList(provider.getFields()); if (fields != null) { for (Map field : fields) { paramInfo.put((String) field.get("key"), field.get("default")); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/ConfigService.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/ConfigService.java index e234e22d..81f19853 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/ConfigService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/ConfigService.java @@ -10,7 +10,7 @@ public interface ConfigService { * @param isCache 是否缓存 * @return 配置信息 */ - Object getConfig(Boolean isCache); + Map getConfig(Boolean isCache); /** * 获取智能体模型配置 @@ -28,4 +28,4 @@ public interface ConfigService { * @return 替换词列表,格式如 ["模板1|模板01", "模板2|模板02"] */ List getCorrectWords(String macAddress); -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index 8e35e0b7..45e465f2 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -65,12 +65,12 @@ public class ConfigServiceImpl implements ConfigService { private final CorrectWordFileService correctWordFileService; @Override - public Object getConfig(Boolean isCache) { + public Map getConfig(Boolean isCache) { if (isCache) { // 先从Redis获取配置 Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey()); if (cachedConfig != null) { - return cachedConfig; + return JsonUtils.toStringObjectMap(cachedConfig); } } @@ -123,7 +123,7 @@ public class ConfigServiceImpl implements ConfigService { if (isAdminRequest != null && "true".equals(isAdminRequest)) { // 管理控制台请求,返回getConfig的结果 redisUtils.delete(redisKey); // 使用后清理 - return (Map) getConfig(true); + return getConfig(true); } // 根据MAC地址查找设备 DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); @@ -277,10 +277,10 @@ public class ConfigServiceImpl implements ConfigService { // 遍历除最后一个key之外的所有key for (int i = 0; i < keys.length - 1; i++) { String key = keys[i]; - if (!current.containsKey(key)) { - current.put(key, new HashMap()); - } - current = (Map) current.get(key); + Object nestedConfig = current.computeIfAbsent(key, ignored -> new HashMap()); + Map nestedMap = JsonUtils.toStringObjectMap(nestedConfig); + current.put(key, nestedMap); + current = nestedMap; } // 处理最后一个key 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 138c4164..b61ede2f 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 @@ -50,6 +50,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.DateUtils; +import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.ToolUtil; import xiaozhi.modules.device.dao.DeviceDao; import xiaozhi.modules.device.dto.DeviceManualAddDTO; @@ -109,7 +110,7 @@ public class DeviceServiceImpl extends BaseServiceImpl String deviceId = (String) cacheDeviceId; String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId); - Map cacheMap = (Map) redisUtils.get(cacheDeviceKey); + Map cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(cacheDeviceKey)); if (ToolUtil.isEmpty(cacheMap)) { throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); } @@ -411,7 +412,7 @@ public class DeviceServiceImpl extends BaseServiceImpl public String geCodeByDeviceId(String deviceId) { String dataKey = getDeviceCacheKey(deviceId); - Map cacheMap = (Map) redisUtils.get(dataKey); + Map cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(dataKey)); if (cacheMap != null && cacheMap.containsKey("activation_code")) { String cachedCode = (String) cacheMap.get("activation_code"); return cachedCode; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/knowledge/rag/impl/RAGFlowAdapter.java b/main/manager-api/src/main/java/xiaozhi/modules/knowledge/rag/impl/RAGFlowAdapter.java index f66623f7..ae6f913a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/knowledge/rag/impl/RAGFlowAdapter.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/knowledge/rag/impl/RAGFlowAdapter.java @@ -149,8 +149,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter { log.info("=== [RAGFlow] 获取文档列表: datasetId={} ===", datasetId); // 使用 Jackson 将 DTO 转为 Map 作为查询参数 - @SuppressWarnings("unchecked") - Map params = objectMapper.convertValue(req, Map.class); + Map params = objectMapper.convertValue(req, new TypeReference>() { + }); Map response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params); @@ -174,13 +174,12 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter { .pageSize(1) .build(); - @SuppressWarnings("unchecked") - Map params = objectMapper.convertValue(req, Map.class); + Map params = objectMapper.convertValue(req, new TypeReference>() { + }); Map response = getClient().get("/api/v1/datasets/" + datasetId + "/documents", params); Object dataObj = response.get("data"); - if (dataObj instanceof Map) { - Map dataMap = (Map) dataObj; + if (dataObj instanceof Map dataMap) { List documents = (List) dataMap.get("docs"); if (documents != null && !documents.isEmpty()) { return objectMapper.convertValue(documents.get(0), DocumentDTO.InfoVO.class); @@ -567,8 +566,8 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter { return new PageData<>(new ArrayList<>(), 0); } - Map dataMap = (Map) dataObj; - List> documents = (List>) dataMap.get("docs"); + Map dataMap = Map.class.cast(dataObj); + List documents = List.class.cast(dataMap.get("docs")); if (documents == null || documents.isEmpty()) { // RAGFlow 明确返回了空文档列表,这是合法的"真空" return new PageData<>(new ArrayList<>(), 0); @@ -679,7 +678,9 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter { dto.setChunkMethod(info.getChunkMethod().name().toLowerCase()); } if (info.getParserConfig() != null) { - dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(), Map.class)); + dto.setParserConfig(objectMapper.convertValue(info.getParserConfig(), + new TypeReference>() { + })); } return dto; @@ -717,4 +718,4 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter { return !multipartFile.isEmpty(); } } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java index 1e3e2513..79d935fb 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java @@ -20,6 +20,7 @@ import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; +import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.ToolUtil; import xiaozhi.modules.sys.dao.SysDictDataDao; import xiaozhi.modules.sys.dao.SysUserDao; @@ -170,7 +171,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl cachedData = (List) redisUtils.get(key); + List cachedData = JsonUtils.toList(redisUtils.get(key), SysDictDataItem.class); if (cachedData != null) { return cachedData; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java index a83c2e68..b8bc796a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java @@ -287,10 +287,10 @@ public class SysParamsServiceImpl extends BaseServiceImpl currentFeatures = (Map) currentMap.get("features"); - Map newFeatures = (Map) newMap.get("features"); + Map currentFeatures = Map.class.cast(currentMap.get("features")); + Map newFeatures = Map.class.cast(newMap.get("features")); if (currentFeatures != null && newFeatures != null) { Object currentAddressBookObj = currentFeatures.get("addressBook"); @@ -308,16 +308,14 @@ public class SysParamsServiceImpl extends BaseServiceImpl currentAddressBook = (Map) currentAddressBookObj; - currentEnabled = currentAddressBook.get("enabled") != null - ? (Boolean) currentAddressBook.get("enabled") : false; + if (currentAddressBookObj instanceof Map currentAddressBook) { + Object enabled = currentAddressBook.get("enabled"); + currentEnabled = enabled != null ? Boolean.class.cast(enabled) : false; } - if (newAddressBookObj instanceof Map) { - Map newAddressBook = (Map) newAddressBookObj; - newEnabled = newAddressBook.get("enabled") != null - ? (Boolean) newAddressBook.get("enabled") : false; + if (newAddressBookObj instanceof Map newAddressBook) { + Object enabled = newAddressBook.get("enabled"); + newEnabled = enabled != null ? Boolean.class.cast(enabled) : false; } // 如果之前是启用状态,现在被禁用,删除所有call_device插件 diff --git a/main/manager-api/src/test/java/xiaozhi/common/redis/RedisSerializationTest.java b/main/manager-api/src/test/java/xiaozhi/common/redis/RedisSerializationTest.java new file mode 100644 index 00000000..26610ae5 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/common/redis/RedisSerializationTest.java @@ -0,0 +1,57 @@ +package xiaozhi.common.redis; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.serializer.RedisSerializer; + +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.modules.sys.vo.SysDictDataItem; + +class RedisSerializationTest { + private final RedisSerializer serializer = RedisSerializer.json(); + + @Test + void serverConfigRoundTripRestoresStringKeyedMaps() { + Map features = new HashMap<>(); + features.put("addressBook", Map.of("enabled", true)); + Map config = new HashMap<>(); + config.put("features", features); + + Object restored = roundTrip(config); + Map restoredConfig = JsonUtils.toStringObjectMap(restored); + Map restoredFeatures = JsonUtils.toStringObjectMap(restoredConfig.get("features")); + Map addressBook = JsonUtils.toStringObjectMap(restoredFeatures.get("addressBook")); + + assertEquals(true, addressBook.get("enabled")); + } + + @Test + void dictionaryListRoundTripRestoresDtoElements() { + SysDictDataItem item = new SysDictDataItem(); + item.setName("enabled"); + item.setKey("1"); + + Object restored = roundTrip(new ArrayList<>(List.of(item))); + List restoredItems = JsonUtils.toList(restored, SysDictDataItem.class); + + assertInstanceOf(SysDictDataItem.class, restoredItems.get(0)); + assertEquals("enabled", restoredItems.get(0).getName()); + assertEquals("1", restoredItems.get(0).getKey()); + } + + private Object roundTrip(Object value) { + byte[] bytes = serializer.serialize(value); + assertNotNull(bytes); + Object restored = serializer.deserialize(bytes); + assertNotNull(restored); + return restored; + } +} diff --git a/main/manager-api/src/test/java/xiaozhi/common/utils/JsonUtilsTest.java b/main/manager-api/src/test/java/xiaozhi/common/utils/JsonUtilsTest.java new file mode 100644 index 00000000..c740ad64 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/common/utils/JsonUtilsTest.java @@ -0,0 +1,77 @@ +package xiaozhi.common.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class JsonUtilsTest { + + @Test + void parsesTypedMapsAndPreservesNestedCollectionShapes() { + Map map = JsonUtils.parseMap( + "{\"enabled\":true,\"nested\":{\"items\":[{\"name\":\"tool\"}]}}"); + Map nested = assertInstanceOf(Map.class, map.get("nested")); + List items = assertInstanceOf(List.class, nested.get("items")); + Map item = assertInstanceOf(Map.class, items.get(0)); + + assertEquals(true, map.get("enabled")); + assertEquals("tool", item.get("name")); + + List> maps = JsonUtils.parseMapList( + "[{\"name\":\"first\",\"values\":[1,2]},{\"name\":\"second\"}]"); + assertEquals("first", maps.get(0).get("name")); + assertEquals(List.of(1, 2), maps.get(0).get("values")); + assertEquals("second", maps.get(1).get("name")); + } + + @Test + void checkedConvertersAreShallowMutableCopies() { + List nested = new ArrayList<>(List.of(Map.of("name", "tool"))); + Map source = new LinkedHashMap<>(); + source.put("nested", nested); + + Map map = JsonUtils.toStringObjectMap(source); + List> maps = JsonUtils.toStringObjectMapList(List.of(source)); + List strings = JsonUtils.toList(Arrays.asList("a", null), String.class); + + assertSame(nested, map.get("nested")); + assertSame(nested, maps.get(0).get("nested")); + map.put("enabled", true); + maps.add(Map.of("name", "second")); + strings.add("b"); + assertEquals(true, map.get("enabled")); + assertEquals(2, maps.size()); + assertEquals(Arrays.asList("a", null, "b"), strings); + } + + @Test + void preservesNullInputs() { + assertNull(JsonUtils.parseMap("")); + assertNull(JsonUtils.parseMapList(null)); + assertNull(JsonUtils.toStringObjectMap(null)); + assertNull(JsonUtils.toStringObjectMapList(null)); + assertNull(JsonUtils.toList(null, String.class)); + } + + @Test + void rejectsUnexpectedJsonShapesAndRuntimeTypes() { + assertThrows(RuntimeException.class, () -> JsonUtils.parseMap("[]")); + assertThrows(RuntimeException.class, () -> JsonUtils.parseMapList("{}")); + assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMap(List.of())); + assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMap(Map.of(1, "value"))); + assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMapList(List.of("value"))); + assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMapList(Map.of())); + assertThrows(ClassCastException.class, () -> JsonUtils.toList(List.of(1), String.class)); + assertThrows(ClassCastException.class, () -> JsonUtils.toList(Map.of(), String.class)); + } +} diff --git a/main/manager-api/src/test/java/xiaozhi/modules/config/service/impl/ConfigServiceImplTest.java b/main/manager-api/src/test/java/xiaozhi/modules/config/service/impl/ConfigServiceImplTest.java new file mode 100644 index 00000000..1e76d4b7 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/config/service/impl/ConfigServiceImplTest.java @@ -0,0 +1,96 @@ +package xiaozhi.modules.config.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import xiaozhi.common.redis.RedisKeys; +import xiaozhi.common.redis.RedisUtils; +import xiaozhi.modules.agent.dao.AgentVoicePrintDao; +import xiaozhi.modules.agent.service.AgentContextProviderService; +import xiaozhi.modules.agent.service.AgentMcpAccessPointService; +import xiaozhi.modules.agent.service.AgentPluginMappingService; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.agent.service.AgentTemplateService; +import xiaozhi.modules.correctword.service.CorrectWordFileService; +import xiaozhi.modules.device.service.DeviceService; +import xiaozhi.modules.model.service.ModelConfigService; +import xiaozhi.modules.sys.dto.SysParamsDTO; +import xiaozhi.modules.sys.service.SysParamsService; +import xiaozhi.modules.timbre.service.TimbreService; +import xiaozhi.modules.voiceclone.service.VoiceCloneService; + +class ConfigServiceImplTest { + + @Test + void cachedServerConfigIsCheckedAsAStringKeyedMapWithoutChangingNestedValues() { + RedisUtils redisUtils = mock(RedisUtils.class); + Map nested = new HashMap<>(); + nested.put("enabled", true); + Map cached = new HashMap<>(); + cached.put("features", nested); + when(redisUtils.get(RedisKeys.getServerConfigKey())).thenReturn(cached); + + ConfigServiceImpl service = newService(mock(SysParamsService.class), redisUtils); + + Map result = service.getConfig(true); + + assertNotSame(cached, result); + assertSame(nested, result.get("features")); + assertEquals(true, ((Map) result.get("features")).get("enabled")); + } + + @Test + void nestedSystemParametersStillShareAndPopulateTheSameConfigBranch() { + SysParamsService sysParamsService = mock(SysParamsService.class); + SysParamsDTO enabled = parameter("server.features.enabled", "true", "boolean"); + SysParamsDTO labels = parameter("server.features.labels", "first;second", "array"); + when(sysParamsService.list(anyMap())).thenReturn(List.of(enabled, labels)); + ConfigServiceImpl service = newService(sysParamsService, mock(RedisUtils.class)); + Map config = new HashMap<>(); + + Object returned = ReflectionTestUtils.invokeMethod(service, "buildConfig", config); + + assertSame(config, returned); + Map server = assertInstanceOf(Map.class, config.get("server")); + Map features = assertInstanceOf(Map.class, server.get("features")); + assertEquals(true, features.get("enabled")); + assertEquals(List.of("first", "second"), features.get("labels")); + } + + private static SysParamsDTO parameter(String code, String value, String type) { + SysParamsDTO parameter = new SysParamsDTO(); + parameter.setParamCode(code); + parameter.setParamValue(value); + parameter.setValueType(type); + return parameter; + } + + private static ConfigServiceImpl newService(SysParamsService sysParamsService, RedisUtils redisUtils) { + return new ConfigServiceImpl( + sysParamsService, + mock(DeviceService.class), + mock(ModelConfigService.class), + mock(AgentService.class), + mock(AgentTemplateService.class), + redisUtils, + mock(TimbreService.class), + mock(AgentPluginMappingService.class), + mock(AgentMcpAccessPointService.class), + mock(AgentContextProviderService.class), + mock(VoiceCloneService.class), + mock(AgentVoicePrintDao.class), + mock(CorrectWordFileService.class)); + } +} diff --git a/main/manager-api/src/test/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImplTest.java b/main/manager-api/src/test/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImplTest.java new file mode 100644 index 00000000..13839690 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImplTest.java @@ -0,0 +1,35 @@ +package xiaozhi.modules.sys.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import xiaozhi.common.redis.RedisKeys; +import xiaozhi.common.redis.RedisUtils; +import xiaozhi.modules.sys.dao.SysUserDao; +import xiaozhi.modules.sys.vo.SysDictDataItem; + +class SysDictDataServiceImplTest { + + @Test + void cachedDictionaryItemsAreCheckedAndReturnedAsDtos() { + RedisUtils redisUtils = mock(RedisUtils.class); + SysDictDataItem item = new SysDictDataItem(); + item.setName("enabled"); + item.setKey("1"); + List cached = new ArrayList<>(List.of(item)); + when(redisUtils.get(RedisKeys.getDictDataByTypeKey("status"))).thenReturn(cached); + SysDictDataServiceImpl service = new SysDictDataServiceImpl(mock(SysUserDao.class), redisUtils); + + List result = service.getDictDataByType("status"); + + assertNotSame(cached, result); + assertEquals(List.of(item), result); + } +} diff --git a/main/manager-api/src/test/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImplTest.java b/main/manager-api/src/test/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImplTest.java new file mode 100644 index 00000000..cf12ee01 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImplTest.java @@ -0,0 +1,36 @@ +package xiaozhi.modules.sys.service.impl; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import xiaozhi.common.constant.Constant; +import xiaozhi.modules.agent.service.AgentPluginMappingService; +import xiaozhi.modules.sys.dao.SysParamsDao; +import xiaozhi.modules.sys.redis.SysParamsRedis; + +class SysParamsServiceImplTest { + + @Test + void disablingAddressBookStillDeletesItsSystemPlugin() { + SysParamsRedis sysParamsRedis = mock(SysParamsRedis.class); + AgentPluginMappingService pluginMappingService = mock(AgentPluginMappingService.class); + SysParamsDao sysParamsDao = mock(SysParamsDao.class); + SysParamsServiceImpl service = new SysParamsServiceImpl(sysParamsRedis, pluginMappingService); + ReflectionTestUtils.setField(service, "baseDao", sysParamsDao); + + String currentConfig = "{\"features\":{\"addressBook\":{\"enabled\":true}}}"; + String newConfig = "{\"features\":{\"addressBook\":{\"enabled\":false}}}"; + when(sysParamsDao.getValueByCode(Constant.SYSTEM_WEB_MENU)).thenReturn(currentConfig); + when(sysParamsDao.updateValueByCode(Constant.SYSTEM_WEB_MENU, newConfig)).thenReturn(1); + + service.updateSystemWebMenu(newConfig); + + verify(pluginMappingService).deleteByPluginId("SYSTEM_PLUGIN_CALL_DEVICE"); + verify(sysParamsDao).updateValueByCode(Constant.SYSTEM_WEB_MENU, newConfig); + verify(sysParamsRedis).set(Constant.SYSTEM_WEB_MENU, newConfig); + } +}