Compare commits

...
Author SHA1 Message Date
Tyke Chen 4a2c90ec87 refactor(manager-api): 升级常用工具依赖并收敛冗余实现 2026-07-24 09:06:15 +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
17 changed files with 245 additions and 205 deletions
+4 -4
View File
@@ -21,14 +21,14 @@
<junit.version>5.10.1</junit.version> <junit.version>5.10.1</junit.version>
<druid.version>1.2.20</druid.version> <druid.version>1.2.20</druid.version>
<mybatisplus.version>3.5.17</mybatisplus.version> <mybatisplus.version>3.5.17</mybatisplus.version>
<hutool.version>5.8.24</hutool.version> <hutool.version>5.8.46</hutool.version>
<jsoup.version>1.19.1</jsoup.version> <jsoup.version>1.22.2</jsoup.version>
<knife4j.version>4.6.0</knife4j.version> <knife4j.version>4.6.0</knife4j.version>
<springdoc.version>2.8.8</springdoc.version> <springdoc.version>2.8.8</springdoc.version>
<commons-lang3.version>3.18.0</commons-lang3.version> <commons-lang3.version>3.20.0</commons-lang3.version>
<shiro.version>2.0.2</shiro.version> <shiro.version>2.0.2</shiro.version>
<captcha.version>1.6.2</captcha.version> <captcha.version>1.6.2</captcha.version>
<guava.version>33.0.0-jre</guava.version> <guava.version>33.6.0-jre</guava.version>
<liquibase-core.version>4.20.0</liquibase-core.version> <liquibase-core.version>4.20.0</liquibase-core.version>
<aliyun-sms-version>4.1.0</aliyun-sms-version> <aliyun-sms-version>4.1.0</aliyun-sms-version>
<okio-version>3.4.0</okio-version> <okio-version>3.4.0</okio-version>
@@ -1,52 +0,0 @@
package xiaozhi.common.utils;
import lombok.extern.slf4j.Slf4j;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 哈希加密算法的工具类
* @author zjy
*/
@Slf4j
public class HashEncryptionUtil {
/**
* 使用md5进行加密
* @param context 被加密的内容
* @return 哈希值
*/
public static String Md5hexDigest(String context){
return hexDigest(context,"MD5");
}
/**
* 指定哈希算法进行加密
* @param context 被加密的内容
* @param algorithm 哈希算法
* @return 哈希值
*/
public static String hexDigest(String context,String algorithm ){
// 获取MD5算法实例
MessageDigest md = null;
try {
md = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
log.error("加密失败的算法:{}",algorithm);
throw new RuntimeException("加密失败,"+ algorithm +"哈希算法系统不支持");
}
// 计算智能体id的MD5值
byte[] messageDigest = md.digest(context.getBytes());
// 将字节数组转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
@@ -1,89 +0,0 @@
package xiaozhi.common.utils;
import cn.hutool.core.util.ReUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 通用工具类
*/
public class ToolUtil {
private static final Logger logger = LoggerFactory.getLogger(ToolUtil.class);
/**
* 对象是否不为空(新增)
*/
public static boolean isNotEmpty(Object o) {
return !isEmpty(o);
}
/**
* 对象是否为空
*/
public static boolean isEmpty(Object o) {
if (o == null) {
return true;
}
if (o instanceof String) {
if (o.toString().trim().equals("")) {
return true;
}
} else if (o instanceof List) {
if (((List) o).size() == 0) {
return true;
}
} else if (o instanceof Map) {
if (((Map) o).size() == 0) {
return true;
}
} else if (o instanceof Set) {
if (((Set) o).size() == 0) {
return true;
}
} else if (o instanceof Object[]) {
if (((Object[]) o).length == 0) {
return true;
}
} else if (o instanceof int[]) {
if (((int[]) o).length == 0) {
return true;
}
} else if (o instanceof long[]) {
if (((long[]) o).length == 0) {
return true;
}
}
return false;
}
/**
* 对象组中是否存在空对象
*/
public static boolean isOneEmpty(Object... os) {
for (Object o : os) {
if (isEmpty(o)) {
return true;
}
}
return false;
}
/**
* 对象组中是否全是空对象
*/
public static boolean isAllEmpty(Object... os) {
for (Object o : os) {
if (!isEmpty(o)) {
return false;
}
}
return true;
}
}
@@ -5,6 +5,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil; import cn.hutool.core.collection.ListUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -20,7 +21,6 @@ import xiaozhi.common.constant.Constant;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.agent.Enums.AgentChatHistoryType; import xiaozhi.modules.agent.Enums.AgentChatHistoryType;
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao; import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
@@ -107,7 +107,7 @@ public class AgentChatHistoryServiceImpl extends CrudRepository<AiAgentChatHisto
if (deleteAudio) { if (deleteAudio) {
// 分批删除音频,避免超时 // 分批删除音频,避免超时
List<String> audioIds = baseMapper.getAudioIdsByAgentId(agentId); List<String> audioIds = baseMapper.getAudioIdsByAgentId(agentId);
if (ToolUtil.isNotEmpty(audioIds)) { if (CollUtil.isNotEmpty(audioIds)) {
// 每批删除1000条 // 每批删除1000条
List<List<String>> batch = ListUtil.split(audioIds, 1000); List<List<String>> batch = ListUtil.split(audioIds, 1000);
batch.forEach(dataList -> { batch.forEach(dataList -> {
@@ -12,11 +12,11 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.utils.AESUtils; import xiaozhi.common.utils.AESUtils;
import xiaozhi.common.utils.HashEncryptionUtil;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.Enums.XiaoZhiMcpJsonRpcJson; import xiaozhi.modules.agent.Enums.XiaoZhiMcpJsonRpcJson;
import xiaozhi.modules.agent.service.AgentMcpAccessPointService; import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
@@ -226,7 +226,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
*/ */
private static String encryptToken(String agentId, String key) { private static String encryptToken(String agentId, String key) {
// 使用md5对智能体id进行加密 // 使用md5对智能体id进行加密
String md5 = HashEncryptionUtil.Md5hexDigest(agentId); String md5 = DigestUtil.md5Hex(agentId);
// aes需要加密文本 // aes需要加密文本
String json = "{\"agentId\": \"%s\"}".formatted(md5); String json = "{\"agentId\": \"%s\"}".formatted(md5);
// 加密后成token值 // 加密后成token值
@@ -18,6 +18,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.repository.IRepository; import com.baomidou.mybatisplus.extension.repository.IRepository;
import cn.hutool.core.collection.CollUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
@@ -29,7 +30,6 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.agent.dao.AgentDao; import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.dao.AgentTagDao; import xiaozhi.modules.agent.dao.AgentTagDao;
import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentCreateDTO;
@@ -243,13 +243,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
.map(DeviceEntity::getAgentId) .map(DeviceEntity::getAgentId)
.distinct() .distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
if (ToolUtil.isNotEmpty(agentIds)) { if (CollUtil.isNotEmpty(agentIds)) {
w.or().in("id", agentIds); w.or().in("id", agentIds);
} }
// 按标签名搜索 // 按标签名搜索
List<String> tagAgentIds = agentTagService.getAgentIdsByTagName(keyword); List<String> tagAgentIds = agentTagService.getAgentIdsByTagName(keyword);
if (ToolUtil.isNotEmpty(tagAgentIds)) { if (CollUtil.isNotEmpty(tagAgentIds)) {
w.or().in("id", tagAgentIds); w.or().in("id", tagAgentIds);
} }
}); });
@@ -291,7 +291,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 获取标签列表 // 获取标签列表
List<AgentTagEntity> tags = agentTagDao.selectByAgentId(agent.getId()); List<AgentTagEntity> tags = agentTagDao.selectByAgentId(agent.getId());
if (ToolUtil.isNotEmpty(tags)) { if (CollUtil.isNotEmpty(tags)) {
dto.setTags(tags.stream().map(this::convertTagToDTO).collect(Collectors.toList())); dto.setTags(tags.stream().map(this::convertTagToDTO).collect(Collectors.toList()));
} }
@@ -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>();
} }
@@ -5,8 +5,6 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -31,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import cn.hutool.crypto.digest.DigestUtil;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.Parameters;
@@ -288,7 +287,7 @@ public class OTAMagController {
// 返回文件路径 // 返回文件路径
return new Result<String>().ok(filePath.toString()); return new Result<String>().ok(filePath.toString());
} catch (IOException | NoSuchAlgorithmException e) { } catch (IOException e) {
return new Result<String>().error("文件上传失败:" + e.getMessage()); return new Result<String>().error("文件上传失败:" + e.getMessage());
} }
} }
@@ -329,13 +328,7 @@ public class OTAMagController {
return result; return result;
} }
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException { private String calculateMD5(MultipartFile file) throws IOException {
MessageDigest md = MessageDigest.getInstance("MD5"); return DigestUtil.md5Hex(file.getBytes());
byte[] digest = md.digest(file.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} }
} }
@@ -31,6 +31,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@@ -51,7 +52,6 @@ import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils; import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.device.dao.DeviceDao; import xiaozhi.modules.device.dao.DeviceDao;
import xiaozhi.modules.device.dto.DeviceManualAddDTO; import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DevicePageUserDTO; import xiaozhi.modules.device.dto.DevicePageUserDTO;
@@ -103,15 +103,15 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY); throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
} }
String deviceKey = RedisKeys.getOtaActivationCode(activationCode); String deviceKey = RedisKeys.getOtaActivationCode(activationCode);
Object cacheDeviceId = redisUtils.get(deviceKey); String cacheDeviceId = (String) redisUtils.get(deviceKey);
if (ToolUtil.isEmpty(cacheDeviceId)) { if (StringUtils.isBlank(cacheDeviceId)) {
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
} }
String deviceId = (String) cacheDeviceId; String deviceId = cacheDeviceId;
String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId); String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(cacheDeviceKey)); Map<String, Object> cacheMap = JsonUtils.toStringObjectMap(redisUtils.get(cacheDeviceKey));
if (ToolUtil.isEmpty(cacheMap)) { if (MapUtil.isEmpty(cacheMap)) {
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
} }
String cachedCode = (String) cacheMap.get("activation_code"); String cachedCode = (String) cacheMap.get("activation_code");
@@ -181,7 +181,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
.builder(new HashMap<String, Set<String>>()) .builder(new HashMap<String, Set<String>>())
.put("clientIds", deviceIds).build(); .put("clientIds", deviceIds).build();
if (ToolUtil.isNotEmpty(deviceIds)) { if (CollUtil.isNotEmpty(deviceIds)) {
return postToMqttGateway(url, params); return postToMqttGateway(url, params);
} }
// 返回响应 // 返回响应
@@ -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;
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import cn.hutool.core.collection.CollUtil;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -23,7 +24,6 @@ import xiaozhi.common.exception.ErrorCode;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO; import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
import xiaozhi.modules.knowledge.service.KnowledgeBaseService; import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
import xiaozhi.modules.knowledge.service.KnowledgeManagerService; import xiaozhi.modules.knowledge.service.KnowledgeManagerService;
@@ -140,7 +140,7 @@ public class KnowledgeBaseController {
List<String> idList = Arrays.asList(ids.split(",")); List<String> idList = Arrays.asList(ids.split(","));
List<KnowledgeBaseDTO> knowledgeBaseDTOs = Optional.ofNullable(knowledgeBaseService.getByDatasetIdList(idList)) List<KnowledgeBaseDTO> knowledgeBaseDTOs = Optional.ofNullable(knowledgeBaseService.getByDatasetIdList(idList))
.orElseGet(ArrayList::new); .orElseGet(ArrayList::new);
if (ToolUtil.isNotEmpty(knowledgeBaseDTOs)) { if (CollUtil.isNotEmpty(knowledgeBaseDTOs)) {
knowledgeBaseDTOs.forEach(item -> { knowledgeBaseDTOs.forEach(item -> {
// 检查权限:用户只能删除自己创建的知识库 // 检查权限:用户只能删除自己创建的知识库
if (item.getCreator() == null || !item.getCreator().equals(currentUserId)) { if (item.getCreator() == null || !item.getCreator().equals(currentUserId)) {
@@ -1,8 +1,7 @@
package xiaozhi.modules.security.service.impl; package xiaozhi.modules.security.service.impl;
import java.io.IOException; import java.io.IOException;
import java.util.Random; import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -13,6 +12,7 @@ import com.google.common.cache.CacheBuilder;
import com.wf.captcha.SpecCaptcha; import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha; import com.wf.captcha.base.Captcha;
import cn.hutool.core.util.RandomUtil;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
@@ -41,7 +41,7 @@ public class CaptchaServiceImpl implements CaptchaService {
* Local Cache 5分钟过期 * Local Cache 5分钟过期
*/ */
Cache<String, String> localCache = CacheBuilder.newBuilder().maximumSize(1000) Cache<String, String> localCache = CacheBuilder.newBuilder().maximumSize(1000)
.expireAfterAccess(5, TimeUnit.MINUTES).build(); .expireAfterAccess(Duration.ofMinutes(5)).build();
@Override @Override
public void create(HttpServletResponse response, String uuid) throws IOException { public void create(HttpServletResponse response, String uuid) throws IOException {
@@ -113,7 +113,7 @@ public class CaptchaServiceImpl implements CaptchaService {
} }
String key = RedisKeys.getSMSValidateCodeKey(phone); String key = RedisKeys.getSMSValidateCodeKey(phone);
String validateCodes = generateValidateCode(6); String validateCodes = RandomUtil.randomNumbers(6);
// 设置验证码 // 设置验证码
setCache(key, validateCodes); setCache(key, validateCodes);
@@ -135,22 +135,6 @@ public class CaptchaServiceImpl implements CaptchaService {
return validate(key, code, delete); return validate(key, code, delete);
} }
/**
* 生成指定数量的随机数验证码
*
* @param length 数量
* @return 随机码
*/
private String generateValidateCode(Integer length) {
String chars = "0123456789"; // 字符范围可以自定义:数字
Random random = new Random();
StringBuilder code = new StringBuilder();
for (int i = 0; i < length; i++) {
code.append(chars.charAt(random.nextInt(chars.length())));
}
return code.toString();
}
private void setCache(String key, String value) { private void setCache(String key, String value) {
if (open) { if (open) {
key = RedisKeys.getCaptchaKey(key); key = RedisKeys.getCaptchaKey(key);
@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import cn.hutool.core.collection.CollUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.ErrorCode;
@@ -21,7 +22,6 @@ import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.sys.dao.SysDictDataDao; import xiaozhi.modules.sys.dao.SysDictDataDao;
import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dao.SysUserDao;
import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.dto.SysDictDataDTO;
@@ -104,13 +104,13 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void delete(Long[] ids) { public void delete(Long[] ids) {
List<Long> idList = Arrays.asList(ids); List<Long> idList = Arrays.asList(ids);
if (ToolUtil.isNotEmpty(idList)) { if (CollUtil.isNotEmpty(idList)) {
//批量删除redis字典 //批量删除redis字典
List<String> redisKeyList = new ArrayList<>(); List<String> redisKeyList = new ArrayList<>();
//批量获取字典类型 //批量获取字典类型
List<String> dictTypeList = Optional.ofNullable(baseDao.getDictTypesByIdList(idList)).orElseGet(ArrayList::new); List<String> dictTypeList = Optional.ofNullable(baseDao.getDictTypesByIdList(idList)).orElseGet(ArrayList::new);
dictTypeList.forEach(dictType -> redisKeyList.add(RedisKeys.getDictDataByTypeKey(dictType))); dictTypeList.forEach(dictType -> redisKeyList.add(RedisKeys.getDictDataByTypeKey(dictType)));
if (ToolUtil.isNotEmpty(redisKeyList)) { if (CollUtil.isNotEmpty(redisKeyList)) {
//清除缓存 //清除缓存
redisUtils.delete(redisKeyList); redisUtils.delete(redisKeyList);
} }
@@ -17,6 +17,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import cn.hutool.core.collection.CollUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
@@ -26,7 +27,6 @@ import xiaozhi.common.page.PageData;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.DateUtils; import xiaozhi.common.utils.DateUtils;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.model.entity.ModelConfigEntity; import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dao.SysUserDao;
@@ -120,7 +120,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
entity.setTrainStatus(0); // 默认训练中 entity.setTrainStatus(0); // 默认训练中
batchInsertList.add(entity); batchInsertList.add(entity);
} }
if (ToolUtil.isNotEmpty(batchInsertList)) { if (CollUtil.isNotEmpty(batchInsertList)) {
insertBatch(batchInsertList); insertBatch(batchInsertList);
} }
} }
@@ -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