diff --git a/Dockerfile-web b/Dockerfile-web index 9e5da678..35c25ed2 100644 --- a/Dockerfile-web +++ b/Dockerfile-web @@ -7,20 +7,23 @@ COPY main/manager-web . RUN npm run build # 第二阶段:构建Java后端 -FROM maven:3-eclipse-temurin-21-alpine as api-builder +FROM maven:3.9.4-eclipse-temurin-21 as api-builder WORKDIR /app COPY main/manager-api/pom.xml . COPY main/manager-api/src ./src RUN mvn clean package -Dmaven.test.skip=true # 第三阶段:构建最终镜像 -FROM eclipse-temurin:21-jdk-jammy +FROM bellsoft/liberica-runtime-container:jre-21-glibc -# 安装Nginx并清理缓存 -RUN apt-get update && \ - apt-get install -y nginx && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* +# 安装Nginx和字体库 +RUN apk update && \ + apk add --no-cache nginx bash && \ + apk add --no-cache fontconfig ttf-dejavu msttcorefonts-installer && \ + rm -rf /var/cache/apk/* + +# 更新字体缓存 +RUN printf 'YES\n' | update-ms-fonts && fc-cache -f -v # 配置Nginx COPY docs/docker/nginx.conf /etc/nginx/nginx.conf diff --git a/docs/FAQ.md b/docs/FAQ.md index cfcbe373..f04f3d78 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -104,7 +104,11 @@ VAD: 参考教程[ESP32设备与HomeAssistant集成指南](./homeassistant-integration.md) -### 8、更多问题,可联系我们反馈 💬 +### 8、如何开启手机注册智控台 📱 + +参考教程[阿里云短信集成指南](./ali-sms-integration.md) + +### 9、更多问题,可联系我们反馈 💬 可以在[issues](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues)提交您的问题。 diff --git a/docs/ali-sms-integration.md b/docs/ali-sms-integration.md new file mode 100644 index 00000000..1be27787 --- /dev/null +++ b/docs/ali-sms-integration.md @@ -0,0 +1,44 @@ +# 阿里云短信集成指南 + +登录阿里云控制台,进入“短信服务”页面:https://dysms.console.aliyun.com/overview + +## 第一步 添加签名 +![步骤](images/alisms/sms-01.png) +![步骤](images/alisms/sms-02.png) + +以上步骤,会得到签名,请把它写入到智控台参数,`aliyun.sms.sign_name` + +## 第二步 添加模版 +![步骤](images/alisms/sms-11.png) + +以上步骤,会得到模版code,请把它写入到智控台参数,`aliyun.sms.sms_code_template_code` + +注意,签名要等7个工作日,等运营商报备成功后才能发送成功。 + +注意,签名要等7个工作日,等运营商报备成功后才能发送成功。 + +注意,签名要等7个工作日,等运营商报备成功后才能发送成功。 + +可以等报备成功后,再继续往下操作。 + +## 第三步 创建短信账户和开通权限 + +登录阿里云控制台,进入“访问控制”页面:https://ram.console.aliyun.com/overview?activeTab=overview + +![步骤](images/alisms/sms-21.png) +![步骤](images/alisms/sms-22.png) +![步骤](images/alisms/sms-23.png) +![步骤](images/alisms/sms-24.png) +![步骤](images/alisms/sms-25.png) + +以上步骤,会得到access_key_id和access_key_secret,请把它写入到智控台参数,`aliyun.sms.access_key_id`、`aliyun.sms.access_key_secret` +## 第四步 启动手机注册功能 + +1、正常来说,以上信息都填完后,会有这个效果,如果没有,可能缺少了某个步骤 + +![步骤](images/alisms/sms-31.png) + +2、开启允许非管理员用户可注册,将参数`server.allow_user_register`设置成`true` + +3、开启手机注册功能,将参数`server.enable_mobile_register`设置成`true` +![步骤](images/alisms/sms-32.png) \ No newline at end of file diff --git a/docs/images/alisms/sms-01.png b/docs/images/alisms/sms-01.png new file mode 100644 index 00000000..89c56f50 Binary files /dev/null and b/docs/images/alisms/sms-01.png differ diff --git a/docs/images/alisms/sms-02.png b/docs/images/alisms/sms-02.png new file mode 100644 index 00000000..22537f36 Binary files /dev/null and b/docs/images/alisms/sms-02.png differ diff --git a/docs/images/alisms/sms-11.png b/docs/images/alisms/sms-11.png new file mode 100644 index 00000000..05fcaf57 Binary files /dev/null and b/docs/images/alisms/sms-11.png differ diff --git a/docs/images/alisms/sms-21.png b/docs/images/alisms/sms-21.png new file mode 100644 index 00000000..5bfc2801 Binary files /dev/null and b/docs/images/alisms/sms-21.png differ diff --git a/docs/images/alisms/sms-22.png b/docs/images/alisms/sms-22.png new file mode 100644 index 00000000..3d83984b Binary files /dev/null and b/docs/images/alisms/sms-22.png differ diff --git a/docs/images/alisms/sms-23.png b/docs/images/alisms/sms-23.png new file mode 100644 index 00000000..584d8d67 Binary files /dev/null and b/docs/images/alisms/sms-23.png differ diff --git a/docs/images/alisms/sms-24.png b/docs/images/alisms/sms-24.png new file mode 100644 index 00000000..1b22c359 Binary files /dev/null and b/docs/images/alisms/sms-24.png differ diff --git a/docs/images/alisms/sms-25.png b/docs/images/alisms/sms-25.png new file mode 100644 index 00000000..3bb68cfa Binary files /dev/null and b/docs/images/alisms/sms-25.png differ diff --git a/docs/images/alisms/sms-31.png b/docs/images/alisms/sms-31.png new file mode 100644 index 00000000..7f0ee6c4 Binary files /dev/null and b/docs/images/alisms/sms-31.png differ diff --git a/docs/images/alisms/sms-32.png b/docs/images/alisms/sms-32.png new file mode 100644 index 00000000..db06cc4d Binary files /dev/null and b/docs/images/alisms/sms-32.png differ diff --git a/main/manager-api/pom.xml b/main/manager-api/pom.xml index d08e32f2..883295bf 100644 --- a/main/manager-api/pom.xml +++ b/main/manager-api/pom.xml @@ -28,6 +28,8 @@ 1.6.2 33.0.0-jre 4.20.0 + 4.1.0 + 3.4.0 @@ -37,6 +39,7 @@ jakarta ${shiro.version} + org.apache.shiro shiro-spring @@ -201,6 +204,18 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + + + com.aliyun + dysmsapi20170525 + ${aliyun-sms-version} + + + + com.squareup.okio + okio + ${okio-version} + diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index b4b361ad..b3c036ee 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -118,29 +118,17 @@ public interface Constant { 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; @@ -153,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; + } + } + /** * 数据状态 */ @@ -199,10 +227,30 @@ public interface Constant { /** * 版本号 */ - public static final String VERSION = "0.4.3"; + 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; + } + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java index c59a6a14..6a9af011 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java @@ -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; diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/RenExceptionHandler.java b/main/manager-api/src/main/java/xiaozhi/common/exception/RenExceptionHandler.java index 0d0aa118..e8eaf462 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/RenExceptionHandler.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/RenExceptionHandler.java @@ -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().error(404, "资源不存在"); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + List 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().error(ErrorCode.PARAM_VALUE_NULL, errorMsg); + } + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java index 62487456..6daa3b2d 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java +++ b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java @@ -117,4 +117,26 @@ public class RedisKeys { 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"; + } + } diff --git a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisUtils.java b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisUtils.java index 9115e806..c81677e9 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisUtils.java +++ b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisUtils.java @@ -6,12 +6,14 @@ 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工具类 @@ -23,6 +25,9 @@ public class RedisUtils { @Resource private RedisTemplate redisTemplate; + @Autowired + private ResourcesUtils resourceUtils; + /** * 默认过期时长为24小时,单位:秒 */ @@ -40,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) { @@ -134,7 +157,7 @@ public class RedisUtils { */ public void emptyAll() { // Lua 脚本 FLUSHALL是redis清空所有库的命令 - String luaScript ="redis.call('FLUSHALL')"; + String luaScript =resourceUtils.loadString("lua/emptyAll.lua"); // 创建 DefaultRedisScript 对象 DefaultRedisScript redisScript = new DefaultRedisScript<>(); @@ -147,4 +170,26 @@ public class RedisUtils { } + /** + * 获取在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 redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(luaScript); + redisScript.setResultType(String.class); + + // 执行 Lua 脚本 + List keys = Collections.singletonList(key); + return redisTemplate.execute(redisScript, keys, defaultValue,expiresInSecond); + } + + + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java index 9d2ba5d5..9d06c09a 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java @@ -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, 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 getPage(Map params, String defaultOrderField, boolean isAsc) { // 分页参数 @@ -65,28 +73,34 @@ public abstract class BaseServiceImpl, 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 orderFields = new ArrayList<>(); + + // 处理排序字段 + if (orderField instanceof String) { + orderFields.add((String) orderField); + } else if (orderField instanceof List) { + orderFields.addAll((List) 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; diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/DateUtils.java b/main/manager-api/src/main/java/xiaozhi/common/utils/DateUtils.java index bfa7afed..800733c1 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/utils/DateUtils.java +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/DateUtils.java @@ -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 diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/ResourcesUtils.java b/main/manager-api/src/main/java/xiaozhi/common/utils/ResourcesUtils.java new file mode 100644 index 00000000..49d9a727 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/ResourcesUtils.java @@ -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(); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/common/validator/ValidatorUtils.java b/main/manager-api/src/main/java/xiaozhi/common/validator/ValidatorUtils.java index 9e31c964..364da0d9 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/validator/ValidatorUtils.java +++ b/main/manager-api/src/main/java/xiaozhi/common/validator/ValidatorUtils.java @@ -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(); + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java b/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java new file mode 100644 index 00000000..fc534a0a --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/controller/ModelProviderController.java @@ -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> getListPage(ModelProviderDTO modelProviderDTO, + @RequestParam(required = true, defaultValue = "0") String page, + @RequestParam(required = true, defaultValue = "10") String limit) { + return new Result>() + .ok(modelProviderService.getListPage(modelProviderDTO, page, limit)); + } + + @PostMapping + @Operation(summary = "新增模型供应器") + @RequiresPermissions("sys:role:superAdmin") + public Result add(@RequestBody @Validated ModelProviderDTO modelProviderDTO) { + ModelProviderDTO resp = modelProviderService.add(modelProviderDTO); + return new Result().ok(resp); + } + + @PutMapping + @Operation(summary = "修改模型供应器") + @RequiresPermissions("sys:role:superAdmin") + public Result edit(@RequestBody @Validated(UpdateGroup.class) ModelProviderDTO modelProviderDTO) { + ModelProviderDTO resp = modelProviderService.edit(modelProviderDTO); + return new Result().ok(resp); + } + + @PostMapping("/delete") + @Operation(summary = "删除模型供应器") + @RequiresPermissions("sys:role:superAdmin") + @Parameter(name = "ids", description = "ID数组", required = true) + public Result delete(@RequestBody List ids) { + modelProviderService.delete(ids); + return new Result<>(); + } + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/dto/ModelProviderDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/model/dto/ModelProviderDTO.java index 605ed13a..317820d5 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/dto/ModelProviderDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/dto/ModelProviderDTO.java @@ -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 = "更新者") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/entity/ModelProviderEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/model/entity/ModelProviderEntity.java index 62e84f67..00904759 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/entity/ModelProviderEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/entity/ModelProviderEntity.java @@ -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 = "排序") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java index 2ace101e..d41766f6 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/ModelProviderService.java @@ -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 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 id); + + PageData getListPage(ModelProviderDTO modelProviderDTO, String page, String limit); List getList(String modelType, String provideCode); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java index fb06e4d5..ffeec288 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelProviderServiceImpl.java @@ -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 getListPage(ModelProviderDTO modelProviderDTO, String page, String limit) { + + Map params = new HashMap(); + 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 pageParam = getPage(params, null, true); + + QueryWrapper wrapper = new QueryWrapper(); + + 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 ids) { + if (modelProviderDao.deleteBatchIds(ids) == 0) { + throw new RenException("删除数据失败"); + } } @Override diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index 7875fa86..c015240f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -80,9 +80,11 @@ public class ShiroConfig { 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("/user/retrieve-password", "anon"); // 将config路径使用server服务过滤器 filterMap.put("/config/**", "server"); filterMap.put("/agent/chat-history/report", "server"); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java index 7cbaa8af..0215cf7e 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java @@ -1,7 +1,9 @@ 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; @@ -24,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; /** * 登录控制层 @@ -43,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 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 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()); @@ -81,11 +107,29 @@ public class LoginController { if (!sysUserService.getAllowUserRegister()) { throw new RenException("当前不允许普通用户注册"); } - // 验证是否正确输入验证码 - boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha()); - if (!validate) { - 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) { @@ -96,7 +140,6 @@ public class LoginController { userDTO.setPassword(login.getPassword()); sysUserService.save(userDTO); return new Result<>(); - } @GetMapping("/info") @@ -118,12 +161,54 @@ public class LoginController { 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> pubConfig() { Map 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 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>().ok(config); } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/dto/LoginDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/security/dto/LoginDTO.java index cab143be..12b99d32 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/dto/LoginDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/dto/LoginDTO.java @@ -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; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/dto/SmsVerificationDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/security/dto/SmsVerificationDTO.java new file mode 100644 index 00000000..425ef059 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/dto/SmsVerificationDTO.java @@ -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; +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/service/CaptchaService.java b/main/manager-api/src/main/java/xiaozhi/modules/security/service/CaptchaService.java index 9e5c153f..0db9d296 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/service/CaptchaService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/service/CaptchaService.java @@ -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); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/service/impl/CaptchaServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/security/service/impl/CaptchaServiceImpl.java index 012b4d3e..e823d70e 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/service/impl/CaptchaServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/service/impl/CaptchaServiceImpl.java @@ -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); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sms/service/SmsService.java b/main/manager-api/src/main/java/xiaozhi/modules/sms/service/SmsService.java new file mode 100644 index 00000000..c33ad945 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sms/service/SmsService.java @@ -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) ; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sms/service/imp/ALiYunSmsService.java b/main/manager-api/src/main/java/xiaozhi/modules/sms/service/imp/ALiYunSmsService.java new file mode 100644 index 00000000..e0ebb523 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sms/service/imp/ALiYunSmsService.java @@ -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("短信连接建立失败"); + } + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java new file mode 100644 index 00000000..0b6b6a5b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/ServerSideManageController.java @@ -0,0 +1,121 @@ +package xiaozhi.modules.sys.controller; + +import java.util.*; +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(); + // 忽略json字符串中存在,但pojo中不存在对应字段的情况 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @Operation(summary = "获取Ws服务端列表") + @GetMapping("/server-list") + @RequiresPermissions("sys:role:superAdmin") + public Result> getWsServerList() { + String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true); + if (StringUtils.isBlank(wsText)) { + return new Result>().ok(Collections.emptyList()); + } + return new Result>().ok(Arrays.asList(wsText.split(";"))); + } + + @Operation(summary = "通知python服务端更新配置") + @PostMapping("/emit-action") + @LogOperation("通知python服务端更新配置") + @RequiresPermissions("sys:role:superAdmin") + public Result 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().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; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java index 9b80c76c..4860cf1c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysParamsController.java @@ -59,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> page(@Parameter(hidden = true) @RequestParam Map params) { @@ -111,7 +111,7 @@ public class SysParamsController { /** * 验证WebSocket地址列表 - * + * * @param urls WebSocket地址列表,以分号分隔 * @return 验证结果 */ @@ -143,6 +143,19 @@ public class SysParamsController { } } + @PostMapping("/delete") + @Operation(summary = "删除") + @LogOperation("删除") + @RequiresPermissions("sys:role:superAdmin") + public Result delete(@RequestBody String[] ids) { + // 效验数据 + AssertUtils.isArrayEmpty(ids, "id"); + + sysParamsService.delete(ids); + configService.getConfig(false); + return new Result(); + } + /** * 验证OTA地址 */ @@ -182,17 +195,4 @@ public class SysParamsController { throw new RenException("OTA接口验证失败:" + e.getMessage()); } } - - @PostMapping("/delete") - @Operation(summary = "删除") - @LogOperation("删除") - @RequiresPermissions("sys:role:superAdmin") - public Result delete(@RequestBody String[] ids) { - // 效验数据 - AssertUtils.isArrayEmpty(ids, "id"); - - sysParamsService.delete(ids); - configService.getConfig(false); - return new Result(); - } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/EmitSeverActionDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/EmitSeverActionDTO.java new file mode 100644 index 00000000..6de89aab --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/EmitSeverActionDTO.java @@ -0,0 +1,26 @@ +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 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EmitSeverActionDTO +{ + @Schema(description = "目标ws地址") + @NotEmpty(message = "目标ws地址不能为空") + private String targetWs; + + @Schema(description = "指定操作") + @NotNull(message = "操作不能为空") + private ServerActionEnum action; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/RetrievePasswordDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/RetrievePasswordDTO.java new file mode 100644 index 00000000..efac8099 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/RetrievePasswordDTO.java @@ -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; + + + +} \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionPayloadDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionPayloadDTO.java new file mode 100644 index 00000000..fb1427f9 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionPayloadDTO.java @@ -0,0 +1,36 @@ +package xiaozhi.modules.sys.dto; + +import lombok.Data; +import xiaozhi.modules.sys.enums.ServerActionEnum; + +import java.util.Map; + +/** + * 服务端动作DTO + */ +@Data +public class ServerActionPayloadDTO +{ + /** + * 类型(智控台发往服务端的都是server) + */ + private String type; + /** + * 动作 + */ + private ServerActionEnum action; + /** + * 内容 + */ + private Map content; + + public static ServerActionPayloadDTO build(ServerActionEnum action, Map content) { + ServerActionPayloadDTO serverActionPayloadDTO = new ServerActionPayloadDTO(); + serverActionPayloadDTO.setAction(action); + serverActionPayloadDTO.setContent(content); + serverActionPayloadDTO.setType("server"); + return serverActionPayloadDTO; + } + // 私有化 + private ServerActionPayloadDTO() {} +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionResponseDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionResponseDTO.java new file mode 100644 index 00000000..6286b74a --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ServerActionResponseDTO.java @@ -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 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); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java new file mode 100644 index 00000000..94e2672f --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionEnum.java @@ -0,0 +1,40 @@ +package xiaozhi.modules.sys.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import xiaozhi.common.exception.RenException; + +/** + * 服务端动作枚举 + */ +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; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java new file mode 100644 index 00000000..9e4c8307 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/enums/ServerActionResponseEnum.java @@ -0,0 +1,46 @@ +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; + +/** + * 服务端调用响应枚举 + */ +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; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java index 2187a8b4..02f595d2 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysParamsServiceImpl.java @@ -1,5 +1,6 @@ package xiaozhi.modules.sys.service.impl; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -55,7 +56,9 @@ public class SysParamsServiceImpl extends BaseServiceImpl wrapper = new QueryWrapper<>(); wrapper.eq("param_type", 1); - wrapper.like(StringUtils.isNotBlank(paramCode), "param_code", paramCode); + wrapper.nested(StringUtils.isNotBlank(paramCode), i -> i.like("param_code", paramCode) + .or() + .like("remark", paramCode)); return wrapper; } @@ -82,7 +85,7 @@ public class SysParamsServiceImpl extends BaseServiceImpl list = new ArrayList(); + list.add(Constant.SysMSMParam.SERVER_SMS_MAX_SEND_COUNT.getValue()); + list.add(Constant.SysMSMParam.ALIYUN_SMS_ACCESS_KEY_ID.getValue()); + list.add(Constant.SysMSMParam.ALIYUN_SMS_ACCESS_KEY_SECRET.getValue()); + list.add(Constant.SysMSMParam.ALIYUN_SMS_SIGN_NAME.getValue()); + list.add(Constant.SysMSMParam.ALIYUN_SMS_SMS_CODE_TEMPLATE_CODE.getValue()); + StringBuilder str = new StringBuilder(); + list.forEach(item -> { + if (!StringUtils.isNoneBlank(item)) { + str.append(",").append(item); + } + }); + if (!str.isEmpty()) { + String promptStr = "%s这些参数不可以为空"; + String substring = str.substring(1, str.length()); + throw new RenException(promptStr.formatted(substring)); + } + return true; + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java index 97944ba9..9f35836f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java @@ -133,6 +133,10 @@ public class SysUserServiceImpl extends BaseServiceImpl page = baseDao.selectPage( getPage(params, "id", true), - new QueryWrapper().eq(StringUtils.isNotBlank(dto.getMobile()), "username", + new QueryWrapper().like(StringUtils.isNotBlank(dto.getMobile()), "username", dto.getMobile())); // 循环处理page获取回来的数据,返回需要的字段 List list = page.getRecords().stream().map(user -> { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketClientManager.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketClientManager.java new file mode 100644 index 00000000..0c49aead --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/utils/WebSocketClientManager.java @@ -0,0 +1,315 @@ +package xiaozhi.modules.sys.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StopWatch; +import org.springframework.web.socket.*; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.handler.AbstractWebSocketHandler; +import xiaozhi.common.utils.DateUtils; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * WebSocketClientResource:支持 try-with-resources 模式 + */ +@Slf4j +public class WebSocketClientManager implements Closeable +{ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + // 全局回调线程池 + private static final ExecutorService CALLBACK_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() { + private final AtomicInteger cnt = new AtomicInteger(); + + public Thread newThread(Runnable r) + { + Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement()); + t.setDaemon(true); + return t; + } + }); + + private volatile WebSocketSession session; + private final BlockingQueue textMessageQueue; + private final BlockingQueue binaryMessageQueue; + private final CompletableFuture errorFuture; + private final long maxSessionDuration; + private final TimeUnit maxSessionDurationUnit; + + private volatile Consumer onText; + private volatile Consumer onBinary; + private volatile Consumer onError; + + private final String uri; + private final WebSocketHttpHeaders headers; + private final long connectTimeout; + private final TimeUnit connectUnit; + private final int queueCapacity; + + // 私有构造,仅由 Builder 调用 + private WebSocketClientManager(Builder b) { + this.uri = b.uri; + this.headers = b.headers != null ? b.headers : new WebSocketHttpHeaders(); + this.connectTimeout = b.connectTimeout; + this.connectUnit = b.connectUnit; + this.maxSessionDuration = b.maxSessionDuration; + this.maxSessionDurationUnit = b.maxSessionDurationUnit; + this.queueCapacity = b.queueCapacity; + this.textMessageQueue = new LinkedBlockingQueue<>(queueCapacity); + this.binaryMessageQueue = new LinkedBlockingQueue<>(queueCapacity); + this.errorFuture = new CompletableFuture<>(); + } + + public static WebSocketClientManager build(Builder b) throws InterruptedException, ExecutionException, TimeoutException, IOException { + WebSocketClientManager ws = new WebSocketClientManager(b); + StandardWebSocketClient client = new StandardWebSocketClient(); + CompletableFuture future = client.execute(ws.new InternalHandler(b.uri), b.headers, URI.create(b.uri)); + WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit); + if (sess == null || !sess.isOpen()) + { + throw new IOException("握手失败或会话未打开"); + } + ws.session = sess; + return ws; + } + + /** + * 发送 Text + */ + public void sendText(String text) throws IOException { + session.sendMessage(new TextMessage(text)); + } + + public void sendBinary(byte[] data) throws IOException { + session.sendMessage(new BinaryMessage(data)); + } + + public void sendJson(Object payload) throws IOException { + String json = OBJECT_MAPPER.writeValueAsString(payload); + session.sendMessage(new TextMessage(json)); + } + + + + private List listenerCustom( + BlockingQueue queue, + Predicate predicate) + throws InterruptedException, TimeoutException, ExecutionException + { + List collected = new ArrayList<>(); + long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration); + + while (true) { + if (errorFuture.isDone()) { + errorFuture.get(); + } + + long remaining = deadline - System.currentTimeMillis(); + if (remaining <= 0) { + throw new TimeoutException("等待批量消息超时"); + } + + T msg = queue.poll(remaining, TimeUnit.MILLISECONDS); + if (msg == null) { + throw new TimeoutException("等待批量消息超时"); + } + + collected.add(msg); + if (predicate.test(msg)) { + break; + } + } + close(); + return collected; + } + + /** + * 同步接收多条消息,直到 predicate 为 true 或超时抛异常; + * @return 返回监听期间的所有消息列表 + */ + public List listener(Predicate predicate) + throws InterruptedException, TimeoutException, ExecutionException + { + return listenerCustom(textMessageQueue, predicate); + } + + public List listenerBinary(Predicate predicate) + throws InterruptedException, TimeoutException, ExecutionException + { + return listenerCustom(binaryMessageQueue, predicate); + } + + /** + * 注册文本回调 + */ + public WebSocketClientManager onText(Consumer c) { + this.onText = c; + return this; + } + + /** + * 注册二进制回调 + */ + public WebSocketClientManager onBinary(Consumer c) { + this.onBinary = c; + return this; + } + + /** + * 注册错误回调 + */ + public WebSocketClientManager onError(Consumer c) { + this.onError = c; + return this; + } + + /** + * 关闭会话,try-with-resources / finally 自动调用 + */ + @Override + public void close() { + try { + if (session != null && session.isOpen()) { + session.close(CloseStatus.NORMAL); + } + } + catch (IOException ignored) {} + textMessageQueue.clear(); + binaryMessageQueue.clear(); + errorFuture.completeExceptionally(new IOException("WebSocket 已关闭")); + } + + private class InternalHandler extends AbstractWebSocketHandler { + private final String targetUri; + private final StopWatch stopWatch; + + InternalHandler(String targetUri) { + this.targetUri = targetUri; + this.stopWatch = new StopWatch(); + } + + /** + * 连接建立时回调 + */ + @Override + public void afterConnectionEstablished(WebSocketSession session) { + // 保存会话 + WebSocketClientManager.this.session = session; + this.stopWatch.start(); + log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN)); + } + + /** + * 处理文本消息 + */ + @Override + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { + String payload = message.getPayload(); + // 入队 + textMessageQueue.offer(payload); + // 回调用户注册的 onText + if (onText != null) { + CALLBACK_EXECUTOR.submit(() -> onText.accept(payload)); + } + } + + /** + * 处理二进制消息 + */ + @Override + protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception { + ByteBuffer buf = message.getPayload(); + byte[] data = new byte[buf.remaining()]; + buf.get(data); + // 入队 + binaryMessageQueue.offer(data); + // 回调用户注册的 onBinary + if (onBinary != null) { + CALLBACK_EXECUTOR.submit(() -> onBinary.accept(data)); + } + } + + /** + * 传输错误时回调 + */ + @Override + public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { + super.handleTransportError(session, exception); + // 保持原有逻辑:完成 errorFuture、回调 onError、关闭会话、异步通知连接失败 + errorFuture.completeExceptionally(exception); + if (onError != null) { + CALLBACK_EXECUTOR.submit(() -> onError.accept(exception)); + } + session.close(CloseStatus.SERVER_ERROR); + } + + /** + * 连接关闭时回调 + */ + @Override + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + super.afterConnectionClosed(session, status); + if (stopWatch.isRunning()) { + stopWatch.stop(); + } + log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s", + targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN), DateUtils.millsToSecond(stopWatch.getTotalTimeMillis())); + } + } + + public static class Builder { + private String uri; // 目标 WS URI + private long connectTimeout = 3; // 请求连接等待时间 + private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位 + private long maxSessionDuration = 5; // 最大连线时间,默认5秒 + private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位 + private int queueCapacity = 100; // 消息队列容量 + private WebSocketHttpHeaders headers; // 请求头 + + /** + * 目标 WS URI + */ + public Builder uri(String uri) { + this.uri = Objects.requireNonNull(uri); + return this; + } + + public Builder headers(WebSocketHttpHeaders h) { + this.headers = h; + return this; + } + + public Builder connectTimeout(long t, TimeUnit u) { + this.connectTimeout = t; + this.connectUnit = u; + return this; + } + + public Builder maxSessionDuration(long t, TimeUnit u) { + this.maxSessionDuration = t; + this.maxSessionDurationUnit = u; + return this; + } + + public Builder queueCapacity(int c) { + this.queueCapacity = c; + return this; + } + + public WebSocketClientManager build() throws InterruptedException, ExecutionException, TimeoutException, IOException { + return WebSocketClientManager.build(this); + } + + } +} diff --git a/main/manager-api/src/main/resources/db/changelog/202505182234.sql b/main/manager-api/src/main/resources/db/changelog/202505182234.sql new file mode 100644 index 00000000..3873e5d4 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505182234.sql @@ -0,0 +1,65 @@ +-- 添加手机短信注册功能的需要的参数 +delete from sys_params where id in (108, 109, 110, 111, 112, 113, 114, 115); +delete from sys_params where id in (610, 611, 612, 613); +INSERT INTO sys_params +(id, param_code, param_value, value_type, param_type, remark, creator, create_date, updater, update_date) + VALUES +(108, 'server.name', 'xiaozhi-esp32-server', 'string', 1, '系统名称', NULL, NULL, NULL, NULL), +(109, 'server.beian_icp_num', 'null', 'string', 1, 'icp备案号,填写null则不设置', NULL, NULL, NULL, NULL), +(110, 'server.beian_ga_num', 'null', 'string', 1, '公安备案号,填写null则不设置', NULL, NULL, NULL, NULL), +(111, 'server.enable_mobile_register', 'false', 'boolean', 1, '是否开启手机注册', NULL, NULL, NULL, NULL), +(112, 'server.sms_max_send_count', '10', 'number', 1, '单号码单日最大短信发送条数', NULL, NULL, NULL, NULL), +(610, 'aliyun.sms.access_key_id', '', 'string', 1, '阿里云平台access_key', NULL, NULL, NULL, NULL), +(611, 'aliyun.sms.access_key_secret', '', 'string', 1, '阿里云平台access_key_secret', NULL, NULL, NULL, NULL), +(612, 'aliyun.sms.sign_name', '', 'string', 1, '阿里云短信签名', NULL, NULL, NULL, NULL), +(613, 'aliyun.sms.sms_code_template_code', '', 'string', 1, '阿里云短信模板', NULL, NULL, NULL, NULL); + +update sys_params set remark = '是否允许管理员以外的人注册' where param_code = 'server.allow_user_register'; + +-- 增加手机区域字典 +-- 插入固件类型字典类型 +delete from `sys_dict_type` where `id` = 102; +INSERT INTO `sys_dict_type` (`id`, `dict_type`, `dict_name`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +(102, 'MOBILE_AREA', '手机区域', '手机区域字典', 0, 1, NOW(), 1, NOW()); + +-- 插入固件类型字典数据 +delete from `sys_dict_data` where `dict_type_id` = 102; +INSERT INTO `sys_dict_data` (`id`, `dict_type_id`, `dict_label`, `dict_value`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +(102001, 102, '中国大陆', '+86', '中国大陆', 1, 1, NOW(), 1, NOW()), +(102002, 102, '中国香港', '+852', '中国香港', 2, 1, NOW(), 1, NOW()), +(102003, 102, '中国澳门', '+853', '中国澳门', 3, 1, NOW(), 1, NOW()), +(102004, 102, '中国台湾', '+886', '中国台湾', 4, 1, NOW(), 1, NOW()), +(102005, 102, '美国/加拿大', '+1', '美国/加拿大', 5, 1, NOW(), 1, NOW()), +(102006, 102, '英国', '+44', '英国', 6, 1, NOW(), 1, NOW()), +(102007, 102, '法国', '+33', '法国', 7, 1, NOW(), 1, NOW()), +(102008, 102, '意大利', '+39', '意大利', 8, 1, NOW(), 1, NOW()), +(102009, 102, '德国', '+49', '德国', 9, 1, NOW(), 1, NOW()), +(102010, 102, '波兰', '+48', '波兰', 10, 1, NOW(), 1, NOW()), +(102011, 102, '瑞士', '+41', '瑞士', 11, 1, NOW(), 1, NOW()), +(102012, 102, '西班牙', '+34', '西班牙', 12, 1, NOW(), 1, NOW()), +(102013, 102, '丹麦', '+45', '丹麦', 13, 1, NOW(), 1, NOW()), +(102014, 102, '马来西亚', '+60', '马来西亚', 14, 1, NOW(), 1, NOW()), +(102015, 102, '澳大利亚', '+61', '澳大利亚', 15, 1, NOW(), 1, NOW()), +(102016, 102, '印度尼西亚', '+62', '印度尼西亚', 16, 1, NOW(), 1, NOW()), +(102017, 102, '菲律宾', '+63', '菲律宾', 17, 1, NOW(), 1, NOW()), +(102018, 102, '新西兰', '+64', '新西兰', 18, 1, NOW(), 1, NOW()), +(102019, 102, '新加坡', '+65', '新加坡', 19, 1, NOW(), 1, NOW()), +(102020, 102, '泰国', '+66', '泰国', 20, 1, NOW(), 1, NOW()), +(102021, 102, '日本', '+81', '日本', 21, 1, NOW(), 1, NOW()), +(102022, 102, '韩国', '+82', '韩国', 22, 1, NOW(), 1, NOW()), +(102023, 102, '越南', '+84', '越南', 23, 1, NOW(), 1, NOW()), +(102024, 102, '印度', '+91', '印度', 24, 1, NOW(), 1, NOW()), +(102025, 102, '巴基斯坦', '+92', '巴基斯坦', 25, 1, NOW(), 1, NOW()), +(102026, 102, '尼日利亚', '+234', '尼日利亚', 26, 1, NOW(), 1, NOW()), +(102027, 102, '孟加拉国', '+880', '孟加拉国', 27, 1, NOW(), 1, NOW()), +(102028, 102, '沙特阿拉伯', '+966', '沙特阿拉伯', 28, 1, NOW(), 1, NOW()), +(102029, 102, '阿联酋', '+971', '阿联酋', 29, 1, NOW(), 1, NOW()), +(102030, 102, '巴西', '+55', '巴西', 30, 1, NOW(), 1, NOW()), +(102031, 102, '墨西哥', '+52', '墨西哥', 31, 1, NOW(), 1, NOW()), +(102032, 102, '智利', '+56', '智利', 32, 1, NOW(), 1, NOW()), +(102033, 102, '阿根廷', '+54', '阿根廷', 33, 1, NOW(), 1, NOW()), +(102034, 102, '埃及', '+20', '埃及', 34, 1, NOW(), 1, NOW()), +(102035, 102, '南非', '+27', '南非', 35, 1, NOW(), 1, NOW()), +(102036, 102, '肯尼亚', '+254', '肯尼亚', 36, 1, NOW(), 1, NOW()), +(102037, 102, '坦桑尼亚', '+255', '坦桑尼亚', 37, 1, NOW(), 1, NOW()), +(102038, 102, '哈萨克斯坦', '+7', '哈萨克斯坦', 38, 1, NOW(), 1, NOW()); diff --git a/main/manager-api/src/main/resources/db/changelog/202505201744.sql b/main/manager-api/src/main/resources/db/changelog/202505201744.sql new file mode 100644 index 00000000..7b31b1cc --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202505201744.sql @@ -0,0 +1,3 @@ +-- 更新ai_model_provider的fields字段,将type为dict的改为string +update ai_model_provider set fields = replace(fields, '"type": "dict"', '"type": "string"') where id not in ('SYSTEM_LLM_fastgpt', 'SYSTEM_TTS_custom'); +update ai_model_provider set fields = replace(fields, '"type":"dict"', '"type": "string"') where id not in ('SYSTEM_LLM_fastgpt', 'SYSTEM_TTS_custom'); \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index a350b76a..add92bc5 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -143,9 +143,23 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202505142037.sql - changeSet: - id: 202505151450 + id: 202505182234 + author: amen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505182234.sql + - changeSet: + id: 202505201744 author: hrz changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202505151450.sql + path: classpath:db/changelog/202505201744.sql + - changeSet: + id: 202505151450 + author: hsoftxl + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202505151450.sql \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages.properties b/main/manager-api/src/main/resources/i18n/messages.properties index 5126feb2..8d04c5a0 100644 --- a/main/manager-api/src/main/resources/i18n/messages.properties +++ b/main/manager-api/src/main/resources/i18n/messages.properties @@ -9,6 +9,7 @@ 10005=\u8D26\u53F7\u5DF2\u88AB\u505C\u7528 10006=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A 10007=\u9A8C\u8BC1\u7801\u4E0D\u6B63\u786E +10008=\u624B\u673A\u53F7\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A 10009=\u539F\u5BC6\u7801\u4E0D\u6B63\u786E 10010=\u8D26\u53F7\u6216\u5BC6\u7801\u4E0D\u6B63\u786E,\u60A8\u8FD8\u6709\u53EF\u4EE5\u5C1D\u8BD5{0}\u6B21 10011=\u4E0A\u7EA7\u90E8\u95E8\u9009\u62E9\u9519\u8BEF diff --git a/main/manager-api/src/main/resources/lua/emptyAll.lua b/main/manager-api/src/main/resources/lua/emptyAll.lua new file mode 100644 index 00000000..1d984cb7 --- /dev/null +++ b/main/manager-api/src/main/resources/lua/emptyAll.lua @@ -0,0 +1 @@ +redis.call('FLUSHALL') \ No newline at end of file diff --git a/main/manager-api/src/main/resources/lua/getKeyOrCreate.lua b/main/manager-api/src/main/resources/lua/getKeyOrCreate.lua new file mode 100644 index 00000000..bc375b12 --- /dev/null +++ b/main/manager-api/src/main/resources/lua/getKeyOrCreate.lua @@ -0,0 +1,11 @@ +local value = redis.call('GET', KEYS[1]) +-- value 如果为空着设置值 +if not value then + local result = redis.call('SET', KEYS[1], ARGV[1]) + -- 检查 ARGV[2] 是否存在且大于 0 + local expireTime = tonumber(ARGV[2]) + if expireTime and expireTime > 0 then + redis.call('EXPIRE', KEYS[1], expireTime) + end +end +return value \ No newline at end of file diff --git a/main/manager-api/src/test/java/xiaozhi/modules/sys/loginControllerTest.java b/main/manager-api/src/test/java/xiaozhi/modules/sys/loginControllerTest.java new file mode 100644 index 00000000..5cc39967 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/sys/loginControllerTest.java @@ -0,0 +1,58 @@ +package xiaozhi.modules.sys; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import lombok.extern.slf4j.Slf4j; +import xiaozhi.modules.security.controller.LoginController; +import xiaozhi.modules.security.dto.LoginDTO; +import xiaozhi.modules.security.dto.SmsVerificationDTO; +import xiaozhi.modules.sys.dto.RetrievePasswordDTO; + +@Slf4j +@SpringBootTest +@ActiveProfiles("dev") +class loginControllerTest { + + @Autowired + LoginController loginController; + + @Test + public void testRegister() { + LoginDTO loginDTO = new LoginDTO(); + loginDTO.setUsername("手机号码"); + loginDTO.setPassword("密码"); + loginDTO.setCaptcha("123456"); + loginController.register(loginDTO); + } + + @Test + public void testSmsVerification() { + try { + SmsVerificationDTO smsVerificationDTO = new SmsVerificationDTO(); + smsVerificationDTO.setPhone("手机号码"); + smsVerificationDTO.setCaptchaId("123456"); + smsVerificationDTO.setCaptcha("123456"); + loginController.smsVerification(smsVerificationDTO); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + @Test + public void testRetrievePassword() { + try { + RetrievePasswordDTO retrievePasswordDTO = new RetrievePasswordDTO(); + retrievePasswordDTO.setCode("123456"); + retrievePasswordDTO.setPhone("手机号码"); + retrievePasswordDTO.setPassword("密码"); + loginController.retrievePassword(retrievePasswordDTO); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + + } + +} \ No newline at end of file diff --git a/main/manager-web/src/apis/httpRequest.js b/main/manager-web/src/apis/httpRequest.js index 43cdf92d..245bf6cc 100755 --- a/main/manager-web/src/apis/httpRequest.js +++ b/main/manager-web/src/apis/httpRequest.js @@ -20,6 +20,7 @@ function sendRequest() { return { _sucCallback: null, _failCallback: null, + _networkFailCallback: null, _method: 'GET', _data: {}, _header: { 'content-type': 'application/json; charset=utf-8' }, @@ -36,7 +37,7 @@ function sendRequest() { headers: this._header, responseType: this._responseType }).then((res) => { - const error = httpHandlerError(res, this._failCallback); + const error = httpHandlerError(res, this._failCallback, this._networkFailCallback); if (error) { return } @@ -47,7 +48,7 @@ function sendRequest() { }).catch((res) => { // 打印失败响应 console.log('catch', res) - httpHandlerError(res, this._failCallback) + httpHandlerError(res, this._failCallback, this._networkFailCallback) }) return this }, @@ -59,6 +60,10 @@ function sendRequest() { this._failCallback = callback return this }, + 'networkFail'(callback) { + this._networkFailCallback = callback + return this + }, 'url'(url) { if (url) { url = url.replaceAll('$', '/') @@ -95,11 +100,11 @@ function sendRequest() { /** * Info 请求完成后返回信息 - * callBack 回调函数 - * errTip 自定义错误信息 + * failCallback 回调函数 + * networkFailCallback 回调函数 */ // 在错误处理函数中添加日志 -function httpHandlerError(info, callBack) { +function httpHandlerError(info, failCallback, networkFailCallback) { /** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */ let networkError = false @@ -111,12 +116,16 @@ function httpHandlerError(info, callBack) { goToPage(Constant.PAGE.LOGIN, true); return true } else { - showDanger(info.data.msg) + if (failCallback) { + failCallback(info) + } else { + showDanger(info.data.msg) + } return true } } - if (callBack) { - callBack(info) + if (networkFailCallback) { + networkFailCallback(info) } else { showDanger(`网络请求出现了错误【${info.status}】`) } diff --git a/main/manager-web/src/apis/module/admin.js b/main/manager-web/src/apis/module/admin.js index fecc5877..27c1bc3e 100644 --- a/main/manager-web/src/apis/module/admin.js +++ b/main/manager-web/src/apis/module/admin.js @@ -18,7 +18,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('请求失败:', err) RequestService.reAjaxFun(() => { this.getUserList(callback) @@ -34,7 +34,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('删除失败:', err) RequestService.reAjaxFun(() => { this.deleteUser(id, callback) @@ -50,7 +50,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('重置密码失败:', err) RequestService.reAjaxFun(() => { this.resetUserPassword(id, callback) @@ -72,7 +72,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('获取参数列表失败:', err) RequestService.reAjaxFun(() => { this.getParamsList(params, callback) @@ -89,7 +89,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('添加参数失败:', err) RequestService.reAjaxFun(() => { this.addParam(data, callback) @@ -106,7 +106,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('更新参数失败:', err) RequestService.reAjaxFun(() => { this.updateParam(data, callback) @@ -123,12 +123,44 @@ export default { RequestService.clearRequestTime() callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('删除参数失败:', err) RequestService.reAjaxFun(() => { this.deleteParam(ids, callback) }) }).send() }, + // 获取ws服务端列表 + getWsServerList(params, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/server/server-list`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .networkFail((err) => { + console.error('获取ws服务端列表失败:', err) + RequestService.reAjaxFun(() => { + this.getWsServerList(params, callback) + }) + }).send(); + }, + // 发送ws服务器动作指令 + sendWsServerAction(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/server/emit-action`) + .method('POST') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .networkFail((err) => { + RequestService.reAjaxFun(() => { + this.sendWsServerAction(data, callback) + }) + }).send(); + } } diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js index 756e1bef..7f33d320 100644 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -12,7 +12,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.getAgentList(callback); }); @@ -28,7 +28,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.addAgent(agentName, callback); }); @@ -43,7 +43,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.deleteAgent(agentId, callback); }); @@ -58,7 +58,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('获取配置失败:', err); RequestService.reAjaxFun(() => { this.getDeviceConfig(agentId, callback); @@ -75,7 +75,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.updateAgentConfig(agentId, configData, callback); }); @@ -90,7 +90,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('获取模板失败:', err); RequestService.reAjaxFun(() => { this.getAgentTemplate(callback); @@ -107,7 +107,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.getAgentSessions(agentId, params, callback); }); @@ -122,7 +122,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.getAgentChatHistory(agentId, sessionId, callback); }); @@ -137,7 +137,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.getAudioId(audioId, callback); }); diff --git a/main/manager-web/src/apis/module/device.js b/main/manager-web/src/apis/module/device.js index 53738bc6..2f20425d 100644 --- a/main/manager-web/src/apis/module/device.js +++ b/main/manager-web/src/apis/module/device.js @@ -11,7 +11,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('获取设备列表失败:', err); RequestService.reAjaxFun(() => { this.getAgentBindDevices(agentId, callback); @@ -28,7 +28,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('解绑设备失败:', err); RequestService.reAjaxFun(() => { this.unbindDevice(device_id, callback); @@ -44,7 +44,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('绑定设备失败:', err); RequestService.reAjaxFun(() => { this.bindDevice(agentId, deviceCode, callback); @@ -59,7 +59,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('更新OTA状态失败:', err) this.$message.error(err.msg || '更新OTA状态失败') RequestService.reAjaxFun(() => { diff --git a/main/manager-web/src/apis/module/dict.js b/main/manager-web/src/apis/module/dict.js index caf78279..8919c46b 100644 --- a/main/manager-web/src/apis/module/dict.js +++ b/main/manager-web/src/apis/module/dict.js @@ -18,7 +18,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('获取字典类型列表失败:', err) this.$message.error(err.msg || '获取字典类型列表失败') RequestService.reAjaxFun(() => { @@ -36,7 +36,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('获取字典类型详情失败:', err) this.$message.error(err.msg || '获取字典类型详情失败') RequestService.reAjaxFun(() => { @@ -55,7 +55,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('新增字典类型失败:', err) this.$message.error(err.msg || '新增字典类型失败') RequestService.reAjaxFun(() => { @@ -74,7 +74,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('更新字典类型失败:', err) this.$message.error(err.msg || '更新字典类型失败') RequestService.reAjaxFun(() => { @@ -93,7 +93,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('删除字典类型失败:', err) this.$message.error(err.msg || '删除字典类型失败') RequestService.reAjaxFun(() => { @@ -119,7 +119,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('获取字典数据列表失败:', err) this.$message.error(err.msg || '获取字典数据列表失败') RequestService.reAjaxFun(() => { @@ -137,7 +137,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('获取字典数据详情失败:', err) this.$message.error(err.msg || '获取字典数据详情失败') RequestService.reAjaxFun(() => { @@ -156,7 +156,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('新增字典数据失败:', err) this.$message.error(err.msg || '新增字典数据失败') RequestService.reAjaxFun(() => { @@ -175,7 +175,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('更新字典数据失败:', err) this.$message.error(err.msg || '更新字典数据失败') RequestService.reAjaxFun(() => { @@ -194,7 +194,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('删除字典数据失败:', err) this.$message.error(err.msg || '删除字典数据失败') RequestService.reAjaxFun(() => { @@ -217,7 +217,7 @@ export default { reject(new Error(res.data?.msg || '获取字典数据列表失败')) } }) - .fail((err) => { + .networkFail((err) => { console.error('获取字典数据列表失败:', err) reject(err) }).send() diff --git a/main/manager-web/src/apis/module/model.js b/main/manager-web/src/apis/module/model.js index ec0ea32f..badf4174 100644 --- a/main/manager-web/src/apis/module/model.js +++ b/main/manager-web/src/apis/module/model.js @@ -18,7 +18,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('获取模型列表失败:', err) RequestService.reAjaxFun(() => { this.getModelList(params, callback) @@ -34,7 +34,7 @@ export default { RequestService.clearRequestTime() callback(res.data?.data || []) }) - .fail((err) => { + .networkFail((err) => { console.error('获取供应器列表失败:', err) this.$message.error('获取供应器列表失败') RequestService.reAjaxFun(() => { @@ -65,7 +65,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('新增模型失败:', err) this.$message.error(err.msg || '新增模型失败') RequestService.reAjaxFun(() => { @@ -82,7 +82,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('删除模型失败:', err) this.$message.error(err.msg || '删除模型失败') RequestService.reAjaxFun(() => { @@ -100,7 +100,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.getModelNames(modelType, modelName, callback); }); @@ -118,7 +118,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail(() => { + .networkFail(() => { RequestService.reAjaxFun(() => { this.getModelVoices(modelId, voiceName, callback); }); @@ -133,7 +133,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('获取模型配置失败:', err) this.$message.error(err.msg || '获取模型配置失败') RequestService.reAjaxFun(() => { @@ -150,7 +150,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('更新模型状态失败:', err) this.$message.error(err.msg || '更新模型状态失败') RequestService.reAjaxFun(() => { @@ -166,20 +166,20 @@ export default { configJson: formData.configJson }; RequestService.sendRequest() - .url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`) - .method('PUT') - .data(payload) - .success((res) => { - RequestService.clearRequestTime(); - callback(res); - }) - .fail((err) => { - console.error('更新模型失败:', err); - this.$message.error(err.msg || '更新模型失败'); - RequestService.reAjaxFun(() => { - this.updateModel(params, callback); - }); - }).send(); + .url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`) + .method('PUT') + .data(payload) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail((err) => { + console.error('更新模型失败:', err); + this.$message.error(err.msg || '更新模型失败'); + RequestService.reAjaxFun(() => { + this.updateModel(params, callback); + }); + }).send(); }, // 设置默认模型 setDefaultModel(id, callback) { @@ -190,12 +190,119 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('设置默认模型失败:', err) this.$message.error(err.msg || '设置默认模型失败') RequestService.reAjaxFun(() => { this.setDefaultModel(id, callback) }) }).send() - } + }, + + /** + * 获取模型配置列表(支持查询参数) + * @param {Object} params - 查询参数对象,例如 { name: 'test', modelType: 1 } + * @param {Function} callback - 回调函数 + */ + getModelProvidersPage(params, callback) { + // 构建查询参数 + const queryParams = new URLSearchParams(); + if (params.name) queryParams.append('name', params.name); + if (params.modelType !== undefined) queryParams.append('modelType', params.modelType); + if (params.page !== undefined) queryParams.append('page', params.page); + if (params.limit !== undefined) queryParams.append('limit', params.limit); + + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider?${queryParams.toString()}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail((err) => { + this.$message.error(err.msg || '获取供应器列表失败'); + RequestService.reAjaxFun(() => { + this.getModelProviders(params, callback); + }); + }).send(); + }, + + /** + * 新增模型供应器配置 + * @param {Object} params - 请求参数对象,例如 { modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 } + * @param {Function} callback - 成功回调函数 + */ + addModelProvider(params, callback) { + const postData = { + modelType: params.modelType || '', + providerCode: params.providerCode || '', + name: params.name || '', + fields: JSON.stringify(params.fields || []), + sort: params.sort || 0 + }; + + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider`) + .method('POST') + .data(postData) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail((err) => { + console.error('新增模型供应器失败:', err) + this.$message.error(err.msg || '新增模型供应器失败') + RequestService.reAjaxFun(() => { + this.addModelProvider(params, callback); + }); + }).send(); + }, + + /** + * 更新模型供应器配置 + * @param {Object} params - 请求参数对象,例如 { id: '111', modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 } + * @param {Function} callback - 成功回调函数 + */ + updateModelProvider(params, callback) { + const putData = { + id: params.id || '', + modelType: params.modelType || '', + providerCode: params.providerCode || '', + name: params.name || '', + fields: JSON.stringify(params.fields || []), + sort: params.sort || 0 + }; + + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider`) + .method('PUT') + .data(putData) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail((err) => { + this.$message.error(err.msg || '更新模型供应器失败') + RequestService.reAjaxFun(() => { + this.updateModelProvider(params, callback); + }); + }).send(); + }, + // 删除 + deleteModelProviderByIds(ids, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/models/provider/delete`) + .method('POST') + .data(ids) + .success((res) => { + RequestService.clearRequestTime() + callback(res); + }) + .networkFail((err) => { + this.$message.error(err.msg || '删除模型供应器失败') + RequestService.reAjaxFun(() => { + this.deleteModelProviderByIds(ids, callback) + }) + }).send() + }, } diff --git a/main/manager-web/src/apis/module/ota.js b/main/manager-web/src/apis/module/ota.js index 438aa3d8..4db86ad7 100644 --- a/main/manager-web/src/apis/module/ota.js +++ b/main/manager-web/src/apis/module/ota.js @@ -12,7 +12,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('获取OTA固件列表失败:', err); RequestService.reAjaxFun(() => { this.getOtaList(params, callback); @@ -28,7 +28,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('获取OTA固件信息失败:', err); RequestService.reAjaxFun(() => { this.getOtaInfo(id, callback); @@ -45,7 +45,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('保存OTA固件信息失败:', err); RequestService.reAjaxFun(() => { this.saveOta(entity, callback); @@ -62,7 +62,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('更新OTA固件信息失败:', err); RequestService.reAjaxFun(() => { this.updateOta(id, entity, callback); @@ -78,7 +78,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('删除OTA固件失败:', err); RequestService.reAjaxFun(() => { this.deleteOta(id, callback); @@ -97,7 +97,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('上传固件文件失败:', err); RequestService.reAjaxFun(() => { this.uploadFirmware(file, callback); @@ -113,7 +113,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('获取下载链接失败:', err); RequestService.reAjaxFun(() => { this.getDownloadUrl(id, callback); diff --git a/main/manager-web/src/apis/module/timbre.js b/main/manager-web/src/apis/module/timbre.js index 91920522..7aa6a711 100644 --- a/main/manager-web/src/apis/module/timbre.js +++ b/main/manager-web/src/apis/module/timbre.js @@ -1,4 +1,4 @@ -import {getServiceUrl} from '../api'; +import { getServiceUrl } from '../api'; import RequestService from '../httpRequest'; export default { @@ -18,7 +18,7 @@ export default { RequestService.clearRequestTime(); callback(res.data || []); }) - .fail((err) => { + .networkFail((err) => { console.error('获取音色列表失败:', err); RequestService.reAjaxFun(() => { this.getVoiceList(params, callback); @@ -42,7 +42,7 @@ export default { .success((res) => { callback(res.data); }) - .fail((err) => { + .networkFail((err) => { console.error('保存音色失败:', err); RequestService.reAjaxFun(() => { this.saveVoice(params, callback); @@ -59,7 +59,7 @@ export default { RequestService.clearRequestTime() callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('删除音色失败:', err); RequestService.reAjaxFun(() => { this.deleteVoice(ids, callback); @@ -82,7 +82,7 @@ export default { .success((res) => { callback(res.data); }) - .fail((err) => { + .networkFail((err) => { console.error('修改音色失败:', err); RequestService.reAjaxFun(() => { this.updateVoice(params, callback); diff --git a/main/manager-web/src/apis/module/user.js b/main/manager-web/src/apis/module/user.js index 84d5b7f9..6c16c3e6 100755 --- a/main/manager-web/src/apis/module/user.js +++ b/main/manager-web/src/apis/module/user.js @@ -4,7 +4,7 @@ import RequestService from '../httpRequest' export default { // 登录 - login(loginForm, callback) { + login(loginForm, callback, failCallback) { RequestService.sendRequest() .url(`${getServiceUrl()}/user/login`) .method('POST') @@ -13,7 +13,11 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail(() => { + .fail((err) => { + RequestService.clearRequestTime() + failCallback(err) + }) + .networkFail(() => { RequestService.reAjaxFun(() => { this.login(loginForm, callback) }) @@ -34,12 +38,32 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { // 添加错误参数 + .networkFail((err) => { // 添加错误参数 }).send() }, + // 发送短信验证码 + sendSmsVerification(data, callback, failCallback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/user/smsVerification`) + .method('POST') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + RequestService.clearRequestTime() + failCallback(err) + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.sendSmsVerification(data, callback, failCallback) + }) + }).send() + }, // 注册账号 - register(registerForm, callback) { + register(registerForm, callback, failCallback) { RequestService.sendRequest() .url(`${getServiceUrl()}/user/register`) .method('POST') @@ -48,7 +72,14 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail(() => { + .fail((err) => { + RequestService.clearRequestTime() + failCallback(err) + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.register(registerForm, callback, failCallback) + }) }).send() }, // 保存设备配置 @@ -61,7 +92,7 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('保存配置失败:', err); RequestService.reAjaxFun(() => { this.saveDeviceConfig(device_id, configData, callback); @@ -77,7 +108,7 @@ export default { RequestService.clearRequestTime() callback(res) }) - .fail((err) => { + .networkFail((err) => { console.error('接口请求失败:', err) RequestService.reAjaxFun(() => { this.getUserInfo(callback) @@ -97,7 +128,7 @@ export default { RequestService.clearRequestTime(); successCallback(res); }) - .fail((error) => { + .networkFail((error) => { RequestService.reAjaxFun(() => { this.changePassword(oldPassword, newPassword, successCallback, errorCallback); }); @@ -115,7 +146,7 @@ export default { RequestService.clearRequestTime() successCallback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('修改用户状态失败:', err) RequestService.reAjaxFun(() => { this.changeUserStatus(status, userIds) @@ -131,11 +162,35 @@ export default { RequestService.clearRequestTime(); callback(res); }) - .fail((err) => { + .networkFail((err) => { console.error('获取公共配置失败:', err); RequestService.reAjaxFun(() => { this.getPubConfig(callback); }); }).send(); }, + // 找回用户密码 + retrievePassword(passwordData, callback, failCallback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/user/retrieve-password`) + .method('PUT') + .data({ + phone: passwordData.phone, + code: passwordData.code, + password: passwordData.password + }) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .fail((err) => { + RequestService.clearRequestTime(); + failCallback(err); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.retrievePassword(passwordData, callback, failCallback); + }); + }).send() + } } diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 842441ce..bb5baca5 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -52,6 +52,9 @@ 供应器管理 + + 服务端管理 + @@ -145,6 +148,9 @@ export default { goProviderManagement() { this.$router.push('/provider-management') }, + goServerSideManagement() { + this.$router.push('/server-side-management') + }, // 获取用户信息 fetchUserInfo() { userApi.getUserInfo(({ data }) => { @@ -322,6 +328,12 @@ export default { line-height: 30px; } +.custom-search-input::v-deep .el-input__suffix-inner { + display: flex; + align-items: center; + height: 100%; +} + .avatar-img { width: 21px; height: 21px; diff --git a/main/manager-web/src/components/ModelEditDialog.vue b/main/manager-web/src/components/ModelEditDialog.vue index 8eebd8d0..406284a5 100644 --- a/main/manager-web/src/components/ModelEditDialog.vue +++ b/main/manager-web/src/components/ModelEditDialog.vue @@ -66,9 +66,12 @@
- - + +
@@ -112,6 +115,7 @@ export default { pendingProviderType: null, pendingModelData: null, dynamicCallInfoFields: [], + fieldJsonMap: {}, // 用于存储JSON字段的字符串形式 form: { id: "", modelType: "", @@ -175,9 +179,7 @@ export default { sort: 0, configJson: {} }; - this.dynamicCallInfoFields.forEach(field => { - this.$set(this.form.configJson, field.prop, ''); - }); + this.fieldJsonMap = {}; }, resetProviders() { this.providers = []; @@ -203,7 +205,14 @@ export default { handleSave() { this.saving = true; // 开始保存加载 - const provideCode = this.form.configJson.type; + // 处理所有JSON字段 + Object.keys(this.fieldJsonMap).forEach(key => { + const parsed = this.validateJson(this.fieldJsonMap[key]); + if (parsed !== null) { + this.form.configJson[key] = parsed; + } + }); + const formData = { id: this.modelData.id, modelCode: this.form.modelCode, @@ -213,13 +222,11 @@ export default { docLink: this.form.docLink, remark: this.form.remark, sort: this.form.sort || 0, - configJson: { - ...this.form.configJson, - } + configJson: { ...this.form.configJson } }; this.$emit("save", { - provideCode, + provideCode: this.form.configJson.type, formData, done: () => { this.saving = false; // 保存完成后回调 @@ -240,7 +247,6 @@ export default { value: String(item.providerCode) })); this.providersLoaded = true; - this.allProvidersData = data; if (this.pendingProviderType) { @@ -255,7 +261,7 @@ export default { this.dynamicCallInfoFields = JSON.parse(provider.fields || '[]').map(f => ({ label: f.label, prop: f.key, - type: f.type === 'password' ? 'password' : 'text', + type: f.type === 'dict' ? 'json-textarea' : (f.type === 'password' ? 'password' : 'text'), placeholder: `请输入${f.label}` })); @@ -272,6 +278,9 @@ export default { this.dynamicCallInfoFields.forEach(field => { if (!configJson.hasOwnProperty(field.prop)) { configJson[field.prop] = ''; + } else if (field.type === 'json-textarea') { + this.$set(this.fieldJsonMap, field.prop, this.formatJson(configJson[field.prop])); + configJson[field.prop] = this.ensureObject(configJson[field.prop]); } else if (typeof configJson[field.prop] !== 'string') { configJson[field.prop] = String(configJson[field.prop]); } @@ -287,10 +296,43 @@ export default { docLink: model.docLink, remark: model.remark, sort: Number(model.sort) || 0, - configJson: { - ...configJson - } + configJson: { ...configJson } }; + }, + handleJsonChange(field, value) { + const parsed = this.validateJson(value); + if (parsed !== null) { + this.form.configJson[field] = parsed; + } + }, + validateJson(value) { + try { + const parsed = JSON.parse(value); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + return parsed; + } + this.$message.error({ + message: '必须输入字典格式(如 {"key":"value"}),保存则使用原数据', + showClose: true + }); + return null; + } catch (e) { + this.$message.error({ + message: 'JSON格式错误(如 {"key":"value"}),保存则使用原数据', + showClose: true + }); + return null; + } + }, + formatJson(obj) { + try { + return JSON.stringify(obj, null, 2); + } catch { + return ''; + } + }, + ensureObject(value) { + return typeof value === 'object' ? value : {}; } } }; @@ -316,7 +358,6 @@ export default { justify-content: center; } - .custom-close-btn { position: absolute; top: 20px; @@ -452,21 +493,14 @@ export default { height: 32px; } - .custom-form .el-form-item { margin-bottom: 20px; } - -.custom-input-bg .el-input__inner { - height: 32px; -} - - .custom-form .el-form-item__label { color: #3d4566; font-weight: normal; text-align: right; padding-right: 20px; } - + \ No newline at end of file diff --git a/main/manager-web/src/components/ProviderDialog.vue b/main/manager-web/src/components/ProviderDialog.vue index 844282ee..7fd1fcf8 100644 --- a/main/manager-web/src/components/ProviderDialog.vue +++ b/main/manager-web/src/components/ProviderDialog.vue @@ -1,5 +1,6 @@