Browse Source

feat(system): 添加邮箱验证码功能并优化验证码样式

- 新增 sendEmailCode 接口用于发送邮箱验证码
- 将验证码有效期从2分钟调整为5分钟
- 优化用户资料页面卡片阴影效果
- 实现HTML格式验证码邮件模板,提升用户体验
- 添加远程配置服务接口和实现类
- 移除参数键值长度限制校验
- 实现验证码模板配置化管理
- 在用户资料页面添加邮箱验证码发送按钮
- 实现验证码发送倒计时功能
- 优化验证码生成器配置,添加字体和背景设置
JX.Li 2 weeks ago
parent
commit
78fef37a77

+ 12 - 0
nexo-api/nexo-api-system/src/main/java/com/nexo/system/api/RemoteConfigService.java

@@ -0,0 +1,12 @@
+package com.nexo.system.api;
+
+/**
+ * 参数配置服务
+ *
+ * @author Lion Li
+ */
+public interface RemoteConfigService {
+
+    String getConfigKey(String configKey);
+
+}

+ 1 - 1
nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/constant/Constants.java

@@ -74,7 +74,7 @@ public interface Constants {
     /**
      * 验证码有效期(分钟)
      */
-    long CAPTCHA_EXPIRATION = 2;
+    long CAPTCHA_EXPIRATION = 5;
 
     /**
      * 防重提交 redis key

File diff suppressed because it is too large
+ 0 - 1
nexo-example/nexo-demo/src/main/java/com/nexo/demo/controller/MailController.java


+ 5 - 4
nexo-gateway/src/main/java/com/nexo/gateway/service/impl/ValidateCodeServiceImpl.java

@@ -1,7 +1,7 @@
 package com.nexo.gateway.service.impl;
 
 import cn.hutool.captcha.AbstractCaptcha;
-import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.captcha.generator.RandomGenerator;
 import cn.hutool.core.util.IdUtil;
 import com.nexo.common.core.constant.CacheConstants;
 import com.nexo.common.core.constant.Constants;
@@ -10,7 +10,6 @@ import com.nexo.common.core.exception.CaptchaException;
 import com.nexo.common.core.exception.user.CaptchaExpireException;
 import com.nexo.common.core.utils.SpringUtils;
 import com.nexo.common.core.utils.StringUtils;
-import com.nexo.common.core.utils.reflect.ReflectUtils;
 import com.nexo.common.redis.utils.RedisUtils;
 import com.nexo.gateway.config.properties.CaptchaProperties;
 import com.nexo.gateway.enums.CaptchaType;
@@ -21,6 +20,7 @@ import org.springframework.expression.ExpressionParser;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 import org.springframework.stereotype.Service;
 
+import java.awt.*;
 import java.io.IOException;
 import java.time.Duration;
 import java.util.HashMap;
@@ -55,9 +55,10 @@ public class ValidateCodeServiceImpl implements ValidateCodeService {
         CaptchaType captchaType = captchaProperties.getType();
         boolean isMath = CaptchaType.MATH == captchaType;
         Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
-        CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
         AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
-        captcha.setGenerator(codeGenerator);
+        captcha.setGenerator(new RandomGenerator("0123456789", length));
+        captcha.setFont(new Font("文泉驿正黑", Font.ITALIC | Font.BOLD, 35));
+        captcha.setBackground(new Color(255, 255, 255));
         captcha.createCode();
         String code = captcha.getCode();
         if (isMath) {

+ 22 - 4
nexo-modules/nexo-resource/src/main/java/com/nexo/resource/controller/SysEmailController.java

@@ -9,8 +9,10 @@ import com.nexo.common.core.web.controller.BaseController;
 import com.nexo.common.mail.config.properties.MailProperties;
 import com.nexo.common.mail.utils.MailUtils;
 import com.nexo.common.redis.utils.RedisUtils;
+import com.nexo.system.api.RemoteConfigService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -18,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.validation.constraints.NotBlank;
 import java.time.Duration;
+import java.util.UUID;
 
 /**
  * 邮件功能
@@ -33,6 +36,9 @@ public class SysEmailController extends BaseController {
 
     private final MailProperties mailProperties;
 
+    @DubboReference
+    private final RemoteConfigService remoteConfigService;
+
     /**
      * 邮箱验证码
      *
@@ -46,13 +52,25 @@ public class SysEmailController extends BaseController {
         String key = CacheConstants.CAPTCHA_CODE_KEY + email;
         String code = RandomUtil.randomNumbers(4);
         RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+        String string = UUID.randomUUID().toString();
+        RedisUtils.setCacheObject(string, key, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
         try {
-            MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
+            String template = remoteConfigService.getConfigKey("auth_code_template");
+            if (template == null || template.isEmpty()) {
+                log.error("验证码模板配置为空");
+                return R.fail("验证码模板配置不存在");
+            }
+            String content = template.replace("{CODE}", code).replace("{EXPIRE}", String.valueOf(Constants.CAPTCHA_EXPIRATION));
+
+            MailUtils.sendHtml(email, "易盟数科邮箱验证", content);
+        } catch (IllegalArgumentException e) {
+            log.error("验证码模板格式错误 => {}", e.getMessage(), e);
+            return R.fail("验证码模板格式错误");
         } catch (Exception e) {
-            log.error("验证码短信发送异常 => {}", e.getMessage());
-            return R.fail(e.getMessage());
+            log.error("验证码邮件发送异常 => {}", e.getMessage(), e);
+            return R.fail("验证码发送失败,请稍后重试");
         }
-        return R.ok();
+        return R.ok(string);
     }
 
 }

+ 0 - 1
nexo-modules/nexo-system/src/main/java/com/nexo/system/domain/SysConfig.java

@@ -55,7 +55,6 @@ public class SysConfig extends BaseEntity {
      */
     @ExcelProperty(value = "参数键值")
     @NotBlank(message = "参数键值不能为空")
-    @Size(min = 0, max = 500, message = "参数键值长度不能超过500个字符")
     private String configValue;
 
     /**

+ 27 - 0
nexo-modules/nexo-system/src/main/java/com/nexo/system/dubbo/RemoteConfigServiceImpl.java

@@ -0,0 +1,27 @@
+package com.nexo.system.dubbo;
+
+import com.nexo.system.api.RemoteConfigService;
+import com.nexo.system.service.ISysConfigService;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.springframework.stereotype.Service;
+
+
+/**
+ * 参数配置服务
+ *
+ * @author nexo
+ */
+
+@RequiredArgsConstructor
+@Service
+@DubboService
+public class RemoteConfigServiceImpl implements RemoteConfigService {
+
+    private final ISysConfigService sysConfigService;
+
+    @Override
+    public String getConfigKey(String configKey) {
+        return sysConfigService.selectConfigByKey(configKey);
+    }
+}

+ 10 - 0
nexo-ui/src/api/system/code.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 发送邮箱验证码
+export function sendEmailCode(email) {
+  return request({
+    url: '/resource/email/code',
+    method: 'get',
+    params: { email: email }
+  })
+}

+ 2 - 2
nexo-ui/src/views/system/user/profile/index.vue

@@ -2,7 +2,7 @@
   <div class="app-container">
     <el-row :gutter="20">
       <el-col :span="6" :xs="24">
-        <el-card class="box-card">
+        <el-card class="box-card" shadow="never">
           <div slot="header" class="clearfix">
             <span>个人信息</span>
           </div>
@@ -40,7 +40,7 @@
         </el-card>
       </el-col>
       <el-col :span="18" :xs="24">
-        <el-card>
+        <el-card shadow="never">
           <div slot="header" class="clearfix">
             <span>基本资料</span>
           </div>

+ 52 - 2
nexo-ui/src/views/system/user/profile/userInfo.vue

@@ -2,12 +2,14 @@
   <el-form ref="form" :model="user" :rules="rules" label-width="80px">
     <el-form-item label="用户昵称" prop="nickName">
       <el-input v-model="user.nickName" maxlength="30" />
-    </el-form-item> 
+    </el-form-item>
     <el-form-item label="手机号码" prop="phonenumber">
       <el-input v-model="user.phonenumber" maxlength="11" />
     </el-form-item>
     <el-form-item label="邮箱" prop="email">
-      <el-input v-model="user.email" maxlength="50" />
+      <el-input v-model="user.email" maxlength="50" >
+        <el-button slot="append" @click="sendEmailCode">{{but_text}}</el-button>
+      </el-input>
     </el-form-item>
     <el-form-item label="性别">
       <el-radio-group v-model="user.sex">
@@ -24,6 +26,7 @@
 
 <script>
 import { updateUserProfile } from "@/api/system/user";
+import { sendEmailCode } from '@/api/system/code'
 
 export default {
   props: {
@@ -33,6 +36,10 @@ export default {
   },
   data() {
     return {
+      but_text: "发送验证码",
+      count_down: 0,
+      count_down_timer: null,
+      is_sending: false,
       // 表单校验
       rules: {
         nickName: [
@@ -57,7 +64,50 @@ export default {
       }
     };
   },
+  beforeDestroy() {
+    if (this.count_down_timer) {
+      clearInterval(this.count_down_timer);
+      this.count_down_timer = null;
+    }
+  },
   methods: {
+    sendEmailCode(){
+      if (this.is_sending || this.count_down > 0) {
+        return;
+      }
+
+      if (!this.user.email) {
+        this.$modal.msgError("请输入邮箱地址");
+        return;
+      }
+
+      const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+      if (!emailRegex.test(this.user.email)) {
+        this.$modal.msgError("请输入正确的邮箱地址");
+        return;
+      }
+
+
+      this.is_sending = true;
+      sendEmailCode(this.user.email).then(response => {
+        this.$modal.msgSuccess("验证码已发送,请注意查收");
+        this.count_down = 60;
+        this.but_text = `重新发送(${this.count_down}s)`;
+        this.count_down_timer = setInterval(() => {
+          this.count_down--;
+          if (this.count_down <= 0) {
+            clearInterval(this.count_down_timer);
+            this.but_text = "发送验证码";
+            this.is_sending = false;
+          } else {
+            this.but_text = `重新发送(${this.count_down}s)`;
+          }
+        }, 1000);
+      }).catch(error => {
+        this.$modal.msgError(error.message || "发送失败,请重试");
+        this.is_sending = false;
+      });
+    },
     submit() {
       this.$refs["form"].validate(valid => {
         if (valid) {

Some files were not shown because too many files changed in this diff