Ver Fonte

```
feat(user): 添加短信验证码发送与手机号修改功能- 新增 getSmsCode 接口用于获取短信验证码,防止手机号重复注册
- 新增 updateUserPhone 接口用于修改用户绑定的手机号
- 集成阿里云短信服务,新增 SmsUtil 工具类实现短信发送逻辑- 在 UserBo 中添加 verifyCode 和 newPhone 字段支持手机号修改
- 忽略 UserMapper 中 tenantLine 拦截器以支持手机号查询
- 添加相关依赖:aliyun-java-sdk-dysmsapi 和 aliyun-java-sdk-core- 调整 loginPass 验证注解,使其在新增和编辑时非必填```

fugui001 há 3 meses atrás
pai
commit
20f09edd98

+ 0 - 1
ruoyi-admin/pom.xml

@@ -97,7 +97,6 @@
             <groupId>de.codecentric</groupId>
             <artifactId>spring-boot-admin-starter-client</artifactId>
         </dependency>
-
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>

+ 10 - 0
ruoyi-modules/ruoyi-system/pom.xml

@@ -99,7 +99,17 @@
             <groupId>org.dromara</groupId>
             <artifactId>ruoyi-common-sse</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
+            <version>1.1.0</version>
+        </dependency>
 
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-core</artifactId>
+            <version>4.5.9</version> <!-- 请检查是否有更新版本 -->
+        </dependency>
     </dependencies>
 
 </project>

+ 21 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/business/controller/UserController.java

@@ -116,4 +116,25 @@ public class UserController extends BaseController {
         return toAjax(userService.sendRewardToos(bo));
     }
 
+
+    @SaCheckPermission("business:user:getSmsCode")
+    @Log(title = "【发送短信】")
+    @RepeatSubmit()
+    @GetMapping("/getSmsCode")
+    public R<Void> getSmsCode(@RequestParam String phone) {
+        return userService.getSmsCode(phone);
+    }
+
+
+    @SaCheckPermission("business:user:updateUserPhone")
+    @Log(title = "【更改手机号】", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping("/updateUserPhone")
+    public R<Void> updateUserPhone(@RequestBody UserBo bo) {
+        return userService.updateUserPhone(bo);
+    }
+
+
+
+
 }

+ 7 - 1
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/business/domain/bo/UserBo.java

@@ -36,7 +36,7 @@ public class UserBo extends BaseEntity {
     /**
      * 登录密码
      */
-    @NotBlank(message = "登录密码不能为空", groups = { AddGroup.class, EditGroup.class })
+    //@NotBlank(message = "登录密码不能为空", groups = { AddGroup.class, EditGroup.class })
     private String loginPass;
 
     /**
@@ -154,4 +154,10 @@ public class UserBo extends BaseEntity {
      */
     private String loginEndTime;
 
+
+    private String verifyCode;
+
+    private String newPhone;
+
+
 }

+ 4 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/business/mapper/UserMapper.java

@@ -43,5 +43,9 @@ public interface UserMapper extends BaseMapperPlus<User, UserVo> {
     @InterceptorIgnore(tenantLine = "true")
     UserVo selUserInfo(@Param("playerNameOrId") String playerNameOrId);
 
+    @InterceptorIgnore(tenantLine = "true")
+    UserVo selUserPhoneExit(@Param("phone") String phone);
+
+
 
 }

+ 17 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/business/service/IUserService.java

@@ -3,6 +3,7 @@ package org.dromara.business.service;
 import org.dromara.business.domain.bo.ItemsUserBo;
 import org.dromara.business.domain.bo.UserBo;
 import org.dromara.business.domain.vo.UserVo;
+import org.dromara.common.core.domain.R;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
 
