mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23:23:55 +08:00
update:更新manager-api模块
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package xiaozhi.common.config;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableAspectJAutoProxy(exposeProxy = true)
|
||||
public class AsyncConfig {
|
||||
|
||||
@Bean(name = "taskExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setMaxPoolSize(4);
|
||||
executor.setQueueCapacity(1000);
|
||||
executor.setThreadNamePrefix("AsyncThread-");
|
||||
// 设置拒绝策略:由调用线程执行
|
||||
executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
|
||||
@Override
|
||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||
try {
|
||||
// 如果线程池已满,则由调用线程执行
|
||||
r.run();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("执行异步任务失败", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package xiaozhi.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* RestTemplate配置
|
||||
*/
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,14 @@ public class SwaggerConfig {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi configApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("config")
|
||||
.pathsToMatch("/config/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI().info(new Info()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package xiaozhi.common.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 常量
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
@@ -74,38 +76,59 @@ public interface Constant {
|
||||
*/
|
||||
String ORDER = "order";
|
||||
|
||||
/**
|
||||
* 请求头授权标识
|
||||
*/
|
||||
String AUTHORIZATION = "Authorization";
|
||||
|
||||
/**
|
||||
* 服务器密钥
|
||||
*/
|
||||
String SERVER_SECRET = "server.secret";
|
||||
|
||||
/**
|
||||
* websocket地址
|
||||
*/
|
||||
String SERVER_WEBSOCKET = "server.websocket";
|
||||
|
||||
/**
|
||||
* ota地址
|
||||
*/
|
||||
String SERVER_OTA = "server.ota";
|
||||
|
||||
/**
|
||||
* 是否允许用户注册
|
||||
*/
|
||||
String SERVER_ALLOW_USER_REGISTER = "server.allow_user_register";
|
||||
|
||||
/**
|
||||
* 下发六位验证码时显示的控制面板地址
|
||||
*/
|
||||
String SERVER_FRONTED_URL = "server.fronted_url";
|
||||
|
||||
/**
|
||||
* 路径分割符
|
||||
*/
|
||||
String FILE_EXTENSION_SEG = ".";
|
||||
|
||||
/**
|
||||
* 无记忆
|
||||
*/
|
||||
String MEMORY_NO_MEM = "Memory_nomem";
|
||||
|
||||
enum SysBaseParam {
|
||||
/**
|
||||
* 系统全称
|
||||
* ICP备案号
|
||||
*/
|
||||
SYS_NAME("SYS_NAME"),
|
||||
BEIAN_ICP_NUM("server.beian_icp_num"),
|
||||
/**
|
||||
* 系统简称
|
||||
* GA备案号
|
||||
*/
|
||||
SYS_SHORT_NAME("SYS_SHORT_NAME"),
|
||||
BEIAN_GA_NUM("server.beian_ga_num"),
|
||||
/**
|
||||
* 系统描述
|
||||
* 系统名称
|
||||
*/
|
||||
SYS_DES("SYS_DES"),
|
||||
/**
|
||||
* 登录失败几次锁定
|
||||
*/
|
||||
LOGIN_LOCK_COUNT("LOGIN_LOCK_COUNT"),
|
||||
/**
|
||||
* 账号失败锁定分钟数
|
||||
*/
|
||||
LOGIN_LOCK_TIME("LOGIN_LOCK_TIME"),
|
||||
/**
|
||||
* TOKEN强验证
|
||||
*/
|
||||
SYS_TOKEN_SECURITY("SYS_TOKEN_SECURITY");
|
||||
SERVER_NAME("server.name");
|
||||
|
||||
private String value;
|
||||
|
||||
@@ -118,6 +141,46 @@ public interface Constant {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统短信
|
||||
*/
|
||||
enum SysMSMParam {
|
||||
/**
|
||||
* 阿里云授权keyID
|
||||
*/
|
||||
ALIYUN_SMS_ACCESS_KEY_ID("aliyun.sms.access_key_id"),
|
||||
/**
|
||||
* 阿里云授权密钥
|
||||
*/
|
||||
ALIYUN_SMS_ACCESS_KEY_SECRET("aliyun.sms.access_key_secret"),
|
||||
/**
|
||||
* 阿里云短信签名
|
||||
*/
|
||||
ALIYUN_SMS_SIGN_NAME("aliyun.sms.sign_name"),
|
||||
/**
|
||||
* 阿里云短信模板
|
||||
*/
|
||||
ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE("aliyun.sms.sms_code_template_code"),
|
||||
/**
|
||||
* 单号码最大短信发送条数
|
||||
*/
|
||||
SERVER_SMS_MAX_SEND_COUNT("server.sms_max_send_count"),
|
||||
/**
|
||||
* 是否开启手机注册
|
||||
*/
|
||||
SERVER_ENABLE_MOBILE_REGISTER("server.enable_mobile_register");
|
||||
|
||||
private String value;
|
||||
|
||||
SysMSMParam(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据状态
|
||||
*/
|
||||
@@ -145,4 +208,49 @@ public interface Constant {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
enum ChatHistoryConfEnum {
|
||||
IGNORE(0, "不记录"),
|
||||
RECORD_TEXT(1, "记录文本"),
|
||||
RECORD_TEXT_AUDIO(2, "文本音频都记录");
|
||||
|
||||
private final int code;
|
||||
private final String name;
|
||||
|
||||
ChatHistoryConfEnum(int code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.4.4";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
*/
|
||||
String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL";
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
enum DictType {
|
||||
/**
|
||||
* 手机区号
|
||||
*/
|
||||
MOBILE_AREA("MOBILE_AREA");
|
||||
|
||||
private String value;
|
||||
|
||||
DictType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public interface ErrorCode {
|
||||
int ACCOUNT_DISABLE = 10005;
|
||||
int IDENTIFIER_NOT_NULL = 10006;
|
||||
int CAPTCHA_ERROR = 10007;
|
||||
int SUB_MENU_EXIST = 10008;
|
||||
int PHONE_NOT_NULL = 10008;
|
||||
int PASSWORD_ERROR = 10009;
|
||||
|
||||
int SUPERIOR_DEPT_ERROR = 10011;
|
||||
@@ -43,4 +43,16 @@ public interface ErrorCode {
|
||||
int PASSWORD_WEAK_ERROR = 10031;
|
||||
int DEL_MYSELF_ERROR = 10032;
|
||||
int DEVICE_CAPTCHA_ERROR = 10033;
|
||||
|
||||
// 参数校验相关错误码
|
||||
int PARAM_VALUE_NULL = 10034;
|
||||
int PARAM_TYPE_NULL = 10035;
|
||||
int PARAM_TYPE_INVALID = 10036;
|
||||
int PARAM_NUMBER_INVALID = 10037;
|
||||
int PARAM_BOOLEAN_INVALID = 10038;
|
||||
int PARAM_ARRAY_INVALID = 10039;
|
||||
int PARAM_JSON_INVALID = 10040;
|
||||
|
||||
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||
int OTA_DEVICE_NEED_BIND = 10042;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package xiaozhi.common.exception;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
@@ -60,4 +66,20 @@ public class RenExceptionHandler {
|
||||
return new Result<Void>().error(404, "资源不存在");
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
||||
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
|
||||
String errorMsg = allErrors.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(err -> {
|
||||
String msg = err.getDefaultMessage();
|
||||
return (msg != null && !msg.trim().isEmpty()) ? msg : null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse("请求参数错误!");
|
||||
|
||||
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,7 +38,14 @@ public class RedisKeys {
|
||||
* 模型名称的Key
|
||||
*/
|
||||
public static String getModelNameById(String id) {
|
||||
return "sys:model:name:" + id;
|
||||
return "model:name:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型配置的Key
|
||||
*/
|
||||
public static String getModelConfigById(String id) {
|
||||
return "model:data:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,4 +61,82 @@ public class RedisKeys {
|
||||
public static String getAgentDeviceCountById(String id) {
|
||||
return "agent:device:count:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体最后连接时间缓存key
|
||||
*/
|
||||
public static String getAgentDeviceLastConnectedAtById(String id) {
|
||||
return "agent:device:lastConnected:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置缓存key
|
||||
*/
|
||||
public static String getServerConfigKey() {
|
||||
return "server:config";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音色详情缓存key
|
||||
*/
|
||||
public static String getTimbreDetailsKey(String id) {
|
||||
return "timbre:details:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本号Key
|
||||
*/
|
||||
public static String getVersionKey() {
|
||||
return "sys:version";
|
||||
}
|
||||
|
||||
/**
|
||||
* OTA固件ID的Key
|
||||
*/
|
||||
public static String getOtaIdKey(String uuid) {
|
||||
return "ota:id:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* OTA固件下载次数的Key
|
||||
*/
|
||||
public static String getOtaDownloadCountKey(String uuid) {
|
||||
return "ota:download:count:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据的缓存key
|
||||
*/
|
||||
public static String getDictDataByTypeKey(String dictType) {
|
||||
return "sys:dict:data:" + dictType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体音频ID的缓存key
|
||||
*/
|
||||
public static String getAgentAudioIdKey(String uuid) {
|
||||
return "agent:audio:id:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码的缓存key
|
||||
*/
|
||||
public static String getSMSValidateCodeKey(String phone) {
|
||||
return "sms:Validate:Code:" + phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码最后发送时间的缓存key
|
||||
*/
|
||||
public static String getSMSLastSendTimeKey(String phone) {
|
||||
return "sms:Validate:Code:" + phone + ":last_send_time";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码今日发送次数的缓存key
|
||||
*/
|
||||
public static String getSMSTodayCountKey(String phone) {
|
||||
return "sms:Validate:Code:" + phone + ":today_count";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package xiaozhi.common.redis;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import xiaozhi.common.utils.ResourcesUtils;
|
||||
|
||||
/**
|
||||
* Redis工具类
|
||||
@@ -20,6 +25,9 @@ public class RedisUtils {
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private ResourcesUtils resourceUtils;
|
||||
|
||||
/**
|
||||
* 默认过期时长为24小时,单位:秒
|
||||
*/
|
||||
@@ -27,7 +35,7 @@ public class RedisUtils {
|
||||
/**
|
||||
* 过期时长为1小时,单位:秒
|
||||
*/
|
||||
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
|
||||
public final static long HOUR_ONE_EXPIRE = (long) 60 * 60;
|
||||
/**
|
||||
* 过期时长为6小时,单位:秒
|
||||
*/
|
||||
@@ -37,6 +45,24 @@ public class RedisUtils {
|
||||
*/
|
||||
public final static long NOT_EXPIRE = -1L;
|
||||
|
||||
public Long increment(String key, long expire) {
|
||||
Long increment = redisTemplate.opsForValue().increment(key, 1L);
|
||||
if (expire != NOT_EXPIRE) {
|
||||
expire(key, expire);
|
||||
}
|
||||
return increment;
|
||||
}
|
||||
|
||||
public Long increment(String key) {
|
||||
return redisTemplate.opsForValue().increment(key, 1L);
|
||||
}
|
||||
|
||||
public Long decrement(String key) {
|
||||
return redisTemplate.opsForValue().decrement(key, 1L);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void set(String key, Object value, long expire) {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
if (expire != NOT_EXPIRE) {
|
||||
@@ -124,4 +150,46 @@ public class RedisUtils {
|
||||
public Object rightPop(String key) {
|
||||
return redisTemplate.opsForList().rightPop(key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清空所有 Redis 数据库中的所有键
|
||||
*/
|
||||
public void emptyAll() {
|
||||
// Lua 脚本 FLUSHALL是redis清空所有库的命令
|
||||
String luaScript =resourceUtils.loadString("lua/emptyAll.lua");
|
||||
|
||||
// 创建 DefaultRedisScript 对象
|
||||
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
|
||||
redisScript.setScriptText(luaScript); // 设置 Lua 脚本内容
|
||||
redisScript.setResultType(Void.class); // 设置返回值类型
|
||||
|
||||
// 执行 Lua 脚本
|
||||
List<String> keys = Collections.emptyList(); // 如果脚本不依赖 key,可以传入空列表
|
||||
redisTemplate.execute(redisScript, keys);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在redis指定key的值,如果值为空,着设置key的默认值
|
||||
* @param key redis的key
|
||||
* @param defaultValue 默认值
|
||||
* @param expiresInSecond 过期时间
|
||||
* @return 返回key的值
|
||||
*/
|
||||
public String getKeyOrCreate(String key, String defaultValue,Long expiresInSecond) {
|
||||
// Lua 脚本
|
||||
String luaScript = resourceUtils.loadString("lua/getKeyOrCreate.lua");
|
||||
|
||||
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
|
||||
redisScript.setScriptText(luaScript);
|
||||
redisScript.setResultType(String.class);
|
||||
|
||||
// 执行 Lua 脚本
|
||||
List<String> keys = Collections.singletonList(key);
|
||||
return redisTemplate.execute(redisScript, keys, defaultValue,expiresInSecond);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.common.service.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -18,6 +19,7 @@ import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
@@ -45,6 +47,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
* @param params 分页查询参数
|
||||
* @param defaultOrderField 默认排序字段
|
||||
* @param isAsc 排序方式
|
||||
* @see xiaozhi.common.constant.Constant
|
||||
* params.put(Constant.PAGE, "1");
|
||||
* params.put(Constant.LIMIT, "10");
|
||||
* params.put(Constant.ORDER_FIELD, "field"); // 单个字段
|
||||
* params.put(Constant.ORDER_FIELD, List.of("field1", "field2")); // 多个字段
|
||||
* params.put(Constant.ORDER, "asc");
|
||||
*/
|
||||
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
||||
// 分页参数
|
||||
@@ -65,28 +73,34 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
params.put(Constant.PAGE, page);
|
||||
|
||||
// 排序字段
|
||||
String orderField = (String) params.get(Constant.ORDER_FIELD);
|
||||
Object orderField = params.get(Constant.ORDER_FIELD);
|
||||
String order = (String) params.get(Constant.ORDER);
|
||||
|
||||
// 前端字段排序
|
||||
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
|
||||
if (Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.asc(orderField));
|
||||
List<String> orderFields = new ArrayList<>();
|
||||
|
||||
// 处理排序字段
|
||||
if (orderField instanceof String) {
|
||||
orderFields.add((String) orderField);
|
||||
} else if (orderField instanceof List) {
|
||||
orderFields.addAll((List<String>) orderField);
|
||||
}
|
||||
|
||||
// 有排序字段则排序
|
||||
if (CollectionUtils.isNotEmpty(orderFields)) {
|
||||
if (StringUtils.isNotBlank(order) && Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.ascs(orderFields.toArray(new String[0])));
|
||||
} else {
|
||||
return page.addOrder(OrderItem.desc(orderField));
|
||||
return page.addOrder(OrderItem.descs(orderFields.toArray(new String[0])));
|
||||
}
|
||||
}
|
||||
|
||||
// 没有排序字段,则不排序
|
||||
if (StringUtils.isBlank(defaultOrderField)) {
|
||||
return page;
|
||||
}
|
||||
|
||||
// 默认排序
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
// 没有排序字段,使用默认排序
|
||||
if (StringUtils.isNotBlank(defaultOrderField)) {
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
}
|
||||
}
|
||||
|
||||
return page;
|
||||
@@ -128,12 +142,10 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
return SqlHelper.retBool(result);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<M> currentMapperClass() {
|
||||
return (Class<M>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Class<T> currentModelClass() {
|
||||
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
|
||||
|
||||
@@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils;
|
||||
public abstract class CrudServiceImpl<M extends BaseMapper<T>, T, D> extends BaseServiceImpl<M, T>
|
||||
implements CrudService<T, D> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Class<D> currentDtoClass() {
|
||||
return (Class<D>) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ public class DateUtils {
|
||||
* 时间格式(yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
public final static String DATE_TIME_MILLIS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
|
||||
|
||||
/**
|
||||
* 日期格式化 日期格式为:yyyy-MM-dd
|
||||
@@ -63,6 +65,19 @@ public class DateUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static String getDateTimeNow() {
|
||||
return getDateTimeNow(DATE_TIME_PATTERN);
|
||||
}
|
||||
|
||||
public static String getDateTimeNow(String pattern) {
|
||||
return format(new Date(), pattern);
|
||||
}
|
||||
|
||||
public static String millsToSecond(long mills) {
|
||||
return String.format("%.3f", mills / 1000.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒
|
||||
* @param date
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* 资源处理工具
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ResourcesUtils {
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
/**
|
||||
* 读取资源,返回字符串
|
||||
* @param fileName 资源路径:resources下开始
|
||||
* @return 字符串
|
||||
*/
|
||||
public String loadString(String fileName) {
|
||||
Resource resource = resourceLoader.getResource("classpath:" + fileName);
|
||||
StringBuilder luaScriptBuilder = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(resource.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
luaScriptBuilder.append(line).append("\n");
|
||||
}
|
||||
} catch (IOException e){
|
||||
log.error("方法:loadString()读取资源失败--{}",e.getMessage());
|
||||
throw new RenException("读取资源失败");
|
||||
}
|
||||
return luaScriptBuilder.toString();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package xiaozhi.common.validator;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
@@ -45,4 +46,32 @@ public class ValidatorUtils {
|
||||
throw new RenException(constraint.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际手机号正则表达式
|
||||
* 要求必须带国际区号,格式:+[国家代码][手机号]
|
||||
* 例如:
|
||||
* - +8613800138000
|
||||
* - +12345678900
|
||||
* - +447123456789
|
||||
*/
|
||||
private static final String INTERNATIONAL_PHONE_REGEX = "^\\+[1-9]\\d{0,3}[1-9]\\d{4,14}$";
|
||||
|
||||
/**
|
||||
* 校验手机号是否有效
|
||||
* 要求必须带国际区号,格式:+[国家代码][手机号]
|
||||
* 例如:+8613800138000
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isValidPhone(String phone) {
|
||||
if (phone == null || phone.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证必须带国际区号的手机号格式
|
||||
Pattern pattern = Pattern.compile(INTERNATIONAL_PHONE_REGEX);
|
||||
return pattern.matcher(phone).matches();
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package xiaozhi.modules.agent.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||
|
||||
@Tag(name = "智能体聊天历史管理")
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/agent/chat-history")
|
||||
public class AgentChatHistoryController {
|
||||
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
||||
|
||||
/**
|
||||
* 小智服务聊天上报请求
|
||||
* <p>
|
||||
* 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。
|
||||
*
|
||||
* @param request 包含上传文件及相关信息的请求对象
|
||||
*/
|
||||
@Operation(summary = "小智服务聊天上报请求")
|
||||
@PostMapping("/report")
|
||||
public Result<Boolean> uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) {
|
||||
Boolean result = agentChatHistoryBizService.report(request);
|
||||
return new Result<Boolean>().ok(result);
|
||||
}
|
||||
}
|
||||
+121
-3
@@ -3,8 +3,13 @@ package xiaozhi.modules.agent.controller;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -25,16 +30,24 @@ import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@@ -46,6 +59,9 @@ public class AgentController {
|
||||
private final AgentService agentService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final DeviceService deviceService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取用户智能体列表")
|
||||
@@ -80,7 +96,7 @@ public class AgentController {
|
||||
@PostMapping
|
||||
@Operation(summary = "创建智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||
public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
||||
|
||||
// 获取默认模板
|
||||
@@ -95,6 +111,8 @@ public class AgentController {
|
||||
entity.setMemModelId(template.getMemModelId());
|
||||
entity.setIntentModelId(template.getIntentModelId());
|
||||
entity.setSystemPrompt(template.getSystemPrompt());
|
||||
entity.setSummaryMemory(template.getSummaryMemory());
|
||||
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||
entity.setLangCode(template.getLangCode());
|
||||
entity.setLanguage(template.getLanguage());
|
||||
}
|
||||
@@ -108,13 +126,29 @@ public class AgentController {
|
||||
// ID、智能体编码和排序会在Service层自动生成
|
||||
agentService.insert(entity);
|
||||
|
||||
return new Result<>();
|
||||
return new Result<String>().ok(entity.getId());
|
||||
}
|
||||
|
||||
@PutMapping("/saveMemory/{macAddress}")
|
||||
@Operation(summary = "根据设备id更新智能体")
|
||||
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null) {
|
||||
return new Result<>();
|
||||
}
|
||||
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
||||
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
||||
return updateAgentById(device.getAgentId(), agentUpdateDTO);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
||||
return updateAgentById(id, dto);
|
||||
}
|
||||
|
||||
private Result<Void> updateAgentById(String id, AgentUpdateDTO dto) {
|
||||
// 先查询现有实体
|
||||
AgentEntity existingEntity = agentService.getAgentById(id);
|
||||
if (existingEntity == null) {
|
||||
@@ -152,6 +186,12 @@ public class AgentController {
|
||||
if (dto.getSystemPrompt() != null) {
|
||||
existingEntity.setSystemPrompt(dto.getSystemPrompt());
|
||||
}
|
||||
if (dto.getSummaryMemory() != null) {
|
||||
existingEntity.setSummaryMemory(dto.getSummaryMemory());
|
||||
}
|
||||
if (dto.getChatHistoryConf() != null) {
|
||||
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
|
||||
}
|
||||
if (dto.getLangCode() != null) {
|
||||
existingEntity.setLangCode(dto.getLangCode());
|
||||
}
|
||||
@@ -167,8 +207,16 @@ public class AgentController {
|
||||
existingEntity.setUpdater(user.getId());
|
||||
existingEntity.setUpdatedAt(new Date());
|
||||
|
||||
// 更新记忆策略
|
||||
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
// 删除所有记录
|
||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
|
||||
existingEntity.setSummaryMemory("");
|
||||
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
|
||||
// 删除音频数据
|
||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
||||
}
|
||||
agentService.updateById(existingEntity);
|
||||
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -178,6 +226,8 @@ public class AgentController {
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
// 先删除关联的设备
|
||||
deviceService.deleteByAgentId(id);
|
||||
// 删除关联的聊天记录
|
||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
return new Result<>();
|
||||
@@ -191,4 +241,72 @@ public class AgentController {
|
||||
.list(new QueryWrapper<AgentTemplateEntity>().orderByAsc("sort"));
|
||||
return new Result<List<AgentTemplateEntity>>().ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/sessions")
|
||||
@Operation(summary = "获取智能体会话列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||
})
|
||||
public Result<PageData<AgentChatSessionDTO>> getAgentSessions(
|
||||
@PathVariable("id") String id,
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
params.put("agentId", id);
|
||||
PageData<AgentChatSessionDTO> page = agentChatHistoryService.getSessionListByAgentId(params);
|
||||
return new Result<PageData<AgentChatSessionDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/chat-history/{sessionId}")
|
||||
@Operation(summary = "获取智能体聊天记录")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentChatHistoryDTO>> getAgentChatHistory(
|
||||
@PathVariable("id") String id,
|
||||
@PathVariable("sessionId") String sessionId) {
|
||||
// 获取当前用户
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
|
||||
// 检查权限
|
||||
if (!agentService.checkAgentPermission(id, user.getId())) {
|
||||
return new Result<List<AgentChatHistoryDTO>>().error("没有权限查看该智能体的聊天记录");
|
||||
}
|
||||
|
||||
// 查询聊天记录
|
||||
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/audio/{audioId}")
|
||||
@Operation(summary = "获取音频下载ID")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> getAudioId(@PathVariable("audioId") String audioId) {
|
||||
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||
if (audioData == null) {
|
||||
return new Result<String>().error("音频不存在");
|
||||
}
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId);
|
||||
return new Result<String>().ok(uuid);
|
||||
}
|
||||
|
||||
@GetMapping("/play/{uuid}")
|
||||
@Operation(summary = "播放音频")
|
||||
public ResponseEntity<byte[]> playAudio(@PathVariable("uuid") String uuid) {
|
||||
|
||||
String audioId = (String) redisUtils.get(RedisKeys.getAgentAudioIdKey(uuid));
|
||||
if (StringUtils.isBlank(audioId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
byte[] audioData = agentChatAudioService.getAudio(audioId);
|
||||
if (audioData == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
redisUtils.delete(RedisKeys.getAgentAudioIdKey(uuid));
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"play.wav\"")
|
||||
.body(audioData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
|
||||
@@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao<AgentEntity> {
|
||||
* @return 设备数量
|
||||
*/
|
||||
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||
*
|
||||
* @param macAddress 设备MAC地址
|
||||
* @return 默认智能体信息
|
||||
*/
|
||||
@Select(" SELECT a.* FROM ai_device d " +
|
||||
" LEFT JOIN ai_agent a ON d.agent_id = a.id " +
|
||||
" WHERE d.mac_address = #{macAddress} " +
|
||||
" ORDER BY d.id DESC LIMIT 1")
|
||||
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||
|
||||
/**
|
||||
* {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/5/8
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiAgentChatAudioDao extends BaseMapper<AgentChatAudioEntity> {
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
|
||||
/**
|
||||
* {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
|
||||
/**
|
||||
* 根据智能体ID删除音频
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAudioByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除聊天历史记录
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteHistoryByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除音频ID
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAudioIdByAgentId(String agentId);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能体聊天记录")
|
||||
public class AgentChatHistoryDTO {
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "消息类型: 1-用户, 2-智能体")
|
||||
private Byte chatType;
|
||||
|
||||
@Schema(description = "聊天内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "音频ID")
|
||||
private String audioId;
|
||||
|
||||
@Schema(description = "MAC地址")
|
||||
private String macAddress;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 小智设备聊天上报请求
|
||||
*
|
||||
* @author Haotian
|
||||
* @version 1.0, 2025/5/8
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "小智设备聊天上报请求")
|
||||
public class AgentChatHistoryReportDTO {
|
||||
@Schema(description = "MAC地址", example = "00:11:22:33:44:55")
|
||||
@NotBlank
|
||||
private String macAddress;
|
||||
@Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a")
|
||||
@NotBlank
|
||||
private String sessionId;
|
||||
@Schema(description = "消息类型: 1-用户, 2-智能体", example = "1")
|
||||
@NotNull
|
||||
private Byte chatType;
|
||||
@Schema(description = "聊天内容", example = "你好呀")
|
||||
@NotBlank
|
||||
private String content;
|
||||
@Schema(description = "base64编码的opus音频数据", example = "")
|
||||
private String audioBase64;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体会话列表DTO
|
||||
*/
|
||||
@Data
|
||||
public class AgentChatSessionDTO {
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 会话时间
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 聊天条数
|
||||
*/
|
||||
private Integer chatCount;
|
||||
}
|
||||
@@ -27,9 +27,16 @@ public class AgentDTO {
|
||||
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
||||
private String llmModelName;
|
||||
|
||||
@Schema(description = "记忆模型ID", example = "mem_model_01")
|
||||
private String memModelId;
|
||||
|
||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
||||
private Date lastConnectedAt;
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体记忆更新DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能体记忆更新对象")
|
||||
public class AgentMemoryDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
}
|
||||
@@ -45,6 +45,13 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false)
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
||||
private String langCode;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体聊天音频数据表
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/5/8
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("ai_agent_chat_audio")
|
||||
public class AgentChatAudioEntity {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 音频opus数据
|
||||
*/
|
||||
private byte[] audio;
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录表
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@TableName(value = "ai_agent_chat_history")
|
||||
public class AgentChatHistoryEntity {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* MAC地址
|
||||
*/
|
||||
@TableField(value = "mac_address")
|
||||
private String macAddress;
|
||||
|
||||
/**
|
||||
* 智能体id
|
||||
*/
|
||||
@TableField(value = "agent_id")
|
||||
private String agentId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@TableField(value = "session_id")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 消息类型: 1-用户, 2-智能体
|
||||
*/
|
||||
@TableField(value = "chat_type")
|
||||
private Byte chatType;
|
||||
|
||||
/**
|
||||
* 聊天内容
|
||||
*/
|
||||
@TableField(value = "content")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 音频base64数据
|
||||
*/
|
||||
@TableField(value = "audio_id")
|
||||
private String audioId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "created_at")
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "updated_at")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -48,9 +48,16 @@ public class AgentEntity {
|
||||
@Schema(description = "意图模型标识")
|
||||
private String intentModelId;
|
||||
|
||||
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)")
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
@Schema(description = "角色设定参数")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "语言编码")
|
||||
private String langCode;
|
||||
|
||||
|
||||
@@ -69,11 +69,20 @@ public class AgentTemplateEntity implements Serializable {
|
||||
*/
|
||||
private String intentModelId;
|
||||
|
||||
/**
|
||||
* 聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)
|
||||
*/
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
/**
|
||||
* 角色设定参数
|
||||
*/
|
||||
private String systemPrompt;
|
||||
|
||||
/**
|
||||
* 总结记忆
|
||||
*/
|
||||
private String summaryMemory;
|
||||
/**
|
||||
* 语言编码
|
||||
*/
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||
|
||||
/**
|
||||
* 智能体聊天音频数据表处理service
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/5/8
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface AgentChatAudioService extends IService<AgentChatAudioEntity> {
|
||||
/**
|
||||
* 保存音频数据
|
||||
*
|
||||
* @param audioData 音频数据
|
||||
* @return 音频ID
|
||||
*/
|
||||
String saveAudio(byte[] audioData);
|
||||
|
||||
/**
|
||||
* 获取音频数据
|
||||
*
|
||||
* @param audioId 音频ID
|
||||
* @return 音频数据
|
||||
*/
|
||||
byte[] getAudio(String audioId);
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录表处理service
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity> {
|
||||
|
||||
/**
|
||||
* 根据智能体ID获取会话列表
|
||||
*
|
||||
* @param params 查询参数,包含agentId、page、limit
|
||||
* @return 分页的会话列表
|
||||
*/
|
||||
PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据会话ID获取聊天记录列表
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param sessionId 会话ID
|
||||
* @return 聊天记录列表
|
||||
*/
|
||||
List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除聊天记录
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param deleteAudio 是否删除音频
|
||||
* @param deleteText 是否删除文本
|
||||
*/
|
||||
void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText);
|
||||
}
|
||||
@@ -8,38 +8,75 @@ import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
|
||||
/**
|
||||
* 智能体表处理service
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface AgentService extends BaseService<AgentEntity> {
|
||||
|
||||
/**
|
||||
* 管理员获取所有智能体列表(分页)
|
||||
* 获取管理员智能体列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @return 分页数据
|
||||
*/
|
||||
PageData<AgentEntity> adminAgentList(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 获取智能体详情
|
||||
* 根据ID获取智能体
|
||||
*
|
||||
* @param id 智能体ID
|
||||
* @return 智能体实体
|
||||
*/
|
||||
AgentEntity getAgentById(String id);
|
||||
|
||||
/**
|
||||
* 删除这个用户的所有
|
||||
*
|
||||
* @param userId
|
||||
* 插入智能体
|
||||
*
|
||||
* @param entity 智能体实体
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean insert(AgentEntity entity);
|
||||
|
||||
/**
|
||||
* 根据用户ID删除智能体
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
void deleteAgentByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 获取用户智能体列表
|
||||
*
|
||||
* @param userId
|
||||
* @return
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 智能体列表
|
||||
*/
|
||||
List<AgentDTO> getUserAgents(Long userId);
|
||||
|
||||
/**
|
||||
* 获取智能体的设备数量
|
||||
*
|
||||
* 根据智能体ID获取设备数量
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 设备数量
|
||||
*/
|
||||
Integer getDeviceCountByAgentId(String agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备MAC地址查询对应设备的默认智能体信息
|
||||
*
|
||||
* @param macAddress 设备MAC地址
|
||||
* @return 默认智能体信息,不存在时返回null
|
||||
*/
|
||||
AgentEntity getDefaultAgentByMacAddress(String macAddress);
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限访问智能体
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param userId 用户ID
|
||||
* @return 是否有权限
|
||||
*/
|
||||
boolean checkAgentPermission(String agentId, Long userId);
|
||||
}
|
||||
|
||||
@@ -17,4 +17,12 @@ public interface AgentTemplateService extends IService<AgentTemplateEntity> {
|
||||
* @return 默认模板实体
|
||||
*/
|
||||
AgentTemplateEntity getDefaultTemplate();
|
||||
|
||||
/**
|
||||
* 更新默认模板中的模型ID
|
||||
*
|
||||
* @param modelType 模型类型
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
void updateDefaultTemplateModelId(String modelType, String modelId);
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.agent.service.biz;
|
||||
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||
|
||||
/**
|
||||
* 智能体聊天历史业务逻辑层
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface AgentChatHistoryBizService {
|
||||
|
||||
/**
|
||||
* 聊天上报方法
|
||||
*
|
||||
* @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象
|
||||
* 例如:设备MAC地址、文件类型、内容等
|
||||
* @return 上传结果,true表示成功,false表示失败
|
||||
*/
|
||||
Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO);
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package xiaozhi.modules.agent.service.biz.impl;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||
|
||||
/**
|
||||
* {@link AgentChatHistoryBizService} impl
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService {
|
||||
private final AgentService agentService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
/**
|
||||
* 处理聊天记录上报,包括文件上传和相关信息记录
|
||||
*
|
||||
* @param report 包含聊天上报所需信息的输入对象
|
||||
* @return 上传结果,true表示成功,false表示失败
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean report(AgentChatHistoryReportDTO report) {
|
||||
String macAddress = report.getMacAddress();
|
||||
Byte chatType = report.getChatType();
|
||||
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
||||
|
||||
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||
if (agentEntity == null) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
|
||||
Integer chatHistoryConf = agentEntity.getChatHistoryConf();
|
||||
String agentId = agentEntity.getId();
|
||||
|
||||
if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT.getCode())) {
|
||||
saveChatText(report, agentId, macAddress, null);
|
||||
} else if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode())) {
|
||||
String audioId = saveChatAudio(report);
|
||||
saveChatText(report, agentId, macAddress, audioId);
|
||||
}
|
||||
|
||||
// 更新设备最后对话时间
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||
*/
|
||||
private String saveChatAudio(AgentChatHistoryReportDTO report) {
|
||||
String audioId = null;
|
||||
|
||||
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
||||
try {
|
||||
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
||||
audioId = agentChatAudioService.saveAudio(audioData);
|
||||
log.info("音频数据保存成功,audioId={}", audioId);
|
||||
} catch (Exception e) {
|
||||
log.error("音频数据保存失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return audioId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装上报数据
|
||||
*/
|
||||
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId) {
|
||||
|
||||
// 构建聊天记录实体
|
||||
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||
.macAddress(macAddress)
|
||||
.agentId(agentId)
|
||||
.sessionId(report.getSessionId())
|
||||
.chatType(report.getChatType())
|
||||
.content(report.getContent())
|
||||
.audioId(audioId)
|
||||
.build();
|
||||
|
||||
// 保存数据
|
||||
agentChatHistoryService.save(entity);
|
||||
|
||||
log.info("设备 {} 对应智能体 {} 上报成功", macAddress, agentId);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
|
||||
import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
|
||||
/**
|
||||
* 智能体聊天音频数据表处理service {@link AgentChatAudioService} impl
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/5/8
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Service
|
||||
public class AgentChatAudioServiceImpl extends ServiceImpl<AiAgentChatAudioDao, AgentChatAudioEntity>
|
||||
implements AgentChatAudioService {
|
||||
@Override
|
||||
public String saveAudio(byte[] audioData) {
|
||||
AgentChatAudioEntity entity = new AgentChatAudioEntity();
|
||||
entity.setAudio(audioData);
|
||||
save(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getAudio(String audioId) {
|
||||
AgentChatAudioEntity entity = getById(audioId);
|
||||
return entity != null ? entity.getAudio() : null;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录表处理service {@link AgentChatHistoryService} impl
|
||||
*
|
||||
* @author Goody
|
||||
* @version 1.0, 2025/4/30
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Service
|
||||
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||
implements AgentChatHistoryService {
|
||||
|
||||
@Override
|
||||
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
|
||||
String agentId = (String) params.get("agentId");
|
||||
int page = Integer.parseInt(params.get(Constant.PAGE).toString());
|
||||
int limit = Integer.parseInt(params.get(Constant.LIMIT).toString());
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.select("session_id", "MAX(created_at) as created_at", "COUNT(*) as chat_count")
|
||||
.eq("agent_id", agentId)
|
||||
.groupBy("session_id")
|
||||
.orderByDesc("created_at");
|
||||
|
||||
// 执行分页查询
|
||||
Page<Map<String, Object>> pageParam = new Page<>(page, limit);
|
||||
IPage<Map<String, Object>> result = this.baseMapper.selectMapsPage(pageParam, wrapper);
|
||||
|
||||
List<AgentChatSessionDTO> records = result.getRecords().stream().map(map -> {
|
||||
AgentChatSessionDTO dto = new AgentChatSessionDTO();
|
||||
dto.setSessionId((String) map.get("session_id"));
|
||||
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
|
||||
dto.setChatCount(((Number) map.get("chat_count")).intValue());
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return new PageData<>(records, result.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentChatHistoryDTO> getChatHistoryBySessionId(String agentId, String sessionId) {
|
||||
// 构建查询条件
|
||||
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("agent_id", agentId)
|
||||
.eq("session_id", sessionId)
|
||||
.orderByAsc("created_at");
|
||||
|
||||
// 查询聊天记录
|
||||
List<AgentChatHistoryEntity> historyList = list(wrapper);
|
||||
|
||||
// 转换为DTO
|
||||
return ConvertUtils.sourceToTarget(historyList, AgentChatHistoryDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||
if (deleteAudio) {
|
||||
baseMapper.deleteAudioByAgentId(agentId);
|
||||
}
|
||||
if (deleteAudio && !deleteText) {
|
||||
baseMapper.deleteAudioIdByAgentId(agentId);
|
||||
}
|
||||
if (deleteText) {
|
||||
baseMapper.deleteHistoryByAgentId(agentId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+52
-18
@@ -6,13 +6,14 @@ import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
@@ -21,37 +22,40 @@ import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||
private final AgentDao agentDao;
|
||||
|
||||
@Autowired
|
||||
private TimbreService timbreModelService;
|
||||
|
||||
@Autowired
|
||||
private ModelConfigService modelConfigService;
|
||||
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
public AgentServiceImpl(AgentDao agentDao) {
|
||||
this.agentDao = agentDao;
|
||||
}
|
||||
private final TimbreService timbreModelService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
IPage<AgentEntity> page = agentDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
getPage(params, "agent_name", true),
|
||||
new QueryWrapper<>());
|
||||
return new PageData<>(page.getRecords(), page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentEntity getAgentById(String id) {
|
||||
return agentDao.selectById(id);
|
||||
AgentEntity agent = agentDao.selectById(id);
|
||||
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||
} else if (agent != null && agent.getMemModelId() != null
|
||||
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||
&& agent.getChatHistoryConf() == null) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,12 +102,17 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
// 获取 LLM 模型名称
|
||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||
|
||||
// 获取记忆模型名称
|
||||
dto.setMemModelId(agent.getMemModelId());
|
||||
|
||||
// 获取 TTS 音色名称
|
||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||
|
||||
// 获取智能体最近的最后连接时长
|
||||
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
||||
|
||||
// 获取设备数量
|
||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
@@ -130,4 +139,29 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
return deviceCount != null ? deviceCount : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentEntity getDefaultAgentByMacAddress(String macAddress) {
|
||||
if (StringUtils.isEmpty(macAddress)) {
|
||||
return null;
|
||||
}
|
||||
return agentDao.getDefaultAgentByMacAddress(macAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkAgentPermission(String agentId, Long userId) {
|
||||
// 获取智能体信息
|
||||
AgentEntity agent = getAgentById(agentId);
|
||||
if (agent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果是超级管理员,直接返回true
|
||||
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否是智能体的所有者
|
||||
return userId.equals(agent.getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.service.impl;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||
@@ -29,4 +30,40 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
||||
.last("LIMIT 1");
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新默认模板中的模型ID
|
||||
*
|
||||
* @param modelType 模型类型
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
@Override
|
||||
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
||||
modelType = modelType.toUpperCase();
|
||||
|
||||
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
|
||||
switch (modelType) {
|
||||
case "ASR":
|
||||
wrapper.set("asr_model_id", modelId);
|
||||
break;
|
||||
case "VAD":
|
||||
wrapper.set("vad_model_id", modelId);
|
||||
break;
|
||||
case "LLM":
|
||||
wrapper.set("llm_model_id", modelId);
|
||||
break;
|
||||
case "TTS":
|
||||
wrapper.set("tts_model_id", modelId);
|
||||
wrapper.set("tts_voice_id", null);
|
||||
break;
|
||||
case "MEMORY":
|
||||
wrapper.set("mem_model_id", modelId);
|
||||
break;
|
||||
case "INTENT":
|
||||
wrapper.set("intent_model_id", modelId);
|
||||
break;
|
||||
}
|
||||
wrapper.ge("sort", 0);
|
||||
update(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package xiaozhi.modules.config.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.config.dto.AgentModelsDTO;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
|
||||
/**
|
||||
* xiaozhi-server 配置获取
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("config")
|
||||
@Tag(name = "参数管理")
|
||||
@AllArgsConstructor
|
||||
public class ConfigController {
|
||||
private final ConfigService configService;
|
||||
|
||||
@PostMapping("server-base")
|
||||
@Operation(summary = "获取配置")
|
||||
public Result<Object> getConfig() {
|
||||
Object config = configService.getConfig(true);
|
||||
return new Result<Object>().ok(config);
|
||||
}
|
||||
|
||||
@PostMapping("agent-models")
|
||||
@Operation(summary = "获取智能体模型")
|
||||
public Result<Object> getAgentModels(@Valid @RequestBody AgentModelsDTO dto) {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
|
||||
return new Result<Object>().ok(models);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package xiaozhi.modules.config.dto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "获取智能体模型配置DTO")
|
||||
public class AgentModelsDTO {
|
||||
|
||||
@NotBlank(message = "设备MAC地址不能为空")
|
||||
@Schema(description = "设备MAC地址")
|
||||
private String macAddress;
|
||||
|
||||
@NotBlank(message = "客户端ID不能为空")
|
||||
@Schema(description = "客户端ID")
|
||||
private String clientId;
|
||||
|
||||
@NotNull(message = "客户端已实例化的模型不能为空")
|
||||
@Schema(description = "客户端已实例化的模型")
|
||||
private Map<String, String> selectedModule;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package xiaozhi.modules.config.init;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Configuration
|
||||
@DependsOn("liquibase")
|
||||
public class SystemInitConfig {
|
||||
|
||||
@Autowired
|
||||
private SysParamsService sysParamsService;
|
||||
|
||||
@Autowired
|
||||
private ConfigService configService;
|
||||
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 检查版本号
|
||||
String redisVersion = (String) redisUtils.get(RedisKeys.getVersionKey());
|
||||
if (!Constant.VERSION.equals(redisVersion)) {
|
||||
// 如果版本不一致,清空Redis
|
||||
redisUtils.emptyAll();
|
||||
// 存储新版本号
|
||||
redisUtils.set(RedisKeys.getVersionKey(), Constant.VERSION);
|
||||
}
|
||||
|
||||
sysParamsService.initServerSecret();
|
||||
configService.getConfig(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.config.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface ConfigService {
|
||||
/**
|
||||
* 获取服务器配置
|
||||
*
|
||||
* @param isCache 是否缓存
|
||||
* @return 配置信息
|
||||
*/
|
||||
Object getConfig(Boolean isCache);
|
||||
|
||||
/**
|
||||
* 获取智能体模型配置
|
||||
*
|
||||
* @param macAddress MAC地址
|
||||
* @param selectedModule 客户端已实例化的模型
|
||||
* @return 模型配置信息
|
||||
*/
|
||||
Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule);
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
package xiaozhi.modules.config.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
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.timbre.vo.TimbreDetailsVO;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class ConfigServiceImpl implements ConfigService {
|
||||
private final SysParamsService sysParamsService;
|
||||
private final DeviceService deviceService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final AgentService agentService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final TimbreService timbreService;
|
||||
|
||||
@Override
|
||||
public Object getConfig(Boolean isCache) {
|
||||
if (isCache) {
|
||||
// 先从Redis获取配置
|
||||
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
|
||||
if (cachedConfig != null) {
|
||||
return cachedConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建配置信息
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
buildConfig(result);
|
||||
|
||||
// 查询默认智能体
|
||||
AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate();
|
||||
if (agent == null) {
|
||||
throw new RenException("默认智能体未找到");
|
||||
}
|
||||
|
||||
// 构建模块配置
|
||||
buildModuleConfig(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
result,
|
||||
isCache);
|
||||
|
||||
// 将配置存入Redis
|
||||
redisUtils.set(RedisKeys.getServerConfigKey(), result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule) {
|
||||
// 根据MAC地址查找设备
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null) {
|
||||
// 如果设备,去redis里看看有没有需要连接的设备
|
||||
String cachedCode = deviceService.geCodeByDeviceId(macAddress);
|
||||
if (StringUtils.isNotBlank(cachedCode)) {
|
||||
throw new RenException(ErrorCode.OTA_DEVICE_NEED_BIND, cachedCode);
|
||||
}
|
||||
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
|
||||
}
|
||||
|
||||
// 获取智能体信息
|
||||
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||
if (agent == null) {
|
||||
throw new RenException("智能体未找到");
|
||||
}
|
||||
// 获取音色信息
|
||||
String voice = null;
|
||||
TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId());
|
||||
if (timbre != null) {
|
||||
voice = timbre.getTtsVoice();
|
||||
}
|
||||
// 构建返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// 获取单台设备每天最多输出字数
|
||||
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||
|
||||
// 获取聊天记录配置
|
||||
Integer chatHistoryConf = agent.getChatHistoryConf();
|
||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
chatHistoryConf = Constant.ChatHistoryConfEnum.IGNORE.getCode();
|
||||
} else if (agent.getMemModelId() != null
|
||||
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||
&& agent.getChatHistoryConf() == null) {
|
||||
chatHistoryConf = Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode();
|
||||
}
|
||||
result.put("chat_history_conf", chatHistoryConf);
|
||||
// 如果客户端已实例化模型,则不返回
|
||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||
agent.setVadModelId(null);
|
||||
}
|
||||
String alreadySelectedAsrModelId = (String) selectedModule.get("ASR");
|
||||
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
||||
agent.setAsrModelId(null);
|
||||
}
|
||||
|
||||
// 构建模块配置
|
||||
buildModuleConfig(
|
||||
agent.getAgentName(),
|
||||
agent.getSystemPrompt(),
|
||||
agent.getSummaryMemory(),
|
||||
voice,
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
agent.getLlmModelId(),
|
||||
agent.getTtsModelId(),
|
||||
agent.getMemModelId(),
|
||||
agent.getIntentModelId(),
|
||||
result,
|
||||
true);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建配置信息
|
||||
*
|
||||
* @param paramsList 系统参数列表
|
||||
* @return 配置信息
|
||||
*/
|
||||
private Object buildConfig(Map<String, Object> config) {
|
||||
|
||||
// 查询所有系统参数
|
||||
List<SysParamsDTO> paramsList = sysParamsService.list(new HashMap<>());
|
||||
|
||||
for (SysParamsDTO param : paramsList) {
|
||||
String[] keys = param.getParamCode().split("\\.");
|
||||
Map<String, Object> current = config;
|
||||
|
||||
// 遍历除最后一个key之外的所有key
|
||||
for (int i = 0; i < keys.length - 1; i++) {
|
||||
String key = keys[i];
|
||||
if (!current.containsKey(key)) {
|
||||
current.put(key, new HashMap<String, Object>());
|
||||
}
|
||||
current = (Map<String, Object>) current.get(key);
|
||||
}
|
||||
|
||||
// 处理最后一个key
|
||||
String lastKey = keys[keys.length - 1];
|
||||
String value = param.getParamValue();
|
||||
|
||||
// 根据valueType转换值
|
||||
switch (param.getValueType().toLowerCase()) {
|
||||
case "number":
|
||||
try {
|
||||
double doubleValue = Double.parseDouble(value);
|
||||
// 如果数值是整数形式,则转换为Integer
|
||||
if (doubleValue == (int) doubleValue) {
|
||||
current.put(lastKey, (int) doubleValue);
|
||||
} else {
|
||||
current.put(lastKey, doubleValue);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
current.put(lastKey, value);
|
||||
}
|
||||
break;
|
||||
case "boolean":
|
||||
current.put(lastKey, Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "array":
|
||||
// 将分号分隔的字符串转换为数字数组
|
||||
List<String> list = new ArrayList<>();
|
||||
for (String num : value.split(";")) {
|
||||
if (StringUtils.isNotBlank(num)) {
|
||||
list.add(num.trim());
|
||||
}
|
||||
}
|
||||
current.put(lastKey, list);
|
||||
break;
|
||||
case "json":
|
||||
try {
|
||||
current.put(lastKey, JsonUtils.parseObject(value, Object.class));
|
||||
} catch (Exception e) {
|
||||
current.put(lastKey, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
current.put(lastKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建模块配置
|
||||
*
|
||||
* @param prompt 提示词
|
||||
* @param voice 音色
|
||||
* @param vadModelId VAD模型ID
|
||||
* @param asrModelId ASR模型ID
|
||||
* @param llmModelId LLM模型ID
|
||||
* @param ttsModelId TTS模型ID
|
||||
* @param memModelId 记忆模型ID
|
||||
* @param intentModelId 意图模型ID
|
||||
* @param result 结果Map
|
||||
*/
|
||||
private void buildModuleConfig(
|
||||
String assistantName,
|
||||
String prompt,
|
||||
String summaryMemory,
|
||||
String voice,
|
||||
String vadModelId,
|
||||
String asrModelId,
|
||||
String llmModelId,
|
||||
String ttsModelId,
|
||||
String memModelId,
|
||||
String intentModelId,
|
||||
Map<String, Object> result,
|
||||
boolean isCache) {
|
||||
Map<String, String> selectedModule = new HashMap<>();
|
||||
|
||||
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM" };
|
||||
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId };
|
||||
String intentLLMModelId = null;
|
||||
|
||||
for (int i = 0; i < modelIds.length; i++) {
|
||||
if (modelIds[i] == null) {
|
||||
continue;
|
||||
}
|
||||
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache);
|
||||
Map<String, Object> typeConfig = new HashMap<>();
|
||||
if (model.getConfigJson() != null) {
|
||||
typeConfig.put(model.getId(), model.getConfigJson());
|
||||
// 如果是TTS类型,添加private_voice属性
|
||||
if ("TTS".equals(modelTypes[i]) && voice != null) {
|
||||
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
|
||||
}
|
||||
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型
|
||||
if ("Intent".equals(modelTypes[i])) {
|
||||
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
||||
if ("intent_llm".equals(map.get("type"))) {
|
||||
intentLLMModelId = (String) map.get("llm");
|
||||
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||
intentLLMModelId = null;
|
||||
}
|
||||
}
|
||||
if (map.get("functions") != null) {
|
||||
String functionStr = (String) map.get("functions");
|
||||
if (StringUtils.isNotBlank(functionStr)) {
|
||||
String[] functions = functionStr.split("\\;");
|
||||
map.put("functions", functions);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
||||
if ("LLM".equals(modelTypes[i]) && intentLLMModelId != null) {
|
||||
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache);
|
||||
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
||||
}
|
||||
}
|
||||
result.put(modelTypes[i], typeConfig);
|
||||
|
||||
selectedModule.put(modelTypes[i], model.getId());
|
||||
}
|
||||
|
||||
result.put("selected_module", selectedModule);
|
||||
if (StringUtils.isNotBlank(prompt)) {
|
||||
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||
}
|
||||
result.put("prompt", prompt);
|
||||
result.put("summaryMemory", summaryMemory);
|
||||
}
|
||||
}
|
||||
+13
@@ -7,6 +7,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -79,4 +80,16 @@ public class DeviceController {
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@PutMapping("/enableOta/{id}/{status}")
|
||||
@Operation(summary = "启用/关闭OTA自动升级")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> enableOtaUpgrade(@PathVariable String id, @PathVariable Integer status) {
|
||||
DeviceEntity entity = deviceService.selectById(id);
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("设备不存在");
|
||||
}
|
||||
entity.setAutoUpdate(status);
|
||||
deviceService.updateById(entity);
|
||||
return new Result<Void>();
|
||||
}
|
||||
}
|
||||
+56
-11
@@ -15,48 +15,78 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.DeviceService;
|
||||
import xiaozhi.modules.device.utils.NetworkUtil;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/ota/")
|
||||
public class OTAController {
|
||||
private final DeviceService deviceService;
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
@Operation(summary = "检查 OTA 版本和设备激活状态")
|
||||
@Operation(summary = "OTA版本和设备激活状态检查")
|
||||
@PostMapping
|
||||
public ResponseEntity<String> checkOTAVersion(
|
||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||
|
||||
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||
|
||||
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
|
||||
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
||||
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||
if (StringUtils.isBlank(deviceId)) {
|
||||
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||
}
|
||||
if (StringUtils.isBlank(clientId)) {
|
||||
clientId = deviceId;
|
||||
}
|
||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||
boolean macAddressValid = isMacAddressValid(macAddress);
|
||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||
}
|
||||
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
||||
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
||||
}
|
||||
|
||||
@Operation(summary = "设备快速检查激活状态")
|
||||
@PostMapping("activate")
|
||||
public ResponseEntity<String> activateDevice(
|
||||
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||
if (StringUtils.isBlank(deviceId)) {
|
||||
return ResponseEntity.status(202).build();
|
||||
}
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(deviceId);
|
||||
if (device == null) {
|
||||
return ResponseEntity.status(202).build();
|
||||
}
|
||||
return ResponseEntity.ok("success");
|
||||
}
|
||||
|
||||
@Operation(summary = "获取 OTA 提示信息")
|
||||
@GetMapping
|
||||
public ResponseEntity<String> getOTAPrompt() {
|
||||
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
|
||||
@Hidden
|
||||
public ResponseEntity<String> getOTA() {
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
}
|
||||
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||
return ResponseEntity.ok("OTA接口不正常,缺少ota地址,请登录智控台,在参数管理找到【server.ota】配置");
|
||||
}
|
||||
return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@@ -71,4 +101,19 @@ public class OTAController {
|
||||
.contentLength(jsonBytes.length)
|
||||
.body(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单判断mac地址是否有效(非严格)
|
||||
*
|
||||
* @param macAddress
|
||||
* @return
|
||||
*/
|
||||
private boolean isMacAddressValid(String macAddress) {
|
||||
if (StringUtils.isBlank(macAddress)) {
|
||||
return false;
|
||||
}
|
||||
// MAC地址通常为12位十六进制数字,可以包含冒号或连字符分隔符
|
||||
String macPattern = "^([0-9A-Za-z]{2}[:-]){5}([0-9A-Za-z]{2})$";
|
||||
return macAddress.matches(macPattern);
|
||||
}
|
||||
}
|
||||
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
package xiaozhi.modules.device.controller;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
|
||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/otaMag")
|
||||
public class OTAMagController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
||||
private final OtaService otaService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询 OTA 固件信息")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||
})
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<OtaEntity>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
ValidatorUtils.validateEntity(params);
|
||||
PageData<OtaEntity> page = otaService.page(params);
|
||||
return new Result<PageData<OtaEntity>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
@Operation(summary = "信息 OTA 固件信息")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<OtaEntity> get(@PathVariable("id") String id) {
|
||||
OtaEntity data = otaService.selectById(id);
|
||||
return new Result<OtaEntity>().ok(data);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "保存 OTA 固件信息")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> save(@RequestBody OtaEntity entity) {
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("固件信息不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getFirmwareName())) {
|
||||
return new Result<Void>().error("固件名称不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getType())) {
|
||||
return new Result<Void>().error("固件类型不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getVersion())) {
|
||||
return new Result<Void>().error("版本号不能为空");
|
||||
}
|
||||
try {
|
||||
otaService.save(entity);
|
||||
return new Result<Void>();
|
||||
} catch (RuntimeException e) {
|
||||
return new Result<Void>().error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "OTA 删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@PathVariable("id") String[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return new Result<Void>().error("删除的固件ID不能为空");
|
||||
}
|
||||
otaService.delete(ids);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "修改 OTA 固件信息")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<?> update(@PathVariable("id") String id, @RequestBody OtaEntity entity) {
|
||||
if (entity == null) {
|
||||
return new Result<>().error("固件信息不能为空");
|
||||
}
|
||||
entity.setId(id);
|
||||
try {
|
||||
otaService.update(entity);
|
||||
return new Result<>();
|
||||
} catch (RuntimeException e) {
|
||||
return new Result<>().error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/getDownloadUrl/{id}")
|
||||
@Operation(summary = "获取 OTA 固件下载链接")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<String> getDownloadUrl(@PathVariable("id") String id) {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getOtaIdKey(uuid), id);
|
||||
return new Result<String>().ok(uuid);
|
||||
}
|
||||
|
||||
@GetMapping("/download/{uuid}")
|
||||
@Operation(summary = "下载固件文件")
|
||||
public ResponseEntity<byte[]> downloadFirmware(@PathVariable("uuid") String uuid) {
|
||||
String id = (String) redisUtils.get(RedisKeys.getOtaIdKey(uuid));
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// 检查下载次数
|
||||
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
||||
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
|
||||
if (downloadCount == null) {
|
||||
downloadCount = 0;
|
||||
}
|
||||
|
||||
// 如果下载次数超过3次,返回404
|
||||
if (downloadCount >= 3) {
|
||||
redisUtils.delete(downloadCountKey);
|
||||
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
|
||||
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
redisUtils.set(downloadCountKey, downloadCount + 1);
|
||||
|
||||
try {
|
||||
// 获取固件信息
|
||||
OtaEntity otaEntity = otaService.selectById(id);
|
||||
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
||||
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
||||
String firmwarePath = otaEntity.getFirmwarePath();
|
||||
Path path;
|
||||
|
||||
// 检查是否是绝对路径
|
||||
if (Paths.get(firmwarePath).isAbsolute()) {
|
||||
path = Paths.get(firmwarePath);
|
||||
} else {
|
||||
// 如果是相对路径,则从当前工作目录解析
|
||||
path = Paths.get(System.getProperty("user.dir"), firmwarePath);
|
||||
}
|
||||
|
||||
logger.info("Attempting to download firmware for ID: {}, DB path: {}, resolved path: {}",
|
||||
id, firmwarePath, path.toAbsolutePath());
|
||||
|
||||
if (!Files.exists(path) || !Files.isRegularFile(path)) {
|
||||
// 尝试直接从firmware目录下查找文件名
|
||||
String fileName = new File(firmwarePath).getName();
|
||||
Path altPath = Paths.get(System.getProperty("user.dir"), "firmware", fileName);
|
||||
|
||||
logger.info("File not found at primary path, trying alternative path: {}", altPath.toAbsolutePath());
|
||||
|
||||
if (Files.exists(altPath) && Files.isRegularFile(altPath)) {
|
||||
path = altPath;
|
||||
} else {
|
||||
logger.error("Firmware file not found at either path: {} or {}",
|
||||
path.toAbsolutePath(), altPath.toAbsolutePath());
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
byte[] fileContent = Files.readAllBytes(path);
|
||||
|
||||
// 设置响应头
|
||||
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||
if (firmwarePath.contains(".")) {
|
||||
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
||||
originalFilename += extension;
|
||||
}
|
||||
|
||||
// 清理文件名,移除不安全字符
|
||||
String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||
|
||||
logger.info("Providing download for firmware ID: {}, filename: {}, size: {} bytes",
|
||||
id, safeFilename, fileContent.length);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + safeFilename + "\"")
|
||||
.body(fileContent);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error reading firmware file for ID: {}", id, e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error during firmware download for ID: {}", id, e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "上传固件文件")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<String> uploadFirmware(@RequestParam("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
return new Result<String>().error("上传文件不能为空");
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
if (originalFilename == null) {
|
||||
return new Result<String>().error("文件名不能为空");
|
||||
}
|
||||
|
||||
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
|
||||
if (!extension.equals(".bin") && !extension.equals(".apk")) {
|
||||
return new Result<String>().error("只允许上传.bin和.apk格式的文件");
|
||||
}
|
||||
|
||||
try {
|
||||
// 计算文件的MD5值
|
||||
String md5 = calculateMD5(file);
|
||||
|
||||
// 设置存储路径
|
||||
String uploadDir = "uploadfile";
|
||||
Path uploadPath = Paths.get(uploadDir);
|
||||
|
||||
// 如果目录不存在,创建目录
|
||||
if (!Files.exists(uploadPath)) {
|
||||
Files.createDirectories(uploadPath);
|
||||
}
|
||||
|
||||
// 使用MD5作为文件名,固定使用.bin扩展名
|
||||
String uniqueFileName = md5 + extension;
|
||||
Path filePath = uploadPath.resolve(uniqueFileName);
|
||||
|
||||
// 检查文件是否已存在
|
||||
if (Files.exists(filePath)) {
|
||||
return new Result<String>().ok(filePath.toString());
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
Files.copy(file.getInputStream(), filePath);
|
||||
|
||||
// 返回文件路径
|
||||
return new Result<String>().ok(filePath.toString());
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
return new Result<String>().error("文件上传失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(file.getBytes());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package xiaozhi.modules.device.dao;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
@@ -8,4 +10,12 @@ import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
|
||||
@Mapper
|
||||
public interface DeviceDao extends BaseMapper<DeviceEntity> {
|
||||
/**
|
||||
* 获取此智能体全部设备的最后连接时间
|
||||
*
|
||||
* @param agentId 智能体id
|
||||
* @return
|
||||
*/
|
||||
Date getAllLastConnectedAtByAgentId(String agentId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package xiaozhi.modules.device.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
|
||||
/**
|
||||
* OTA固件管理
|
||||
*/
|
||||
@Mapper
|
||||
public interface OtaDao extends BaseMapper<OtaEntity> {
|
||||
|
||||
}
|
||||
@@ -19,6 +19,9 @@ public class DeviceReportRespDTO {
|
||||
|
||||
@Schema(description = "固件版本信息")
|
||||
private Firmware firmware;
|
||||
|
||||
@Schema(description = "WebSocket配置")
|
||||
private Websocket websocket;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -44,6 +47,8 @@ public class DeviceReportRespDTO {
|
||||
@Schema(description = "激活码信息: 激活地址")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "挑战码")
|
||||
private String challenge;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -58,4 +63,11 @@ public class DeviceReportRespDTO {
|
||||
@Schema(description = "时区偏移量,单位为分钟")
|
||||
private Integer timezone_offset;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Websocket {
|
||||
@Schema(description = "WebSocket服务器地址")
|
||||
private String url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package xiaozhi.modules.device.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName("ai_ota")
|
||||
@Schema(description = "固件信息")
|
||||
public class OtaEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "固件名称")
|
||||
private String firmwareName;
|
||||
|
||||
@Schema(description = "固件类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "版本号")
|
||||
private String version;
|
||||
|
||||
@Schema(description = "文件大小(字节)")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "备注/说明")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "固件路径")
|
||||
private String firmwarePath;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Date updateDate;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createDate;
|
||||
}
|
||||
@@ -1,25 +1,22 @@
|
||||
package xiaozhi.modules.device.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
|
||||
public interface DeviceService {
|
||||
|
||||
/**
|
||||
* 根据Mac地址获取设备信息
|
||||
*/
|
||||
DeviceEntity getDeviceById(String macAddress);
|
||||
public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
|
||||
/**
|
||||
* 检查设备是否激活
|
||||
*/
|
||||
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
||||
DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||
DeviceReportReqDTO deviceReport);
|
||||
|
||||
/**
|
||||
@@ -66,4 +63,29 @@ public interface DeviceService {
|
||||
* @return 用户列表分页数据
|
||||
*/
|
||||
PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto);
|
||||
|
||||
/**
|
||||
* 根据MAC地址获取设备信息
|
||||
*
|
||||
* @param macAddress MAC地址
|
||||
* @return 设备信息
|
||||
*/
|
||||
DeviceEntity getDeviceByMacAddress(String macAddress);
|
||||
|
||||
/**
|
||||
* 根据设备ID获取激活码
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 激活码
|
||||
*/
|
||||
String geCodeByDeviceId(String deviceId);
|
||||
|
||||
/**
|
||||
* 获取这个智能体设备理的最近的最后连接时间
|
||||
* @param agentId 智能体id
|
||||
* @return 返回设备最近的最后连接时间
|
||||
*/
|
||||
Date getLatestLastConnectionTime(String agentId);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.device.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
|
||||
/**
|
||||
* OTA固件管理
|
||||
*/
|
||||
public interface OtaService extends BaseService<OtaEntity> {
|
||||
PageData<OtaEntity> page(Map<String, Object> params);
|
||||
|
||||
boolean save(OtaEntity entity);
|
||||
|
||||
void update(OtaEntity entity);
|
||||
|
||||
void delete(String[] ids);
|
||||
|
||||
OtaEntity getLatestOta(String type);
|
||||
}
|
||||
+237
-75
@@ -6,22 +6,28 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.aop.framework.AopContext;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
@@ -31,37 +37,41 @@ import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||
|
||||
private final DeviceDao deviceDao;
|
||||
|
||||
private final SysUserUtilService sysUserUtilService;
|
||||
private final SysParamsService sysParamsService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final OtaService otaService;
|
||||
|
||||
private final String frontedUrl;
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
// 添加构造函数来初始化 deviceMapper
|
||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
||||
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
||||
RedisTemplate<String, Object> redisTemplate) {
|
||||
this.deviceDao = deviceDao;
|
||||
this.sysUserUtilService = sysUserUtilService;
|
||||
this.frontedUrl = frontedUrl;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceEntity getDeviceById(String deviceId) {
|
||||
LambdaQueryWrapper<DeviceEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(DeviceEntity::getId, deviceId);
|
||||
return deviceDao.selectOne(queryWrapper);
|
||||
@Async
|
||||
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
||||
try {
|
||||
DeviceEntity device = new DeviceEntity();
|
||||
device.setId(deviceId);
|
||||
device.setLastConnectedAt(new Date());
|
||||
if (StringUtils.isNotBlank(appVersion)) {
|
||||
device.setAppVersion(appVersion);
|
||||
}
|
||||
deviceDao.updateById(device);
|
||||
if (StringUtils.isNotBlank(agentId)) {
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("异步更新设备连接信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,14 +80,14 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
throw new RenException("激活码不能为空");
|
||||
}
|
||||
String deviceKey = "ota:activation:code:" + activationCode;
|
||||
Object cacheDeviceId = redisTemplate.opsForValue().get(deviceKey);
|
||||
Object cacheDeviceId = redisUtils.get(deviceKey);
|
||||
if (cacheDeviceId == null) {
|
||||
throw new RenException("激活码错误");
|
||||
}
|
||||
String deviceId = (String) cacheDeviceId;
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(cacheDeviceKey);
|
||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||
if (cacheMap == null) {
|
||||
throw new RenException("激活码错误");
|
||||
}
|
||||
@@ -107,6 +117,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
deviceEntity.setMacAddress(macAddress);
|
||||
deviceEntity.setUserId(user.getId());
|
||||
deviceEntity.setCreator(user.getId());
|
||||
deviceEntity.setAutoUpdate(1);
|
||||
deviceEntity.setCreateDate(currentTime);
|
||||
deviceEntity.setUpdater(user.getId());
|
||||
deviceEntity.setUpdateDate(currentTime);
|
||||
@@ -114,64 +125,66 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
deviceDao.insert(deviceEntity);
|
||||
|
||||
// 清理redis缓存
|
||||
redisTemplate.delete(cacheDeviceKey);
|
||||
redisTemplate.delete(deviceKey);
|
||||
redisUtils.delete(cacheDeviceKey);
|
||||
redisUtils.delete(deviceKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||
DeviceReportReqDTO deviceReport) {
|
||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||
response.setServer_time(buildServerTime());
|
||||
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
|
||||
// 在未来开发了固件更新功能,需要更换此处代码,
|
||||
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
|
||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
||||
response.setFirmware(firmware);
|
||||
|
||||
DeviceEntity deviceById = getDeviceById(deviceId);
|
||||
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
||||
deviceById.setLastConnectedAt(new Date());
|
||||
deviceDao.updateById(deviceById);
|
||||
} else { // 如果设备不存在,则生成激活码
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
||||
|
||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
||||
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
||||
|
||||
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||
String cachedCode = (String) cacheMap.get("activation_code");
|
||||
code.setCode(cachedCode);
|
||||
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||
} else {
|
||||
String newCode = RandomUtil.randomNumbers(6);
|
||||
code.setCode(newCode);
|
||||
code.setMessage(frontedUrl + "\n" + newCode);
|
||||
|
||||
Map<String, Object> dataMap = new HashMap<>();
|
||||
dataMap.put("id", deviceId);
|
||||
dataMap.put("mac_address", macAddress);
|
||||
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
|
||||
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
|
||||
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
||||
? deviceReport.getApplication().getVersion()
|
||||
: null);
|
||||
dataMap.put("deviceId", deviceId);
|
||||
dataMap.put("activation_code", newCode);
|
||||
|
||||
// 写入主数据 key
|
||||
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
||||
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
||||
|
||||
// 写入反查激活码 key
|
||||
String codeKey = "ota:activation:code:" + newCode;
|
||||
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
||||
// 设备未绑定,则返回当前上传的固件信息(不更新)以此兼容旧固件版本
|
||||
if (deviceById == null) {
|
||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
|
||||
response.setFirmware(firmware);
|
||||
} else {
|
||||
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
||||
if (deviceById.getAutoUpdate() != 0) {
|
||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||
response.setFirmware(firmware);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加WebSocket配置
|
||||
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||
websocket.setUrl(wsUrl);
|
||||
} else {
|
||||
String[] wsUrls = wsUrl.split("\\;");
|
||||
if (wsUrls.length > 0) {
|
||||
// 随机选择一个WebSocket URL
|
||||
websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]);
|
||||
} else {
|
||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/");
|
||||
}
|
||||
}
|
||||
|
||||
response.setWebsocket(websocket);
|
||||
|
||||
if (deviceById != null) {
|
||||
// 如果设备存在,则异步更新上次连接时间和版本信息
|
||||
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
|
||||
: null;
|
||||
// 通过Spring代理调用异步方法
|
||||
((DeviceServiceImpl) AopContext.currentProxy()).updateDeviceConnectionInfo(deviceById.getAgentId(),
|
||||
deviceById.getId(), appVersion);
|
||||
} else {
|
||||
// 如果设备不存在,则生成激活码
|
||||
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||
response.setActivation(code);
|
||||
}
|
||||
|
||||
@@ -221,7 +234,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
params.put(Constant.PAGE, dto.getPage());
|
||||
params.put(Constant.LIMIT, dto.getLimit());
|
||||
IPage<DeviceEntity> page = baseDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
getPage(params, "mac_address", true),
|
||||
// 定义查询条件
|
||||
new QueryWrapper<DeviceEntity>()
|
||||
// 必须设备关键词查找
|
||||
@@ -240,6 +253,16 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
return new PageData<>(list, page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceEntity getDeviceByMacAddress(String macAddress) {
|
||||
if (StringUtils.isBlank(macAddress)) {
|
||||
return null;
|
||||
}
|
||||
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("mac_address", macAddress);
|
||||
return baseDao.selectOne(wrapper);
|
||||
}
|
||||
|
||||
private DeviceReportRespDTO.ServerTime buildServerTime() {
|
||||
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
|
||||
TimeZone tz = TimeZone.getDefault();
|
||||
@@ -248,4 +271,143 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||
return serverTime;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String geCodeByDeviceId(String deviceId) {
|
||||
String dataKey = getDeviceCacheKey(deviceId);
|
||||
|
||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(dataKey);
|
||||
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||
String cachedCode = (String) cacheMap.get("activation_code");
|
||||
return cachedCode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getLatestLastConnectionTime(String agentId) {
|
||||
// 查询是否有缓存时间,有则返回
|
||||
Date cachedDate = (Date) redisUtils.get(RedisKeys.getAgentDeviceLastConnectedAtById(agentId));
|
||||
if (cachedDate != null) {
|
||||
return cachedDate;
|
||||
}
|
||||
Date maxDate = deviceDao.getAllLastConnectedAtByAgentId(agentId);
|
||||
if (maxDate != null) {
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), maxDate);
|
||||
}
|
||||
return maxDate;
|
||||
}
|
||||
|
||||
private String getDeviceCacheKey(String deviceId) {
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||
return dataKey;
|
||||
}
|
||||
|
||||
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
|
||||
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
||||
|
||||
String cachedCode = geCodeByDeviceId(deviceId);
|
||||
|
||||
if (StringUtils.isNotBlank(cachedCode)) {
|
||||
code.setCode(cachedCode);
|
||||
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||
code.setChallenge(deviceId);
|
||||
} else {
|
||||
String newCode = RandomUtil.randomNumbers(6);
|
||||
code.setCode(newCode);
|
||||
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||
code.setMessage(frontedUrl + "\n" + newCode);
|
||||
code.setChallenge(deviceId);
|
||||
|
||||
Map<String, Object> dataMap = new HashMap<>();
|
||||
dataMap.put("id", deviceId);
|
||||
dataMap.put("mac_address", deviceId);
|
||||
|
||||
dataMap.put("board", (deviceReport.getBoard() != null && deviceReport.getBoard().getType() != null)
|
||||
? deviceReport.getBoard().getType()
|
||||
: (deviceReport.getChipModelName() != null ? deviceReport.getChipModelName() : "unknown"));
|
||||
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
||||
? deviceReport.getApplication().getVersion()
|
||||
: null);
|
||||
|
||||
dataMap.put("deviceId", deviceId);
|
||||
dataMap.put("activation_code", newCode);
|
||||
|
||||
// 写入主数据 key
|
||||
String dataKey = getDeviceCacheKey(deviceId);
|
||||
redisUtils.set(dataKey, dataMap);
|
||||
|
||||
// 写入反查激活码 key
|
||||
String codeKey = "ota:activation:code:" + newCode;
|
||||
redisUtils.set(codeKey, deviceId);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private DeviceReportRespDTO.Firmware buildFirmwareInfo(String type, String currentVersion) {
|
||||
if (StringUtils.isBlank(type)) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isBlank(currentVersion)) {
|
||||
currentVersion = "0.0.0";
|
||||
}
|
||||
|
||||
OtaEntity ota = otaService.getLatestOta(type);
|
||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||
String downloadUrl = null;
|
||||
|
||||
if (ota != null) {
|
||||
// 如果设备没有版本信息,或者OTA版本比设备版本新,则返回下载地址
|
||||
if (compareVersions(ota.getVersion(), currentVersion) > 0) {
|
||||
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||
log.error("OTA地址未配置,请登录智控台,在参数管理找到【server.ota】配置");
|
||||
// 尝试从请求中获取
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes())
|
||||
.getRequest();
|
||||
otaUrl = request.getRequestURL().toString();
|
||||
}
|
||||
// 将URL中的/ota/替换为/otaMag/download/
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getOtaIdKey(uuid), ota.getId());
|
||||
downloadUrl = otaUrl.replace("/ota/", "/otaMag/download/") + uuid;
|
||||
}
|
||||
}
|
||||
|
||||
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
|
||||
firmware.setUrl(downloadUrl == null ? Constant.INVALID_FIRMWARE_URL : downloadUrl);
|
||||
return firmware;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个版本号
|
||||
*
|
||||
* @param version1 版本1
|
||||
* @param version2 版本2
|
||||
* @return 如果version1 > version2返回1,version1 < version2返回-1,相等返回0
|
||||
*/
|
||||
private static int compareVersions(String version1, String version2) {
|
||||
if (version1 == null || version2 == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String[] v1Parts = version1.split("\\.");
|
||||
String[] v2Parts = version2.split("\\.");
|
||||
|
||||
int length = Math.max(v1Parts.length, v2Parts.length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int v1 = i < v1Parts.length ? Integer.parseInt(v1Parts[i]) : 0;
|
||||
int v2 = i < v2Parts.length ? Integer.parseInt(v2Parts[i]) : 0;
|
||||
|
||||
if (v1 > v2) {
|
||||
return 1;
|
||||
} else if (v1 < v2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.modules.device.dao.OtaDao;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
|
||||
@Service
|
||||
public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implements OtaService {
|
||||
|
||||
@Override
|
||||
public PageData<OtaEntity> page(Map<String, Object> params) {
|
||||
IPage<OtaEntity> page = baseDao.selectPage(
|
||||
getPage(params, "update_date", true),
|
||||
getWrapper(params));
|
||||
|
||||
return new PageData<>(page.getRecords(), page.getTotal());
|
||||
}
|
||||
|
||||
private QueryWrapper<OtaEntity> getWrapper(Map<String, Object> params) {
|
||||
String firmwareName = (String) params.get("firmwareName");
|
||||
|
||||
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.like(StringUtils.isNotBlank(firmwareName), "firmware_name", firmwareName);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(OtaEntity entity) {
|
||||
// 检查是否存在相同类型和版本的固件(排除当前记录)
|
||||
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||
.eq("type", entity.getType())
|
||||
.eq("version", entity.getVersion())
|
||||
.ne("id", entity.getId()); // 排除当前记录
|
||||
|
||||
if (baseDao.selectCount(queryWrapper) > 0) {
|
||||
throw new RuntimeException("已存在相同类型和版本的固件,请修改后重试");
|
||||
}
|
||||
|
||||
entity.setUpdateDate(new Date());
|
||||
baseDao.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(OtaEntity entity) {
|
||||
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||
.eq("type", entity.getType());
|
||||
// 同类固件只保留最新的一条
|
||||
List<OtaEntity> otaList = baseDao.selectList(queryWrapper);
|
||||
if (otaList != null && otaList.size() > 0) {
|
||||
OtaEntity otaBefore = otaList.getFirst();
|
||||
entity.setId(otaBefore.getId());
|
||||
baseDao.updateById(entity);
|
||||
return true;
|
||||
}
|
||||
return baseDao.insert(entity) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OtaEntity getLatestOta(String type) {
|
||||
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("type", type)
|
||||
.orderByDesc("update_date")
|
||||
.last("LIMIT 1");
|
||||
return baseDao.selectOne(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package xiaozhi.modules.device.utils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 网络工具类
|
||||
*/
|
||||
public class NetworkUtil {
|
||||
/**
|
||||
* MAC地址正则表达式
|
||||
*/
|
||||
private static final Pattern macPattern = Pattern.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
|
||||
|
||||
/**
|
||||
* 判断MAC地址是否合法
|
||||
*/
|
||||
public static boolean isMacAddressValid(String mac) {
|
||||
if (StringUtils.isBlank(mac)) {
|
||||
return false;
|
||||
}
|
||||
// 正则校验格式
|
||||
if (!macPattern.matcher(mac).matches()) {
|
||||
return false;
|
||||
}
|
||||
// 校验MAC地址是否为单播地址
|
||||
String normalized = mac.toLowerCase();
|
||||
String[] parts = normalized.split("[:-]");
|
||||
int firstByte = Integer.parseInt(parts[0], 16);
|
||||
return (firstByte & 1) == 0; // 最低位为0表示单播地址,合法
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||
@@ -38,6 +40,8 @@ public class ModelController {
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final TimbreService timbreService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final ConfigService configService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
|
||||
@GetMapping("/names")
|
||||
@Operation(summary = "获取所有模型名称")
|
||||
@@ -75,6 +79,7 @@ public class ModelController {
|
||||
@PathVariable String provideCode,
|
||||
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
ModelConfigDTO modelConfigDTO = modelConfigService.add(modelType, provideCode, modelConfigBodyDTO);
|
||||
configService.getConfig(false);
|
||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||
}
|
||||
|
||||
@@ -86,6 +91,7 @@ public class ModelController {
|
||||
@PathVariable String id,
|
||||
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
ModelConfigDTO modelConfigDTO = modelConfigService.edit(modelType, provideCode, id, modelConfigBodyDTO);
|
||||
configService.getConfig(false);
|
||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||
}
|
||||
|
||||
@@ -119,6 +125,27 @@ public class ModelController {
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@PutMapping("/default/{id}")
|
||||
@Operation(summary = "设置默认模型")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> setDefaultModel(@PathVariable String id) {
|
||||
ModelConfigEntity entity = modelConfigService.selectById(id);
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("模型配置不存在");
|
||||
}
|
||||
// 将其他模型设置为非默认
|
||||
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||
entity.setIsEnabled(1);
|
||||
entity.setIsDefault(1);
|
||||
modelConfigService.updateById(entity);
|
||||
|
||||
// 更新模板表中对应的模型ID
|
||||
agentTemplateService.updateDefaultTemplateModelId(entity.getModelType(), entity.getId());
|
||||
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@GetMapping("/{modelId}/voices")
|
||||
@Operation(summary = "获取模型音色")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package xiaozhi.modules.model.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/models/provider")
|
||||
@Tag(name = "模型供应器")
|
||||
public class ModelProviderController {
|
||||
|
||||
private final ModelProviderService modelProviderService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取模型供应器列表")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<ModelProviderDTO>> getListPage(ModelProviderDTO modelProviderDTO,
|
||||
@RequestParam(required = true, defaultValue = "0") String page,
|
||||
@RequestParam(required = true, defaultValue = "10") String limit) {
|
||||
return new Result<PageData<ModelProviderDTO>>()
|
||||
.ok(modelProviderService.getListPage(modelProviderDTO, page, limit));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "新增模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelProviderDTO> add(@RequestBody @Validated ModelProviderDTO modelProviderDTO) {
|
||||
ModelProviderDTO resp = modelProviderService.add(modelProviderDTO);
|
||||
return new Result<ModelProviderDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "修改模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelProviderDTO> edit(@RequestBody @Validated(UpdateGroup.class) ModelProviderDTO modelProviderDTO) {
|
||||
ModelProviderDTO resp = modelProviderService.edit(modelProviderDTO);
|
||||
return new Result<ModelProviderDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||
public Result<Void> delete(@RequestBody List<String> ids) {
|
||||
modelProviderService.delete(ids);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,29 +8,37 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
|
||||
@Data
|
||||
@Schema(description = "模型供应器/商")
|
||||
public class ModelProviderDTO implements Serializable {
|
||||
//
|
||||
// @Schema(description = "主键")
|
||||
// private Long id;
|
||||
@Schema(description = "主键")
|
||||
@NotBlank(message = "id不能为空", groups = UpdateGroup.class)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||
@NotBlank(message = "modelType不能为空")
|
||||
private String modelType;
|
||||
|
||||
@Schema(description = "供应器类型")
|
||||
@NotBlank(message = "providerCode不能为空")
|
||||
private String providerCode;
|
||||
|
||||
@Schema(description = "供应器名称")
|
||||
@NotBlank(message = "name不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "供应器字段列表(JSON格式)")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@NotBlank(message = "fields(JSON格式)不能为空")
|
||||
private String fields;
|
||||
|
||||
@Schema(description = "排序")
|
||||
@NotNull(message = "sort不能为空")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
|
||||
@@ -3,10 +3,8 @@ package xiaozhi.modules.model.entity;
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -30,7 +28,6 @@ public class ModelProviderEntity {
|
||||
private String name;
|
||||
|
||||
@Schema(description = "供应器字段列表(JSON格式)")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private String fields;
|
||||
|
||||
@Schema(description = "排序")
|
||||
|
||||
@@ -28,4 +28,21 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
||||
* @return 模型名称
|
||||
*/
|
||||
String getModelNameById(String id);
|
||||
|
||||
/**
|
||||
* 根据ID获取模型配置
|
||||
*
|
||||
* @param id 模型ID
|
||||
* @param isCache 是否缓存
|
||||
* @return 模型配置实体
|
||||
*/
|
||||
ModelConfigEntity getModelById(String id, boolean isCache);
|
||||
|
||||
/**
|
||||
* 设置默认模型
|
||||
*
|
||||
* @param modelType 模型类型
|
||||
* @param isDefault 是否默认
|
||||
*/
|
||||
void setDefaultModel(String modelType, int isDefault);
|
||||
}
|
||||
|
||||
+8
-4
@@ -2,8 +2,8 @@ package xiaozhi.modules.model.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
|
||||
public interface ModelProviderService {
|
||||
|
||||
@@ -11,11 +11,15 @@ public interface ModelProviderService {
|
||||
|
||||
List<ModelProviderDTO> getListByModelType(String modelType);
|
||||
|
||||
ModelProviderDTO add(ModelProviderEntity modelProviderEntity);
|
||||
ModelProviderDTO add(ModelProviderDTO modelProviderDTO);
|
||||
|
||||
ModelProviderDTO edit(ModelProviderEntity modelProviderEntity);
|
||||
ModelProviderDTO edit(ModelProviderDTO modelProviderDTO);
|
||||
|
||||
void delete();
|
||||
void delete(String id);
|
||||
|
||||
void delete(List<String> id);
|
||||
|
||||
PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit);
|
||||
|
||||
List<ModelProviderDTO> getList(String modelType, String provideCode);
|
||||
}
|
||||
|
||||
+89
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.service.impl;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -19,6 +20,8 @@ import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
@@ -36,12 +39,14 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
private final ModelConfigDao modelConfigDao;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final AgentDao agentDao;
|
||||
|
||||
@Override
|
||||
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
|
||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType)
|
||||
.eq("is_enabled", 1)
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||
.select("id", "model_name"));
|
||||
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||
@@ -74,6 +79,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
// 再保存供应器提供的模型
|
||||
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||
modelConfigEntity.setModelType(modelType);
|
||||
modelConfigEntity.setIsDefault(0);
|
||||
modelConfigDao.insert(modelConfigEntity);
|
||||
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||
}
|
||||
@@ -94,14 +100,71 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
modelConfigEntity.setId(id);
|
||||
modelConfigEntity.setModelType(modelType);
|
||||
modelConfigDao.updateById(modelConfigEntity);
|
||||
// 清除缓存
|
||||
redisUtils.delete(RedisKeys.getModelConfigById(modelConfigEntity.getId()));
|
||||
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
// 查看是否是默认
|
||||
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
|
||||
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
|
||||
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
|
||||
}
|
||||
// 验证是否有引用
|
||||
checkAgentReference(id);
|
||||
checkIntentConfigReference(id);
|
||||
|
||||
modelConfigDao.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查智能体配置是否有引用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
private void checkAgentReference(String modelId) {
|
||||
List<AgentEntity> agents = agentDao.selectList(
|
||||
new QueryWrapper<AgentEntity>()
|
||||
.eq("vad_model_id", modelId)
|
||||
.or()
|
||||
.eq("asr_model_id", modelId)
|
||||
.or()
|
||||
.eq("llm_model_id", modelId)
|
||||
.or()
|
||||
.eq("tts_model_id", modelId)
|
||||
.or()
|
||||
.eq("mem_model_id", modelId)
|
||||
.or()
|
||||
.eq("intent_model_id", modelId));
|
||||
if (!agents.isEmpty()) {
|
||||
String agentNames = agents.stream()
|
||||
.map(AgentEntity::getAgentName)
|
||||
.collect(Collectors.joining("、"));
|
||||
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查意图识别配置是否有引用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
private void checkIntentConfigReference(String modelId) {
|
||||
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
|
||||
if (modelConfig != null
|
||||
&& "LLM".equals(modelConfig.getModelType() == null ? null : modelConfig.getModelType().toUpperCase())) {
|
||||
List<ModelConfigEntity> intentConfigs = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", "Intent")
|
||||
.like("config_json", "%" + modelId + "%"));
|
||||
if (!intentConfigs.isEmpty()) {
|
||||
throw new RenException("该LLM模型已被意图识别配置引用,无法删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelNameById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
@@ -125,4 +188,30 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigEntity getModelById(String id, boolean isCache) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
if (isCache) {
|
||||
ModelConfigEntity cachedConfig = (ModelConfigEntity) redisUtils.get(RedisKeys.getModelConfigById(id));
|
||||
if (cachedConfig != null) {
|
||||
return ConvertUtils.sourceToTarget(cachedConfig, ModelConfigEntity.class);
|
||||
}
|
||||
}
|
||||
ModelConfigEntity entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
redisUtils.set(RedisKeys.getModelConfigById(id), entity);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultModel(String modelType, int isDefault) {
|
||||
ModelConfigEntity entity = new ModelConfigEntity();
|
||||
entity.setIsDefault(isDefault);
|
||||
modelConfigDao.update(entity, new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType));
|
||||
}
|
||||
}
|
||||
|
||||
+75
-5
@@ -1,19 +1,29 @@
|
||||
package xiaozhi.modules.model.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import cn.hutool.json.JSONArray;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.model.dao.ModelProviderDao;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@@ -32,18 +42,78 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProviderDTO add(ModelProviderEntity modelProviderEntity) {
|
||||
return null;
|
||||
public PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit) {
|
||||
|
||||
Map<String, Object> params = new HashMap<String, Object>();
|
||||
params.put(Constant.PAGE, page);
|
||||
params.put(Constant.LIMIT, limit);
|
||||
params.put(Constant.ORDER_FIELD, List.of("model_type", "sort"));
|
||||
params.put(Constant.ORDER, "asc");
|
||||
|
||||
IPage<ModelProviderEntity> pageParam = getPage(params, null, true);
|
||||
|
||||
QueryWrapper<ModelProviderEntity> wrapper = new QueryWrapper<ModelProviderEntity>();
|
||||
|
||||
if (StringUtils.isNotBlank(modelProviderDTO.getModelType())) {
|
||||
wrapper.eq("model_type", modelProviderDTO.getModelType());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(modelProviderDTO.getName())) {
|
||||
wrapper.and(w -> w.like("name", modelProviderDTO.getName())
|
||||
.or()
|
||||
.like("provider_code", modelProviderDTO.getName()));
|
||||
}
|
||||
return getPageData(modelProviderDao.selectPage(pageParam, wrapper), ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String jsonString = "\"[]\"";
|
||||
JSONArray jsonArray = new JSONArray(jsonString);
|
||||
System.out.println("字符串转 JSONArray: " + jsonArray.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProviderDTO edit(ModelProviderEntity modelProviderEntity) {
|
||||
return null;
|
||||
public ModelProviderDTO add(ModelProviderDTO modelProviderDTO) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
modelProviderDTO.setCreator(user.getId());
|
||||
modelProviderDTO.setUpdater(user.getId());
|
||||
modelProviderDTO.setCreateDate(new Date());
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
// 去除Fields左右的双引号
|
||||
|
||||
modelProviderDTO.setFields(modelProviderDTO.getFields());
|
||||
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
|
||||
if (modelProviderDao.insert(entity) == 0) {
|
||||
throw new RenException("新增数据失败");
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
public ModelProviderDTO edit(ModelProviderDTO modelProviderDTO) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
modelProviderDTO.setUpdater(user.getId());
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
if (modelProviderDao
|
||||
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||
throw new RenException("修改数据失败");
|
||||
}
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
if (modelProviderDao.deleteById(id) == 0) {
|
||||
throw new RenException("删除数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(List<String> ids) {
|
||||
if (modelProviderDao.deleteBatchIds(ids) == 0) {
|
||||
throw new RenException("删除数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import jakarta.servlet.Filter;
|
||||
import xiaozhi.modules.security.oauth2.Oauth2Filter;
|
||||
import xiaozhi.modules.security.oauth2.Oauth2Realm;
|
||||
import xiaozhi.modules.security.secret.ServerSecretFilter;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* Shiro的配置文件
|
||||
@@ -46,7 +48,7 @@ public class ShiroConfig {
|
||||
}
|
||||
|
||||
@Bean("shiroFilter")
|
||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
|
||||
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) {
|
||||
ShiroFilterConfiguration config = new ShiroFilterConfiguration();
|
||||
config.setFilterOncePerRequest(true);
|
||||
|
||||
@@ -54,9 +56,11 @@ public class ShiroConfig {
|
||||
shiroFilter.setSecurityManager(securityManager);
|
||||
shiroFilter.setShiroFilterConfiguration(config);
|
||||
|
||||
// oauth过滤
|
||||
Map<String, Filter> filters = new HashMap<>();
|
||||
// oauth过滤
|
||||
filters.put("oauth2", new Oauth2Filter());
|
||||
// 服务密钥过滤
|
||||
filters.put("server", new ServerSecretFilter(sysParamsService));
|
||||
shiroFilter.setFilters(filters);
|
||||
|
||||
// 添加Shiro的内置过滤器
|
||||
@@ -69,15 +73,23 @@ public class ShiroConfig {
|
||||
*/
|
||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||
filterMap.put("/ota/**", "anon");
|
||||
filterMap.put("/otaMag/download/**", "anon");
|
||||
filterMap.put("/webjars/**", "anon");
|
||||
filterMap.put("/druid/**", "anon");
|
||||
filterMap.put("/v3/api-docs/**", "anon");
|
||||
filterMap.put("/doc.html", "anon");
|
||||
filterMap.put("/favicon.ico", "anon");
|
||||
filterMap.put("/user/captcha", "anon");
|
||||
filterMap.put("/user/smsVerification", "anon");
|
||||
filterMap.put("/user/login", "anon");
|
||||
filterMap.put("/user/pub-config", "anon");
|
||||
filterMap.put("/user/register", "anon");
|
||||
filterMap.put("/device/register", "anon");
|
||||
filterMap.put("/user/retrieve-password", "anon");
|
||||
// 将config路径使用server服务过滤器
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/saveMemory/**", "server");
|
||||
filterMap.put("/agent/play/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
@@ -95,4 +107,4 @@ public class ShiroConfig {
|
||||
advisor.setSecurityManager(securityManager);
|
||||
return advisor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package xiaozhi.modules.security.config;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
@@ -18,6 +19,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
@@ -33,11 +43,15 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
// 特殊用途的转换器
|
||||
converters.add(new ByteArrayHttpMessageConverter());
|
||||
converters.add(new StringHttpMessageConverter());
|
||||
converters.add(new ResourceHttpMessageConverter());
|
||||
converters.add(new AllEncompassingFormHttpMessageConverter());
|
||||
|
||||
// 通用转换器
|
||||
converters.add(new StringHttpMessageConverter());
|
||||
converters.add(new AllEncompassingFormHttpMessageConverter());
|
||||
|
||||
// JSON 转换器
|
||||
converters.add(jackson2HttpMessageConverter());
|
||||
}
|
||||
|
||||
@@ -49,10 +63,29 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
// 忽略未知属性
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
// 日期格式转换
|
||||
// mapper.setDateFormat(new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN));
|
||||
// 设置时区
|
||||
mapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
|
||||
|
||||
// 配置Java8日期时间序列化
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
javaTimeModule.addSerializer(java.time.LocalDateTime.class, new LocalDateTimeSerializer(
|
||||
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
|
||||
javaTimeModule.addSerializer(java.time.LocalDate.class, new LocalDateSerializer(
|
||||
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
|
||||
javaTimeModule.addSerializer(java.time.LocalTime.class,
|
||||
new LocalTimeSerializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||
javaTimeModule.addDeserializer(java.time.LocalDateTime.class, new LocalDateTimeDeserializer(
|
||||
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_TIME_PATTERN)));
|
||||
javaTimeModule.addDeserializer(java.time.LocalDate.class, new LocalDateDeserializer(
|
||||
java.time.format.DateTimeFormatter.ofPattern(DateUtils.DATE_PATTERN)));
|
||||
javaTimeModule.addDeserializer(java.time.LocalTime.class,
|
||||
new LocalTimeDeserializer(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||
mapper.registerModule(javaTimeModule);
|
||||
|
||||
// 配置java.util.Date的序列化和反序列化
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat(DateUtils.DATE_TIME_PATTERN);
|
||||
mapper.setDateFormat(dateFormat);
|
||||
|
||||
// Long类型转String类型
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||
|
||||
+108
-8
@@ -1,6 +1,10 @@
|
||||
package xiaozhi.modules.security.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -13,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.TokenDTO;
|
||||
@@ -21,13 +26,18 @@ import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.security.dto.LoginDTO;
|
||||
import xiaozhi.modules.security.dto.SmsVerificationDTO;
|
||||
import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.security.service.CaptchaService;
|
||||
import xiaozhi.modules.security.service.SysUserTokenService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
|
||||
/**
|
||||
* 登录控制层
|
||||
@@ -40,24 +50,43 @@ public class LoginController {
|
||||
private final SysUserService sysUserService;
|
||||
private final SysUserTokenService sysUserTokenService;
|
||||
private final CaptchaService captchaService;
|
||||
private final SysParamsService sysParamsService;
|
||||
private final SysDictDataService sysDictDataService;
|
||||
|
||||
@GetMapping("/captcha")
|
||||
@Operation(summary = "验证码")
|
||||
public void captcha(HttpServletResponse response, String uuid) throws IOException {
|
||||
// uuid不能为空
|
||||
AssertUtils.isBlank(uuid, ErrorCode.IDENTIFIER_NOT_NULL);
|
||||
|
||||
// 生成验证码
|
||||
captchaService.create(response, uuid);
|
||||
}
|
||||
|
||||
@PostMapping("/smsVerification")
|
||||
@Operation(summary = "短信验证码")
|
||||
public Result<Void> smsVerification(@RequestBody SmsVerificationDTO dto) {
|
||||
// 验证图形验证码
|
||||
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
|
||||
if (!validate) {
|
||||
throw new RenException("图形验证码错误");
|
||||
}
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
if (!isMobileRegister) {
|
||||
throw new RenException("没有开启手机注册,没法使用短信验证码功能");
|
||||
}
|
||||
// 发送短信验证码
|
||||
captchaService.sendSMSValidateCode(dto.getPhone());
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "登录")
|
||||
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
||||
if (!validate) {
|
||||
throw new RenException("验证码错误,请重新获取");
|
||||
throw new RenException("图形验证码错误,请重新获取");
|
||||
}
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
@@ -75,11 +104,32 @@ public class LoginController {
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "注册")
|
||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||
if (!validate) {
|
||||
throw new RenException("验证码错误,请重新获取");
|
||||
if (!sysUserService.getAllowUserRegister()) {
|
||||
throw new RenException("当前不允许普通用户注册");
|
||||
}
|
||||
// 是否开启手机注册
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
boolean validate;
|
||||
if (isMobileRegister) {
|
||||
// 验证用户是否是手机号码
|
||||
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
|
||||
if (!validPhone) {
|
||||
throw new RenException("用户名不是手机号码,请重新输入");
|
||||
}
|
||||
// 验证短信验证码是否正常
|
||||
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
|
||||
if (!validate) {
|
||||
throw new RenException("手机验证码错误,请重新获取");
|
||||
}
|
||||
} else {
|
||||
// 验证是否正确输入验证码
|
||||
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
||||
if (!validate) {
|
||||
throw new RenException("图形验证码错误,请重新获取");
|
||||
}
|
||||
}
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
if (userDTO != null) {
|
||||
@@ -90,7 +140,6 @@ public class LoginController {
|
||||
userDTO.setPassword(login.getPassword());
|
||||
sysUserService.save(userDTO);
|
||||
return new Result<>();
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/info")
|
||||
@@ -111,4 +160,55 @@ public class LoginController {
|
||||
sysUserTokenService.changePassword(userId, passwordDTO);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PutMapping("/retrieve-password")
|
||||
@Operation(summary = "找回密码")
|
||||
public Result<?> retrievePassword(@RequestBody RetrievePasswordDTO dto) {
|
||||
// 是否开启手机注册
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
if (!isMobileRegister) {
|
||||
throw new RenException("没有开启手机注册,没法使用找回密码功能");
|
||||
}
|
||||
// 判断非空
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
// 验证用户是否是手机号码
|
||||
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
|
||||
if (!validPhone) {
|
||||
throw new RenException("输入的手机号码格式不正确");
|
||||
}
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
|
||||
if (userDTO == null) {
|
||||
throw new RenException("输入的手机号码未注册");
|
||||
}
|
||||
// 验证短信验证码是否正常
|
||||
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
|
||||
// 判断是否通过验证
|
||||
if (!validate) {
|
||||
throw new RenException("输入的手机验证码错误");
|
||||
}
|
||||
|
||||
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@GetMapping("/pub-config")
|
||||
@Operation(summary = "公共配置")
|
||||
public Result<Map<String, Object>> pubConfig() {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("enableMobileRegister", sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class));
|
||||
config.put("version", Constant.VERSION);
|
||||
config.put("year", "©" + Calendar.getInstance().get(Calendar.YEAR));
|
||||
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(Constant.DictType.MOBILE_AREA.getValue());
|
||||
config.put("mobileAreaList", list);
|
||||
config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true));
|
||||
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
|
||||
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
|
||||
|
||||
return new Result<Map<String, Object>>().ok(config);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ public class LoginDTO implements Serializable {
|
||||
@NotBlank(message = "{sysuser.captcha.require}")
|
||||
private String captcha;
|
||||
|
||||
@Schema(description = "手机验证码")
|
||||
private String mobileCaptcha;
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
@NotBlank(message = "{sysuser.uuid.require}")
|
||||
private String captchaId;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package xiaozhi.modules.security.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 短信验证码请求DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "短信验证码请求")
|
||||
public class SmsVerificationDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "手机号码")
|
||||
@NotBlank(message = "{sysuser.username.require}")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "验证码")
|
||||
@NotBlank(message = "{sysuser.captcha.require}")
|
||||
private String captcha;
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
@NotBlank(message = "{sysuser.uuid.require}")
|
||||
private String captchaId;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package xiaozhi.modules.security.secret;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.utils.HttpContextUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* Config API 过滤器
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ServerSecretFilter extends AuthenticatingFilter {
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
@Override
|
||||
protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) {
|
||||
// 获取请求token
|
||||
String token = getRequestToken((HttpServletRequest) request);
|
||||
|
||||
if (StringUtils.isBlank(token)) {
|
||||
log.warn("createToken:token is empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ServerSecretToken(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
||||
// 对OPTIONS请求放行
|
||||
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
|
||||
// 获取token并校验
|
||||
String token = getRequestToken((HttpServletRequest) servletRequest);
|
||||
if (StringUtils.isBlank(token)) {
|
||||
// token为空,返回401
|
||||
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "服务器密钥不能为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证token是否匹配
|
||||
String serverSecret = getServerSecret();
|
||||
if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) {
|
||||
// token无效,返回401
|
||||
this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的服务器密钥");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送未授权响应
|
||||
*/
|
||||
private void sendUnauthorizedResponse(HttpServletResponse response, String message) {
|
||||
response.setContentType("application/json;charset=utf-8");
|
||||
response.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
||||
|
||||
try {
|
||||
String json = JsonUtils.toJsonString(new Result<Void>().error(ErrorCode.UNAUTHORIZED, message));
|
||||
response.getWriter().print(json);
|
||||
} catch (IOException e) {
|
||||
log.error("响应输出失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求的token
|
||||
*/
|
||||
private String getRequestToken(HttpServletRequest httpRequest) {
|
||||
String token = null;
|
||||
// 从header中获取token
|
||||
String authorization = httpRequest.getHeader("Authorization");
|
||||
if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
|
||||
token = authorization.replace("Bearer ", "");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private String getServerSecret() {
|
||||
return sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package xiaozhi.modules.security.secret;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
|
||||
/**
|
||||
* Config API Token
|
||||
*/
|
||||
public class ServerSecretToken implements AuthenticationToken {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final String token;
|
||||
|
||||
public ServerSecretToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
+22
-4
@@ -18,10 +18,28 @@ public interface CaptchaService {
|
||||
|
||||
/**
|
||||
* 验证码效验
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @param code 验证码
|
||||
*
|
||||
* @param uuid uuid
|
||||
* @param code 验证码
|
||||
* @param delete 是否删除验证码
|
||||
* @return true:成功 false:失败
|
||||
*/
|
||||
boolean validate(String uuid, String code);
|
||||
boolean validate(String uuid, String code, Boolean delete);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param phone 手机
|
||||
*/
|
||||
void sendSMSValidateCode(String phone);
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*
|
||||
* @param phone 手机
|
||||
* @param code 验证码
|
||||
* @param delete 是否删除验证码
|
||||
* @return true:成功 false:失败
|
||||
*/
|
||||
boolean validateSMSValidateCode(String phone, String code, Boolean delete);
|
||||
}
|
||||
|
||||
+89
-4
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.security.service.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -14,9 +15,13 @@ import com.wf.captcha.base.Captcha;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.security.service.CaptchaService;
|
||||
import xiaozhi.modules.sms.service.SmsService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
@@ -25,6 +30,10 @@ import xiaozhi.modules.security.service.CaptchaService;
|
||||
public class CaptchaServiceImpl implements CaptchaService {
|
||||
@Resource
|
||||
private RedisUtils redisUtils;
|
||||
@Resource
|
||||
private SmsService smsService;
|
||||
@Resource
|
||||
private SysParamsService sysParamsService;
|
||||
@Value("${renren.redis.open}")
|
||||
private boolean open;
|
||||
/**
|
||||
@@ -51,12 +60,12 @@ public class CaptchaServiceImpl implements CaptchaService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(String uuid, String code) {
|
||||
public boolean validate(String uuid, String code, Boolean delete) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return false;
|
||||
}
|
||||
// 获取验证码
|
||||
String captcha = getCache(uuid);
|
||||
String captcha = getCache(uuid, delete);
|
||||
|
||||
// 效验成功
|
||||
if (code.equalsIgnoreCase(captcha)) {
|
||||
@@ -66,21 +75,97 @@ public class CaptchaServiceImpl implements CaptchaService {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSMSValidateCode(String phone) {
|
||||
// 检查发送间隔
|
||||
String lastSendTimeKey = RedisKeys.getSMSLastSendTimeKey(phone);
|
||||
// 获取是否发送过,如果没有设置最后发送时间(60秒)
|
||||
String lastSendTime = redisUtils
|
||||
.getKeyOrCreate(lastSendTimeKey,
|
||||
String.valueOf(System.currentTimeMillis()), 60L);
|
||||
if (lastSendTime != null) {
|
||||
long lastSendTimeLong = Long.parseLong(lastSendTime);
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeDiff = currentTime - lastSendTimeLong;
|
||||
if (timeDiff < 60000) {
|
||||
throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查今日发送次数
|
||||
String todayCountKey = RedisKeys.getSMSTodayCountKey(phone);
|
||||
Integer todayCount = (Integer) redisUtils.get(todayCountKey);
|
||||
if (todayCount == null) {
|
||||
todayCount = 0;
|
||||
}
|
||||
|
||||
// 获取最大发送次数限制
|
||||
Integer maxSendCount = sysParamsService.getValueObject(
|
||||
Constant.SysMSMParam.SERVER_SMS_MAX_SEND_COUNT.getValue(),
|
||||
Integer.class);
|
||||
if (maxSendCount == null) {
|
||||
maxSendCount = 5; // 默认值
|
||||
}
|
||||
|
||||
if (todayCount >= maxSendCount) {
|
||||
throw new RenException("今日发送次数已达上限");
|
||||
}
|
||||
|
||||
String key = RedisKeys.getSMSValidateCodeKey(phone);
|
||||
String validateCodes = generateValidateCode(6);
|
||||
|
||||
// 设置验证码
|
||||
setCache(key, validateCodes);
|
||||
|
||||
// 更新今日发送次数
|
||||
if (todayCount == 0) {
|
||||
redisUtils.increment(todayCountKey, RedisUtils.DEFAULT_EXPIRE);
|
||||
} else {
|
||||
redisUtils.increment(todayCountKey);
|
||||
}
|
||||
|
||||
// 发送验证码短信
|
||||
smsService.sendVerificationCodeSms(phone, validateCodes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateSMSValidateCode(String phone, String code, Boolean delete) {
|
||||
String key = RedisKeys.getSMSValidateCodeKey(phone);
|
||||
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) {
|
||||
if (open) {
|
||||
key = RedisKeys.getCaptchaKey(key);
|
||||
// 设置5分钟过期
|
||||
redisUtils.set(key, value, 300);
|
||||
} else {
|
||||
localCache.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private String getCache(String key) {
|
||||
private String getCache(String key, Boolean delete) {
|
||||
if (open) {
|
||||
key = RedisKeys.getCaptchaKey(key);
|
||||
String captcha = (String) redisUtils.get(key);
|
||||
// 删除验证码
|
||||
if (captcha != null) {
|
||||
if (captcha != null && delete) {
|
||||
redisUtils.delete(key);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package xiaozhi.modules.sms.service;
|
||||
|
||||
/**
|
||||
* 短信服务的方法定义接口
|
||||
*
|
||||
* @author zjy
|
||||
* @since 2025-05-12
|
||||
*/
|
||||
public interface SmsService {
|
||||
|
||||
/**
|
||||
* 发送验证码短信
|
||||
* @param phone 手机号码
|
||||
* @param VerificationCode 验证码
|
||||
*/
|
||||
void sendVerificationCodeSms(String phone, String VerificationCode) ;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package xiaozhi.modules.sms.service.imp;
|
||||
|
||||
import com.aliyun.dysmsapi20170525.Client;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
|
||||
import com.aliyun.teaopenapi.models.Config;
|
||||
import com.aliyun.teautil.models.RuntimeOptions;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.sms.service.SmsService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class ALiYunSmsService implements SmsService {
|
||||
private final SysParamsService sysParamsService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public void sendVerificationCodeSms(String phone, String VerificationCode) {
|
||||
Client client = createClient();
|
||||
String SignName = sysParamsService.getValue(Constant.SysMSMParam
|
||||
.ALIYUN_SMS_SIGN_NAME.getValue(),true);
|
||||
String TemplateCode = sysParamsService.getValue(Constant.SysMSMParam
|
||||
.ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE.getValue(),true);
|
||||
try {
|
||||
SendSmsRequest sendSmsRequest = new SendSmsRequest()
|
||||
.setSignName(SignName)
|
||||
.setTemplateCode(TemplateCode)
|
||||
.setPhoneNumbers(phone)
|
||||
.setTemplateParam(String.format("{\"code\":\"%s\"}", VerificationCode));
|
||||
RuntimeOptions runtime = new RuntimeOptions();
|
||||
// 复制代码运行请自行打印 API 的返回值
|
||||
SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
|
||||
log.info("发送短信响应的requestID: {}", sendSmsResponse.getBody().getRequestId());
|
||||
} catch (Exception e) {
|
||||
// 如果发送失败了退还这次发送数
|
||||
String todayCountKey = RedisKeys.getSMSTodayCountKey(phone);
|
||||
redisUtils.delete(todayCountKey);
|
||||
// 错误 message
|
||||
log.error(e.getMessage());
|
||||
throw new RenException("短信发送失败");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建阿里云连接
|
||||
* @return 返回连接对象
|
||||
*/
|
||||
private Client createClient(){
|
||||
String ACCESS_KEY_ID = sysParamsService.getValue(Constant.SysMSMParam
|
||||
.ALIYUN_SMS_ACCESS_KEY_ID.getValue(),true);
|
||||
String ACCESS_KEY_SECRET = sysParamsService.getValue(Constant.SysMSMParam
|
||||
.ALIYUN_SMS_ACCESS_KEY_SECRET.getValue(),true);
|
||||
try {
|
||||
Config config = new Config()
|
||||
.setAccessKeyId(ACCESS_KEY_ID)
|
||||
.setAccessKeySecret(ACCESS_KEY_SECRET);
|
||||
// 配置 Endpoint。中国站请使用dysmsapi.aliyuncs.com
|
||||
config.endpoint = "dysmsapi.aliyuncs.com";
|
||||
return new Client(config);
|
||||
}catch (Exception e){
|
||||
// 错误 message
|
||||
log.error(e.getMessage());
|
||||
throw new RenException("短信连接建立失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,6 @@ public class AdminController {
|
||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||
dto.setPage((String) params.get(Constant.PAGE));
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||
}
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package xiaozhi.modules.sys.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
|
||||
import xiaozhi.modules.sys.dto.ServerActionPayloadDTO;
|
||||
import xiaozhi.modules.sys.dto.ServerActionResponseDTO;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.utils.WebSocketClientManager;
|
||||
|
||||
/**
|
||||
* 服务端管理控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("admin/server")
|
||||
@Tag(name = "服务端管理")
|
||||
@AllArgsConstructor
|
||||
public class ServerSideManageController {
|
||||
private final SysParamsService sysParamsService;
|
||||
private static final ObjectMapper objectMapper;
|
||||
static {
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取Ws服务端列表")
|
||||
@GetMapping("/server-list")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<List<String>> getWsServerList() {
|
||||
String cachedWs = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
return new Result<List<String>>().ok(Arrays.asList(cachedWs.split(";")));
|
||||
}
|
||||
|
||||
@Operation(summary = "通知python服务端更新配置")
|
||||
@PostMapping("/emit-action")
|
||||
@LogOperation("通知python服务端更新配置")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
|
||||
if (emitSeverActionDTO.getAction() == null) {
|
||||
throw new RenException("无效服务端操作");
|
||||
}
|
||||
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsText)) {
|
||||
throw new RenException("未配置服务端WebSocket地址");
|
||||
}
|
||||
String targetWs = emitSeverActionDTO.getTargetWs();
|
||||
String[] wsList = wsText.split(";");
|
||||
// 找到需要发起的
|
||||
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
|
||||
throw new RenException("目标WebSocket地址不存在");
|
||||
}
|
||||
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
|
||||
}
|
||||
|
||||
private Boolean emitServerActionByWs(String targetWsUri, ServerActionEnum actionEnum) {
|
||||
if (StringUtils.isBlank(targetWsUri) || actionEnum == null) {
|
||||
return false;
|
||||
}
|
||||
String serverSK = sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.add("device-id", UUID.randomUUID().toString());
|
||||
headers.add("client-id", UUID.randomUUID().toString());
|
||||
|
||||
try (WebSocketClientManager client = new WebSocketClientManager.Builder()
|
||||
.connectTimeout(3, TimeUnit.SECONDS)
|
||||
.maxSessionDuration(120, TimeUnit.SECONDS)
|
||||
.uri(targetWsUri)
|
||||
.headers(headers)
|
||||
.build()) {
|
||||
// 如果连接成功则发送一个json数据包并等待服务端响应
|
||||
client.sendJson(
|
||||
ServerActionPayloadDTO.build(
|
||||
actionEnum,
|
||||
Map.of("secret", serverSK)));
|
||||
// 等待服务端响应并持续监听信息
|
||||
client.listener((jsonText) -> {
|
||||
if (StringUtils.isBlank(jsonText)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ServerActionResponseDTO response = objectMapper.readValue(jsonText, ServerActionResponseDTO.class);
|
||||
Boolean isSuccess = ServerActionResponseDTO.isSuccess(response);
|
||||
return isSuccess;
|
||||
} catch (JsonProcessingException e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// 捕获全部错误,由全局异常处理器返回
|
||||
throw new RenException("WebSocket连接失败或连接超时");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package xiaozhi.modules.sys.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||
|
||||
/**
|
||||
* 字典数据管理
|
||||
*
|
||||
* @author czc
|
||||
* @since 2025-04-30
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/admin/dict/data")
|
||||
@Tag(name = "字典数据管理")
|
||||
public class SysDictDataController {
|
||||
private final SysDictDataService sysDictDataService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页查询字典数据")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameters({ @Parameter(name = "dictTypeId", description = "字典类型ID", required = true),
|
||||
@Parameter(name = "dictLabel", description = "数据标签"), @Parameter(name = "dictValue", description = "数据值"),
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
|
||||
public Result<PageData<SysDictDataVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
ValidatorUtils.validateEntity(params);
|
||||
// 强制校验dictTypeId是否存在
|
||||
if (!params.containsKey("dictTypeId") || StringUtils.isEmpty(String.valueOf(params.get("dictTypeId")))) {
|
||||
return new Result<PageData<SysDictDataVO>>().error("dictTypeId不能为空");
|
||||
}
|
||||
|
||||
PageData<SysDictDataVO> page = sysDictDataService.page(params);
|
||||
return new Result<PageData<SysDictDataVO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取字典数据详情")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<SysDictDataVO> get(@PathVariable("id") Long id) {
|
||||
SysDictDataVO vo = sysDictDataService.get(id);
|
||||
return new Result<SysDictDataVO>().ok(vo);
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@Operation(summary = "新增字典数据")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> save(@RequestBody SysDictDataDTO dto) {
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
sysDictDataService.save(dto);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "修改字典数据")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> update(@RequestBody SysDictDataDTO dto) {
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
sysDictDataService.update(dto);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除字典数据")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||
sysDictDataService.delete(ids);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@GetMapping("/type/{dictType}")
|
||||
@Operation(summary = "获取字典数据列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<SysDictDataItem>> getDictDataByType(@PathVariable("dictType") String dictType) {
|
||||
List<SysDictDataItem> list = sysDictDataService.getDictDataByType(dictType);
|
||||
return new Result<List<SysDictDataItem>>().ok(list);
|
||||
}
|
||||
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package xiaozhi.modules.sys.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||
|
||||
/**
|
||||
* 字典类型管理
|
||||
*
|
||||
* @author czc
|
||||
* @since 2025-04-30
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/admin/dict/type")
|
||||
@Tag(name = "字典类型管理")
|
||||
public class SysDictTypeController {
|
||||
private final SysDictTypeService sysDictTypeService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页查询字典类型")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameters({ @Parameter(name = "dictType", description = "字典类型编码"),
|
||||
@Parameter(name = "dictName", description = "字典类型名称"),
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) })
|
||||
public Result<PageData<SysDictTypeVO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
ValidatorUtils.validateEntity(params);
|
||||
PageData<SysDictTypeVO> page = sysDictTypeService.page(params);
|
||||
return new Result<PageData<SysDictTypeVO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取字典类型详情")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<SysDictTypeVO> get(@PathVariable("id") Long id) {
|
||||
SysDictTypeVO vo = sysDictTypeService.get(id);
|
||||
return new Result<SysDictTypeVO>().ok(vo);
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
@Operation(summary = "保存字典类型")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> save(@RequestBody SysDictTypeDTO dto) {
|
||||
// 参数校验
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
|
||||
sysDictTypeService.save(dto);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "修改字典类型")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> update(@RequestBody SysDictTypeDTO dto) {
|
||||
// 参数校验
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
|
||||
sysDictTypeService.update(dto);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除字典类型")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||
sysDictTypeService.delete(ids);
|
||||
return new Result<>();
|
||||
}
|
||||
}
|
||||
+95
-7
@@ -2,8 +2,10 @@ package xiaozhi.modules.sys.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -12,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -21,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
@@ -28,8 +32,10 @@ import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.utils.WebSocketValidator;
|
||||
|
||||
/**
|
||||
* 参数管理
|
||||
@@ -43,6 +49,8 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
||||
@AllArgsConstructor
|
||||
public class SysParamsController {
|
||||
private final SysParamsService sysParamsService;
|
||||
private final ConfigService configService;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@GetMapping("page")
|
||||
@Operation(summary = "分页")
|
||||
@@ -51,7 +59,7 @@ public class SysParamsController {
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", in = ParameterIn.QUERY, required = true, ref = "int"),
|
||||
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段", in = ParameterIn.QUERY, ref = "String"),
|
||||
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)", in = ParameterIn.QUERY, ref = "String"),
|
||||
@Parameter(name = "paramCode", description = "参数编码", in = ParameterIn.QUERY, ref = "String")
|
||||
@Parameter(name = "paramCode", description = "参数编码或参数备注", in = ParameterIn.QUERY, ref = "String")
|
||||
})
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<SysParamsDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
@@ -78,7 +86,7 @@ public class SysParamsController {
|
||||
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||
|
||||
sysParamsService.save(dto);
|
||||
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@@ -90,21 +98,101 @@ public class SysParamsController {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||
|
||||
sysParamsService.update(dto);
|
||||
// 验证WebSocket地址列表
|
||||
validateWebSocketUrls(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
// 验证OTA地址
|
||||
validateOtaUrl(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
sysParamsService.update(dto);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
/**
|
||||
* 验证WebSocket地址列表
|
||||
*
|
||||
* @param urls WebSocket地址列表,以分号分隔
|
||||
* @return 验证结果
|
||||
*/
|
||||
private void validateWebSocketUrls(String paramCode, String urls) {
|
||||
if (!paramCode.equals(Constant.SERVER_WEBSOCKET)) {
|
||||
return;
|
||||
}
|
||||
String[] wsUrls = urls.split("\\;");
|
||||
if (wsUrls.length == 0) {
|
||||
throw new RenException("WebSocket地址列表不能为空");
|
||||
}
|
||||
for (String url : wsUrls) {
|
||||
if (StringUtils.isNotBlank(url)) {
|
||||
// 检查是否包含localhost或127.0.0.1
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1");
|
||||
}
|
||||
|
||||
// 验证WebSocket地址格式
|
||||
if (!WebSocketValidator.validateUrlFormat(url)) {
|
||||
throw new RenException("WebSocket地址格式不正确: " + url);
|
||||
}
|
||||
|
||||
// 测试WebSocket连接
|
||||
if (!WebSocketValidator.testConnection(url)) {
|
||||
throw new RenException("WebSocket连接测试失败: " + url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||
public Result<Void> delete(@RequestBody String[] ids) {
|
||||
// 效验数据
|
||||
AssertUtils.isArrayEmpty(ids, "id");
|
||||
|
||||
sysParamsService.delete(ids);
|
||||
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证OTA地址
|
||||
*/
|
||||
private void validateOtaUrl(String paramCode, String url) {
|
||||
if (!paramCode.equals(Constant.SERVER_OTA)) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException("OTA地址不能为空");
|
||||
}
|
||||
|
||||
// 检查是否包含localhost或127.0.0.1
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("OTA地址不能使用localhost或127.0.0.1");
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
if (!url.toLowerCase().startsWith("http")) {
|
||||
throw new RenException("OTA地址必须以http或https开头");
|
||||
}
|
||||
if (!url.endsWith("/ota/")) {
|
||||
throw new RenException("OTA地址必须以/ota/结尾");
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送GET请求
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode());
|
||||
}
|
||||
// 检查响应内容是否包含OTA相关信息
|
||||
String body = response.getBody();
|
||||
if (body == null || !body.contains("OTA")) {
|
||||
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RenException("OTA接口验证失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||
import xiaozhi.modules.sys.entity.DictData;
|
||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
|
||||
/**
|
||||
* 字典数据
|
||||
@@ -15,10 +14,13 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||
@Mapper
|
||||
public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
||||
|
||||
/**
|
||||
* 字典数据列表
|
||||
*/
|
||||
List<DictData> getDictDataList();
|
||||
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||
|
||||
List<SysDictDataDTO> getDataByTypeCode(String dictType);
|
||||
/**
|
||||
* 根据字典类型ID获取字典类型编码
|
||||
*
|
||||
* @param dictTypeId 字典类型ID
|
||||
* @return 字典类型编码
|
||||
*/
|
||||
String getTypeByTypeId(Long dictTypeId);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package xiaozhi.modules.sys.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
|
||||
/**
|
||||
@@ -14,9 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
@Mapper
|
||||
public interface SysDictTypeDao extends BaseDao<SysDictTypeEntity> {
|
||||
|
||||
/**
|
||||
* 字典类型列表
|
||||
*/
|
||||
List<DictType> getDictTypeList();
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public interface SysParamsDao extends BaseDao<SysParamsEntity> {
|
||||
* @param ids ids
|
||||
* @return 返回参数编码列表
|
||||
*/
|
||||
List<String> getParamCodeList(Long[] ids);
|
||||
List<String> getParamCodeList(String[] ids);
|
||||
|
||||
/**
|
||||
* 根据参数编码,更新value
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
|
||||
/**
|
||||
* 发送python服务端操作DTO
|
||||
*
|
||||
* @Author CAIXYPROMISE
|
||||
* @since 2025/5/15 4:55
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class EmitSeverActionDTO
|
||||
{
|
||||
@Schema(description = "目标ws地址")
|
||||
@NotEmpty(message = "目标ws地址不能为空")
|
||||
private String targetWs;
|
||||
|
||||
@Schema(description = "指定操作")
|
||||
@NotNull(message = "操作不能为空")
|
||||
private ServerActionEnum action;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 找回密码DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "找回密码")
|
||||
public class RetrievePasswordDTO implements Serializable {
|
||||
|
||||
@Schema(description = "手机号码")
|
||||
@NotBlank(message = "{sysuser.password.require}")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "验证码")
|
||||
@NotBlank(message = "{sysuser.password.require}")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "新密码")
|
||||
@NotBlank(message = "{sysuser.password.require}")
|
||||
private String password;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 服务端动作DTO
|
||||
*
|
||||
* @Author CAIXYPROMISE
|
||||
* @since 2025/5/15 5:10
|
||||
*/
|
||||
@Data
|
||||
public class ServerActionPayloadDTO
|
||||
{
|
||||
/**
|
||||
* 类型(智控台发往服务端的都是server)
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* 动作
|
||||
*/
|
||||
private ServerActionEnum action;
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
private Map<String, Object> content;
|
||||
|
||||
public static ServerActionPayloadDTO build(ServerActionEnum action, Map<String, Object> content) {
|
||||
ServerActionPayloadDTO serverActionPayloadDTO = new ServerActionPayloadDTO();
|
||||
serverActionPayloadDTO.setAction(action);
|
||||
serverActionPayloadDTO.setContent(content);
|
||||
serverActionPayloadDTO.setType("server");
|
||||
return serverActionPayloadDTO;
|
||||
}
|
||||
// 私有化
|
||||
private ServerActionPayloadDTO() {}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.sys.enums.ServerActionResponseEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 服务端动作响应体
|
||||
*/
|
||||
@Data
|
||||
public class ServerActionResponseDTO
|
||||
{
|
||||
private ServerActionResponseEnum status;
|
||||
private String message;
|
||||
private String type;
|
||||
private Map<String, Object> content; // 后续这个字段可以移除,并把这个类作为基类,针对业务写自己的content类型
|
||||
public static final String DEFAULT_TYPE_FORM_SERVER = "server";
|
||||
|
||||
public static Boolean isSuccess(ServerActionResponseDTO actionResponseDTO) {
|
||||
System.out.println(actionResponseDTO);
|
||||
if (actionResponseDTO == null) {
|
||||
return false;
|
||||
}
|
||||
if (actionResponseDTO.getStatus() == null || !actionResponseDTO.getStatus().equals(ServerActionResponseEnum.SUCCESS)) {
|
||||
return false;
|
||||
}
|
||||
Object actionType = actionResponseDTO.getContent().get("action");
|
||||
if (actionType == null) {
|
||||
return false;
|
||||
}
|
||||
return actionResponseDTO.getType() != null && actionResponseDTO.getType().equals(DEFAULT_TYPE_FORM_SERVER);
|
||||
}
|
||||
}
|
||||
@@ -51,5 +51,6 @@ public class SysDictTypeDTO implements Serializable {
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
@JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN)
|
||||
private Date updateDate;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Null;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
@@ -36,6 +37,11 @@ public class SysParamsDTO implements Serializable {
|
||||
@NotBlank(message = "{sysparams.paramvalue.require}", groups = DefaultGroup.class)
|
||||
private String paramValue;
|
||||
|
||||
@Schema(description = "值类型")
|
||||
@NotBlank(message = "{sysparams.valuetype.require}", groups = DefaultGroup.class)
|
||||
@Pattern(regexp = "^(string|number|boolean|array|json)$", message = "{sysparams.valuetype.pattern}", groups = DefaultGroup.class)
|
||||
private String valueType;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package xiaozhi.modules.sys.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 字典数据
|
||||
*/
|
||||
@Data
|
||||
public class DictData {
|
||||
@JsonIgnore
|
||||
private Long dictTypeId;
|
||||
private String dictLabel;
|
||||
private String dictValue;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package xiaozhi.modules.sys.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@Data
|
||||
public class DictType {
|
||||
@JsonIgnore
|
||||
private Long id;
|
||||
private String dictType;
|
||||
private List<DictData> dataList = new ArrayList<>();
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import xiaozhi.common.entity.BaseEntity;
|
||||
@TableName("sys_dict_type")
|
||||
public class SysDictTypeEntity extends BaseEntity {
|
||||
/**
|
||||
* 字典类型
|
||||
* 字典类型编码
|
||||
*/
|
||||
private String dictType;
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,10 @@ public class SysParamsEntity extends BaseEntity {
|
||||
* 参数值
|
||||
*/
|
||||
private String paramValue;
|
||||
/**
|
||||
* 值类型:string-字符串,number-数字,boolean-布尔,array-数组
|
||||
*/
|
||||
private String valueType;
|
||||
/**
|
||||
* 类型 0:系统参数 1:非系统参数
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package xiaozhi.modules.sys.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
/**
|
||||
* 服务端动作枚举
|
||||
*
|
||||
* @Author CAIXYPROMISE
|
||||
* @since 2025/5/15 4:57
|
||||
*/
|
||||
public enum ServerActionEnum
|
||||
{
|
||||
RESTART("restart"),
|
||||
UPDATE_CONFIG("update_config");
|
||||
|
||||
private final String value;
|
||||
|
||||
ServerActionEnum(String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static ServerActionEnum fromValue(String value)
|
||||
{
|
||||
for (ServerActionEnum action : ServerActionEnum.values())
|
||||
{
|
||||
if (action.value.equalsIgnoreCase(value))
|
||||
{
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package xiaozhi.modules.sys.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 服务端调用响应枚举
|
||||
*
|
||||
* @Author CAIXYPROMISE
|
||||
* @since 2025/5/15 5:50
|
||||
*/
|
||||
public enum ServerActionResponseEnum
|
||||
{
|
||||
SUCCESS("success"), FAIL("fail");
|
||||
private final String value;
|
||||
|
||||
ServerActionResponseEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static ServerActionResponseEnum fromValue(String value) {
|
||||
ServerActionResponseEnum byValue = getByValue(value);
|
||||
if (byValue == null) {
|
||||
throw new IllegalArgumentException("Unknown enum value: " + value);
|
||||
}
|
||||
return byValue;
|
||||
}
|
||||
|
||||
public static ServerActionResponseEnum getByValue(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
for (ServerActionResponseEnum action : ServerActionResponseEnum.values()) {
|
||||
if (action.value.equals(value)) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,70 @@
|
||||
package xiaozhi.modules.sys.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||
|
||||
/**
|
||||
* 数据字典
|
||||
*/
|
||||
public interface SysDictDataService extends BaseService<SysDictDataEntity> {
|
||||
|
||||
PageData<SysDictDataDTO> page(Map<String, Object> params);
|
||||
/**
|
||||
* 分页查询数据字典信息
|
||||
*
|
||||
* @param params 查询参数,包含分页信息和查询条件
|
||||
* @return 返回数据字典的分页查询结果
|
||||
*/
|
||||
PageData<SysDictDataVO> page(Map<String, Object> params);
|
||||
|
||||
SysDictDataDTO get(Long id);
|
||||
/**
|
||||
* 根据ID获取数据字典实体
|
||||
*
|
||||
* @param id 数据字典实体的唯一标识
|
||||
* @return 返回数据字典实体的详细信息
|
||||
*/
|
||||
SysDictDataVO get(Long id);
|
||||
|
||||
/**
|
||||
* 保存新的数据字典项
|
||||
*
|
||||
* @param dto 数据字典项的保存数据传输对象
|
||||
*/
|
||||
void save(SysDictDataDTO dto);
|
||||
|
||||
/**
|
||||
* 更新数据字典项
|
||||
*
|
||||
* @param dto 数据字典项的更新数据传输对象
|
||||
*/
|
||||
void update(SysDictDataDTO dto);
|
||||
|
||||
/**
|
||||
* 删除数据字典项
|
||||
*
|
||||
* @param ids 要删除的数据字典项的ID数组
|
||||
*/
|
||||
void delete(Long[] ids);
|
||||
|
||||
/**
|
||||
* 根据字典类型ID删除对应的字典数据
|
||||
*
|
||||
* @param dictTypeId 字典类型ID
|
||||
*/
|
||||
void deleteByTypeId(Long dictTypeId);
|
||||
|
||||
/**
|
||||
* 根据字典类型获取字典数据列表
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return 返回字典数据列表
|
||||
*/
|
||||
List<SysDictDataItem> getDictDataByType(String dictType);
|
||||
|
||||
}
|
||||
@@ -6,25 +6,55 @@ import java.util.Map;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||
|
||||
/**
|
||||
* 数据字典
|
||||
*/
|
||||
public interface SysDictTypeService extends BaseService<SysDictTypeEntity> {
|
||||
|
||||
PageData<SysDictTypeDTO> page(Map<String, Object> params);
|
||||
/**
|
||||
* 分页查询字典类型信息
|
||||
*
|
||||
* @param params 查询参数,包含分页信息和查询条件
|
||||
* @return 返回分页的字典类型数据
|
||||
*/
|
||||
PageData<SysDictTypeVO> page(Map<String, Object> params);
|
||||
|
||||
SysDictTypeDTO get(Long id);
|
||||
/**
|
||||
* 根据ID获取字典类型信息
|
||||
*
|
||||
* @param id 字典类型ID
|
||||
* @return 返回字典类型对象
|
||||
*/
|
||||
SysDictTypeVO get(Long id);
|
||||
|
||||
/**
|
||||
* 保存字典类型信息
|
||||
*
|
||||
* @param dto 字典类型数据传输对象
|
||||
*/
|
||||
void save(SysDictTypeDTO dto);
|
||||
|
||||
/**
|
||||
* 更新字典类型信息
|
||||
*
|
||||
* @param dto 字典类型数据传输对象
|
||||
*/
|
||||
void update(SysDictTypeDTO dto);
|
||||
|
||||
/**
|
||||
* 删除字典类型信息
|
||||
*
|
||||
* @param ids 要删除的字典类型ID数组
|
||||
*/
|
||||
void delete(Long[] ids);
|
||||
|
||||
List<DictType> getAllList();
|
||||
|
||||
List<DictType> getDictTypeList();
|
||||
/**
|
||||
* 列出所有字典类型信息
|
||||
*
|
||||
* @return 返回字典类型列表
|
||||
*/
|
||||
List<SysDictTypeVO> list(Map<String, Object> params);
|
||||
}
|
||||
@@ -23,14 +23,15 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
|
||||
|
||||
void update(SysParamsDTO dto);
|
||||
|
||||
void delete(Long[] ids);
|
||||
void delete(String[] ids);
|
||||
|
||||
/**
|
||||
* 根据参数编码,获取参数的value值
|
||||
*
|
||||
* @param paramCode 参数编码
|
||||
* @param fromCache 是否从缓存中获取
|
||||
*/
|
||||
String getValue(String paramCode);
|
||||
String getValue(String paramCode, Boolean fromCache);
|
||||
|
||||
/**
|
||||
* 根据参数编码,获取value的Object对象
|
||||
@@ -47,4 +48,9 @@ public interface SysParamsService extends BaseService<SysParamsEntity> {
|
||||
* @param paramValue 参数值
|
||||
*/
|
||||
int updateValueByCode(String paramCode, String paramValue);
|
||||
|
||||
/**
|
||||
* 初始化服务器密钥
|
||||
*/
|
||||
void initServerSecret();
|
||||
}
|
||||
|
||||
@@ -65,4 +65,11 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
||||
* @param userIds 用户ID数组
|
||||
*/
|
||||
void changeStatus(Integer status, String[] userIds);
|
||||
|
||||
/**
|
||||
* 获取是否允许用户注册
|
||||
*
|
||||
* @return 是否允许用户注册
|
||||
*/
|
||||
boolean getAllowUserRegister();
|
||||
}
|
||||
|
||||
+114
-10
@@ -1,37 +1,55 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataVO;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysDictDataEntity>
|
||||
implements SysDictDataService {
|
||||
private final SysUserDao sysUserDao;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public PageData<SysDictDataDTO> page(Map<String, Object> params) {
|
||||
IPage<SysDictDataEntity> page = baseDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
getWrapper(params));
|
||||
public PageData<SysDictDataVO> page(Map<String, Object> params) {
|
||||
IPage<SysDictDataEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
|
||||
|
||||
return getPageData(page, SysDictDataDTO.class);
|
||||
PageData<SysDictDataVO> pageData = getPageData(page, SysDictDataVO.class);
|
||||
|
||||
setUserName(pageData.getList());
|
||||
|
||||
return pageData;
|
||||
}
|
||||
|
||||
private QueryWrapper<SysDictDataEntity> getWrapper(Map<String, Object> params) {
|
||||
@@ -48,32 +66,118 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysDictDataDTO get(Long id) {
|
||||
public SysDictDataVO get(Long id) {
|
||||
SysDictDataEntity entity = baseDao.selectById(id);
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictDataDTO.class);
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictDataVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(SysDictDataDTO dto) {
|
||||
// 相同字典类型的标签不能相同
|
||||
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), null);
|
||||
|
||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||
|
||||
insert(entity);
|
||||
// 删除Redis缓存
|
||||
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(SysDictDataDTO dto) {
|
||||
// 相同字典类型的标签不能相同
|
||||
checkDictValueUnique(dto.getDictTypeId(), dto.getDictValue(), String.valueOf(dto.getId()));
|
||||
|
||||
SysDictDataEntity entity = ConvertUtils.sourceToTarget(dto, SysDictDataEntity.class);
|
||||
|
||||
updateById(entity);
|
||||
// 删除Redis缓存
|
||||
String dictType = baseDao.getTypeByTypeId(dto.getDictTypeId());
|
||||
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long[] ids) {
|
||||
// 删除
|
||||
deleteBatchIds(Arrays.asList(ids));
|
||||
for (Long id : ids) {
|
||||
SysDictDataEntity entity = baseDao.selectById(id);
|
||||
// 删除Redis缓存
|
||||
String dictType = baseDao.getTypeByTypeId(entity.getDictTypeId());
|
||||
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||
// 删除
|
||||
deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByTypeId(Long dictTypeId) {
|
||||
LambdaQueryWrapper<SysDictDataEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId);
|
||||
baseDao.delete(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户名
|
||||
*
|
||||
* @param sysDictDataList 字典类型集合
|
||||
*/
|
||||
private void setUserName(List<SysDictDataVO> sysDictDataList) {
|
||||
// 收集所有用户 ID
|
||||
Set<Long> userIds = sysDictDataList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
|
||||
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
|
||||
// 设置更新者和创建者名称
|
||||
if (!userIds.isEmpty()) {
|
||||
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||
// 把List转成Map,Map<Long, String>
|
||||
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||
|
||||
sysDictDataList.forEach(vo -> {
|
||||
vo.setCreatorName(userNameMap.get(vo.getCreator()));
|
||||
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDictValueUnique(Long dictTypeId, String dictValue, String excludeId) {
|
||||
LambdaQueryWrapper<SysDictDataEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId).eq(SysDictDataEntity::getDictLabel, dictValue);
|
||||
if (StringUtils.isNotBlank(excludeId)) {
|
||||
queryWrapper.ne(SysDictDataEntity::getId, excludeId);
|
||||
}
|
||||
boolean exists = baseDao.exists(queryWrapper);
|
||||
if (exists) {
|
||||
throw new RenException("字典标签重复");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysDictDataItem> getDictDataByType(String dictType) {
|
||||
if (StringUtils.isBlank(dictType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先从Redis获取缓存
|
||||
String key = RedisKeys.getDictDataByTypeKey(dictType);
|
||||
List<SysDictDataItem> cachedData = (List<SysDictDataItem>) redisUtils.get(key);
|
||||
if (cachedData != null) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
// 如果缓存中没有,则从数据库获取
|
||||
List<SysDictDataItem> data = baseDao.getDictDataByType(dictType);
|
||||
|
||||
// 存入Redis缓存
|
||||
if (data != null) {
|
||||
redisUtils.set(key, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
+76
-27
@@ -3,42 +3,52 @@ package xiaozhi.modules.sys.service.impl;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||
import xiaozhi.modules.sys.dao.SysDictTypeDao;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.dto.SysDictTypeDTO;
|
||||
import xiaozhi.modules.sys.entity.DictData;
|
||||
import xiaozhi.modules.sys.entity.DictType;
|
||||
import xiaozhi.modules.sys.entity.SysDictTypeEntity;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import xiaozhi.modules.sys.service.SysDictDataService;
|
||||
import xiaozhi.modules.sys.service.SysDictTypeService;
|
||||
import xiaozhi.modules.sys.vo.SysDictTypeVO;
|
||||
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysDictTypeEntity>
|
||||
implements SysDictTypeService {
|
||||
private final SysDictDataDao sysDictDataDao;
|
||||
private final SysUserDao sysUserDao;
|
||||
private final SysDictDataService sysDictDataService;
|
||||
|
||||
@Override
|
||||
public PageData<SysDictTypeDTO> page(Map<String, Object> params) {
|
||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
getWrapper(params));
|
||||
public PageData<SysDictTypeVO> page(Map<String, Object> params) {
|
||||
IPage<SysDictTypeEntity> page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params));
|
||||
|
||||
return getPageData(page, SysDictTypeDTO.class);
|
||||
PageData<SysDictTypeVO> pageData = getPageData(page, SysDictTypeVO.class);
|
||||
|
||||
setUserName(pageData.getList());
|
||||
|
||||
return pageData;
|
||||
}
|
||||
|
||||
private QueryWrapper<SysDictTypeEntity> getWrapper(Map<String, Object> params) {
|
||||
@@ -53,15 +63,21 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysDictTypeDTO get(Long id) {
|
||||
public SysDictTypeVO get(Long id) {
|
||||
SysDictTypeEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
throw new RenException("字典类型不存在");
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeDTO.class);
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(SysDictTypeDTO dto) {
|
||||
// 字典类型编码不能重复
|
||||
checkDictTypeUnique(dto.getDictType(), null);
|
||||
|
||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||
|
||||
insert(entity);
|
||||
@@ -70,6 +86,9 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void update(SysDictTypeDTO dto) {
|
||||
// 字典类型编码不能重复
|
||||
checkDictTypeUnique(dto.getDictType(), String.valueOf(dto.getId()));
|
||||
|
||||
SysDictTypeEntity entity = ConvertUtils.sourceToTarget(dto, SysDictTypeEntity.class);
|
||||
|
||||
updateById(entity);
|
||||
@@ -78,27 +97,57 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long[] ids) {
|
||||
// 删除
|
||||
// 先删除对应的字典数据
|
||||
for (Long id : ids) {
|
||||
sysDictDataService.deleteByTypeId(id);
|
||||
}
|
||||
// 再删除字典类型
|
||||
deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DictType> getAllList() {
|
||||
List<DictType> typeList = baseDao.getDictTypeList();
|
||||
List<DictData> dataList = sysDictDataDao.getDictDataList();
|
||||
for (DictType type : typeList) {
|
||||
for (DictData data : dataList) {
|
||||
if (type.getId().equals(data.getDictTypeId())) {
|
||||
type.getDataList().add(data);
|
||||
}
|
||||
}
|
||||
public List<SysDictTypeVO> list(Map<String, Object> params) {
|
||||
List<SysDictTypeEntity> entityList = baseDao.selectList(getWrapper(params));
|
||||
List<SysDictTypeVO> sysDictTypeVOList = ConvertUtils.sourceToTarget(entityList, SysDictTypeVO.class);
|
||||
// 设置用户名
|
||||
setUserName(sysDictTypeVOList);
|
||||
|
||||
return sysDictTypeVOList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户名
|
||||
*
|
||||
* @param sysDictTypeList 字典类型集合
|
||||
*/
|
||||
private void setUserName(List<SysDictTypeVO> sysDictTypeList) {
|
||||
// 收集所有用户 ID
|
||||
Set<Long> userIds = sysDictTypeList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater()))
|
||||
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
|
||||
// 设置更新者和创建者名称
|
||||
if (!userIds.isEmpty()) {
|
||||
List<SysUserEntity> sysUserEntities = sysUserDao.selectBatchIds(userIds);
|
||||
// 把List转成Map,Map<Long, String>
|
||||
Map<Long, String> userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId,
|
||||
SysUserEntity::getUsername, (existing, replacement) -> existing));
|
||||
|
||||
sysDictTypeList.forEach(vo -> {
|
||||
vo.setCreatorName(userNameMap.get(vo.getCreator()));
|
||||
vo.setUpdaterName(userNameMap.get(vo.getUpdater()));
|
||||
});
|
||||
}
|
||||
return typeList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DictType> getDictTypeList() {
|
||||
return baseDao.getDictTypeList();
|
||||
private void checkDictTypeUnique(String dictType, String excludeId) {
|
||||
LambdaQueryWrapper<SysDictTypeEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SysDictTypeEntity::getDictType, dictType);
|
||||
if (StringUtils.isNotBlank(excludeId)) {
|
||||
queryWrapper.ne(SysDictTypeEntity::getId, excludeId);
|
||||
}
|
||||
boolean exists = baseDao.exists(queryWrapper);
|
||||
if (exists) {
|
||||
throw new RenException("字典类型编码重复");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user