@@ -75,5 +76,21 @@ public interface IUserService {
      */
     Boolean sendRewardToos(ItemsUserBo bo);
 
+    /**
+     * 获取验证码
+     *
+     * @param phone
+     * @return
+     */
+    R<Void> getSmsCode(String phone);
+
+    /**
+     * 修改用户手机号
+     *
+     * @param bo
+     * @return
+     */
+    R<Void> updateUserPhone(UserBo bo);
+
 
 }

+ 100 - 4
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/business/service/impl/UserServiceImpl.java

@@ -1,8 +1,12 @@
 package org.dromara.business.service.impl;
 
 import cn.hutool.json.JSONObject;
+import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
+import com.aliyuncs.exceptions.ClientException;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.dromara.business.domain.PlayerItems;
 import org.dromara.business.domain.PlayersItemsLog;
 import org.dromara.business.domain.User;
@@ -16,6 +20,8 @@ import org.dromara.business.service.IUserService;
 import org.dromara.business.utils.ItemOperationLock;
 import org.dromara.business.utils.RedisKeys;
 import org.dromara.business.utils.RedisUtil;
+import org.dromara.business.utils.SmsUtil;
+import org.dromara.common.core.domain.R;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -29,10 +35,10 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Collection;
+import java.rmi.ServerException;
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 【请填写功能名称】Service业务层处理
@@ -55,6 +61,9 @@ public class UserServiceImpl implements IUserService {
 
     private final PlayersItemsLogMapper playersItemsLogMapper;
 
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
     @Autowired
     RedisUtil redisUtil;
 
@@ -388,6 +397,93 @@ public class UserServiceImpl implements IUserService {
         }
     }
 
+    // 用于存储验证码信息
+    private final Map<String, String> codeStore = new HashMap<>();
+    // 存储验证码过期时间,默认5分钟过期
+    private final Map<String, Long> expireTime = new HashMap<>();
+
+
+    @Override
+    public R<Void> getSmsCode(String phone) {
+        //查看对应手机号是否已经存在了
+        UserVo userVo = baseMapper.selUserPhoneExit(phone);
+        if(userVo!=null){
+            return R.fail("手机号码已被占用!");
+        }
+        String code = generateVerificationCode();
+        codeStore.put(phone, code);
+        expireTime.put(phone, System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(1));
+        try {
+            SendSmsResponse sendSmsResponse = SmsUtil.sendSms(phone, SmsUtil.signName, SmsUtil.templateCode, "{\"code\":\"" + code + "\"}", SmsUtil.ACCESS_KEY_ID, SmsUtil.ACCESS_KEY_SECRET);
+            if ("OK".equals(sendSmsResponse.getCode())) {
+                logger.info("✅ 短信发送成功!");
+            } else {
+                logger.info("❌ 短信发送失败:" + sendSmsResponse.getMessage());
+                return R.fail("短信发送失败");
+            }
+            // 方法1:转换为 JSON 字符串
+            String jsonString = objectMapper.writeValueAsString(sendSmsResponse);
+            logger.info("[SendSmsResponse] 短信发送:{}",jsonString);
+
+            String contact = phone;
+            String smsKey="RuoYi:code:sendObj";
+            String redisKey = smsKey+"_"+contact;
+            // 更新 Redis,记录本次发送的对象
+            redisUtil.set(redisKey, 000000, 60); // 设置过期时间为60秒
+
+        } catch (ServerException | JsonProcessingException e) {
+            throw new RuntimeException(e);
+        } catch (ClientException e) {
+            throw new RuntimeException(e);
+        }
+        logger.info("Sending SMS to " + phone+"----code----"+code);
+
+        return R.ok();
+    }
+
+    @Override
+    public R<Void> updateUserPhone(UserBo bo) {
+         // 验证验证码(调用验证码校验接口)
+        boolean isValid = this.verifyCode(null,bo.getNewPhone(), bo.getVerifyCode());
+        if(!isValid){
+            return R.fail("验证码错误!");
+        }
+
+        //查看对应手机号是否已经存在了
+        UserVo userVo = baseMapper.selUserPhoneExit(bo.getNewPhone());
+        if(userVo!=null){
+            return R.fail("手机号码已被占用!");
+        }
+
+        User update = new User();
+        update.setId(bo.getId());
+        update.setPhone(bo.getNewPhone());
+        baseMapper.updateUserById(update);
+
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("channelType", "login_out");
+        jsonObject.put("value", bo.getId());
+        jsonObject.put("phone", bo.getPhone());
+
+        redisUtil.publish("RuoYi:admin", jsonObject);
+
+        return R.ok();
+    }
+
+    public boolean verifyCode(String email, String phone, String inputCode) {
+        String target = (email != null ? email : phone);
+        String storedCode = codeStore.get(target);
+        Long expiry = expireTime.get(target);
+
+        // 验证码是否存在、是否正确以及是否未过期
+        return storedCode != null && storedCode.equals(inputCode) && expiry != null && expiry > System.currentTimeMillis();
+    }
+
+
+    private String generateVerificationCode() {
+        return String.format("%06d", ThreadLocalRandom.current().nextInt(1000000));
+    }
+
     // 参数校验
     private void validateParams(ItemsUserBo bo) {
         if (bo == null || bo.getUserId() == null || bo.getItemId() == null || bo.getQuantity() == null) {

+ 136 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/business/utils/SmsUtil.java

@@ -0,0 +1,136 @@
+package org.dromara.business.utils;
+
+
+
+import com.aliyuncs.DefaultAcsClient;
+import com.aliyuncs.IAcsClient;
+import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
+import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
+import com.aliyuncs.exceptions.ClientException;
+import com.aliyuncs.http.MethodType;
+import com.aliyuncs.profile.DefaultProfile;
+
+import java.rmi.ServerException;
+
+/**
+ * 阿里云短信服务工具类
+ */
+public class SmsUtil {
+
+    // 替换为您的 AccessKey ID 和 Secret
+    public static final String ACCESS_KEY_ID = "LTAI5tFNAZPUC9Uhxm1r3siG";
+    public static final String ACCESS_KEY_SECRET = "8ekYpD4ArqjCzdo6NVFfou7diCC4oV";
+
+    public static final String signName = "湖南湘州体育文化产业发展";
+
+    public static final String templateCode = "SMS_324565378";
+
+    // 短信服务区域(固定为 cn-hangzhou)
+    private static final String REGION_ID = "cn-hangzhou";
+
+    /**
+     * 发送短信的公共方法
+     *
+     * @param phoneNumber     接收短信的手机号(如:13800138000)
+     * @param signName        短信签名(需审核通过)
+     * @param templateCode    短信模板CODE(需审核通过)
+     * @param templateParam   模板参数,JSON格式字符串(如:{"code":"123456"})
+     * @return SendSmsResponse 返回响应对象,包含 requestId、code、message 等
+     * @throws ClientException 客户端异常(权限、参数错误等)
+     * @throws ServerException 服务端异常(系统错误)
+     */
+    public static SendSmsResponse sendSms(
+            String phoneNumber,
+            String signName,
+            String templateCode,
+            String templateParam,String accessKeyId,String accessKeySecret)
+            throws ServerException, ClientException, com.aliyuncs.exceptions.ClientException {
+
+        // 初始化客户端
+        DefaultProfile profile = DefaultProfile.getProfile(REGION_ID, accessKeyId, accessKeySecret);
+        IAcsClient client = new DefaultAcsClient(profile);
+
+        // 创建请求
+        SendSmsRequest request = new SendSmsRequest();
+        request.setPhoneNumbers(phoneNumber);     // 手机号
+        request.setSignName(signName);            // 签名
+        request.setTemplateCode(templateCode);    // 模板CODE
+        request.setTemplateParam(templateParam);  // 模板参数
+        request.setMethod(com.aliyuncs.http.MethodType.POST);
+
+        // 发送并返回响应
+        return client.getAcsResponse(request);
+    }
+
+
+    // ===================== 使用示例 ===================== 18684779106 联通
+    public static void main(String[] args) {
+        try {
+            SendSmsResponse response = sendSms(
+                    "19073115769",                           // 手机号
+                    "湖南湘州体育文化产业发展",                // 签名
+                    "SMS_324565378",                         // 模板CODE
+                    "{\"code\":\"123456\"}"                  // 模板参数
+                    ,ACCESS_KEY_ID,ACCESS_KEY_SECRET
+            );
+
+            // 输出结果
+            System.out.println("RequestId: " + response.getRequestId());
+            System.out.println("Code: " + response.getCode());
+            System.out.println("Message: " + response.getMessage());
+            System.out.println("BizId: " + response.getBizId());
+
+            if ("OK".equals(response.getCode())) {
+                System.out.println("✅ 短信发送成功!");
+            } else {
+                System.out.println("❌ 短信发送失败:" + response.getMessage());
+            }
+
+        } catch (Exception e) {
+            System.out.println("短信发送异常:");
+            e.printStackTrace();
+        }
+    }
+
+
+    public static void main1(String[] args) {
+        // 初始化客户端
+        DefaultProfile profile = DefaultProfile.getProfile(REGION_ID, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
+        IAcsClient client = new DefaultAcsClient(profile);
+
+        // 创建发送短信请求
+        SendSmsRequest request = new SendSmsRequest();
+
+        // 设置请求参数
+        request.setMethod(MethodType.POST); // 推荐使用 POST
+        request.setPhoneNumbers("19073115769"); // 接收短信的手机号,多个号码用英文逗号分隔
+        request.setSignName("湖南湘州体育文化产业发展");     // 已审核通过的短信签名
+        request.setTemplateCode("SMS_324565378"); // 已审核通过的短信模板 CODE
+        request.setTemplateParam("{\"code\":\"123456\"}"); // 模板变量,JSON 格式字符串
+
+        try {
+            // 发送请求并获取响应
+            SendSmsResponse response = client.getAcsResponse(request);
+
+            // 输出响应结果
+            System.out.println("RequestId: " + response.getRequestId());
+            System.out.println("Code: " + response.getCode());
+            System.out.println("Message: " + response.getMessage());
+            System.out.println("BizId: " + response.getBizId());
+
+            // 判断发送是否成功
+            if ("OK".equals(response.getCode())) {
+                System.out.println("✅ 短信发送成功!");
+            } else {
+                System.out.println("❌ 短信发送失败:" + response.getMessage());
+            }
+
+        } catch (Exception e) {
+            System.out.println("服务端错误:");
+            e.printStackTrace();
+        }
+    }
+
+
+
+}

+ 36 - 0
ruoyi-modules/ruoyi-system/src/main/resources/mapper/business/UserMapper.xml

@@ -264,4 +264,40 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             )
         and a.del_flag=0 limit 1
     </select>
+
+    <select id="selUserPhoneExit" resultType="org.dromara.business.domain.vo.UserVo">
+         SELECT
+            a.id,
+            a.login_name,
+            a.login_pass,
+            a.phone,
+            a.create_at,
+            a.update_at,
+            a.sex,
+            a.email,
+            a.nick_name,
+            a.remark,
+            a.captcha,
+            a.avatar,
+            a.province,
+            a.city,
+            a.area,
+            a.place_detail,
+            a.register_ip,
+            a.register_device,
+            a.status,
+
+            CASE
+            WHEN a.status = 0 THEN '禁用'
+            WHEN a.status = 1 THEN '启用'
+            ELSE ''
+            END AS statusText,
+
+            a.is_locked,
+            a.last_login_time,
+            a.last_login_ip,
+            a.del_flag
+            FROM user a  where a.phone=#{phone} and a.del_flag=0
+    </select>
+
 </mapper>