Browse Source

修改:<修改登陆流程,添加登陆过滤器,限流拦截器>

张旭锋 3 days ago
parent
commit
dff1ccee1a
50 changed files with 1376 additions and 250 deletions
  1. 2 0
      src/main/java/com/jtzx/crm/JtzxCrmSysApplication.java
  2. 48 28
      src/main/java/com/jtzx/crm/aspect/SysLogAspect.java
  3. 52 0
      src/main/java/com/jtzx/crm/aspect/extension/SysLogAspectExtensionForSmsCodeLogin.java
  4. 55 0
      src/main/java/com/jtzx/crm/aspect/extension/SysLogAspectExtensionForSysUserAccountLogin.java
  5. 0 24
      src/main/java/com/jtzx/crm/aspect/extension/SysLogAspectExtensionForSysUserLogin.java
  6. 49 0
      src/main/java/com/jtzx/crm/aspect/extension/SysLogAspectExtensionForSysUserSmsCodeLogin.java
  7. 5 0
      src/main/java/com/jtzx/crm/module/api/constant/ApiConstant.java
  8. 35 0
      src/main/java/com/jtzx/crm/module/api/constant/LimitConstant.java
  9. 40 0
      src/main/java/com/jtzx/crm/module/api/constant/RedisConstant.java
  10. 43 0
      src/main/java/com/jtzx/crm/module/api/model/response/LoginRespDataVO.java
  11. 98 0
      src/main/java/com/jtzx/crm/module/api/ratelimit/AccountRateLimitStrategy.java
  12. 95 0
      src/main/java/com/jtzx/crm/module/api/ratelimit/CustomRedisBasedRateLimitStrategy.java
  13. 88 0
      src/main/java/com/jtzx/crm/module/api/ratelimit/SmsCodeLoginRateLimitStrategy.java
  14. 86 0
      src/main/java/com/jtzx/crm/module/api/ratelimit/SmsCodeRateLimitStrategy.java
  15. 87 0
      src/main/java/com/jtzx/crm/module/api/util/ReqIpUtil.java
  16. 203 19
      src/main/java/com/jtzx/crm/module/api/web/auth/AdminAuthController.java
  17. 0 61
      src/main/java/com/jtzx/crm/module/api/web/oss/AdminOssFileInfoController.java
  18. 15 16
      src/main/java/com/jtzx/crm/module/api/web/oss/AdminOssUploadDownloadController.java
  19. 70 0
      src/main/java/com/jtzx/crm/module/api/web/sms/SmsController.java
  20. 1 1
      src/main/java/com/jtzx/crm/module/oss/enums/UploadFileCheckResultEnum.java
  21. 1 2
      src/main/java/com/jtzx/crm/module/oss/model/request/UploadFileAttributeDTO.java
  22. 38 0
      src/main/java/com/jtzx/crm/module/sms/facade/SmsFacade.java
  23. 35 0
      src/main/java/com/jtzx/crm/module/sms/facade/impl/SmsFacadeImpl.java
  24. 44 0
      src/main/java/com/jtzx/crm/module/sms/service/SmsService.java
  25. 4 0
      src/main/java/com/jtzx/crm/module/sys/constant/SysConstant.java
  26. 2 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysDataDictClassifiedEntity.java
  27. 2 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysDataDictItemEntity.java
  28. 2 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysDeptEntity.java
  29. 6 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysLogEntity.java
  30. 2 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysMenuEntity.java
  31. 2 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysParamEntity.java
  32. 15 6
      src/main/java/com/jtzx/crm/module/sys/entity/SysRoleEntity.java
  33. 2 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysRoleMenuRelationEntity.java
  34. 2 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysTenantEntity.java
  35. 2 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysUserDeptRelationEntity.java
  36. 31 3
      src/main/java/com/jtzx/crm/module/sys/entity/SysUserEntity.java
  37. 10 0
      src/main/java/com/jtzx/crm/module/sys/entity/SysUserRoleRelationEntity.java
  38. 1 1
      src/main/java/com/jtzx/crm/module/sys/mapper/xml/SysUserMapper.xml
  39. 5 11
      src/main/java/com/jtzx/crm/module/sys/model/interior/UserRoleContainer.java
  40. 2 0
      src/main/java/com/jtzx/crm/module/sys/model/request/AdminInsertSysLogDTO.java
  41. 3 5
      src/main/java/com/jtzx/crm/module/sys/model/request/SysUserLoginDTO.java
  42. 7 3
      src/main/java/com/jtzx/crm/module/sys/model/response/SysUserLoginBO.java
  43. 5 5
      src/main/java/com/jtzx/crm/module/sys/service/SysDeptService.java
  44. 1 1
      src/main/java/com/jtzx/crm/module/sys/service/SysMenuService.java
  45. 39 28
      src/main/java/com/jtzx/crm/module/sys/service/SysRoleService.java
  46. 26 27
      src/main/java/com/jtzx/crm/module/sys/service/SysUserService.java
  47. 2 1
      src/main/resources/application-dev.yml
  48. 1 0
      src/main/resources/application-prod.yml
  49. 2 1
      src/main/resources/application-test.yml
  50. 10 7
      src/main/resources/application.yml

+ 2 - 0
src/main/java/com/jtzx/crm/JtzxCrmSysApplication.java

@@ -1,6 +1,7 @@
 package com.jtzx.crm;
 
 import cc.uncarbon.framework.crud.annotation.EnableInitHikariPoolAtStartup;
+import org.dromara.x.file.storage.spring.EnableFileStorage;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@@ -10,6 +11,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
  */
 @EnableInitHikariPoolAtStartup
 @SpringBootApplication
+@EnableFileStorage
 public class JtzxCrmSysApplication {
     public static void main(String[] args) {
         SpringApplication.run(JtzxCrmSysApplication.class, args);

+ 48 - 28
src/main/java/com/jtzx/crm/aspect/SysLogAspect.java

@@ -2,14 +2,6 @@ package com.jtzx.crm.aspect;
 
 import cc.uncarbon.framework.core.context.UserContext;
 import cc.uncarbon.framework.core.context.UserContextHolder;
-import cc.uncarbon.framework.web.util.IPUtil;
-import com.jtzx.crm.module.sys.annotation.SysLog;
-import com.jtzx.crm.module.sys.enums.SysLogStatusEnum;
-import com.jtzx.crm.module.sys.extension.SysLogAspectExtension;
-import com.jtzx.crm.module.sys.extension.impl.DefaultSysLogAspectExtension;
-import com.jtzx.crm.module.sys.model.request.AdminInsertSysLogDTO;
-import com.jtzx.crm.module.sys.model.response.IPLocationBO;
-import com.jtzx.crm.module.sys.service.SysLogService;
 import cn.dev33.satoken.spring.SpringMVCUtil;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.bean.copier.CopyOptions;
@@ -23,6 +15,14 @@ import cn.hutool.core.util.ReflectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONConfig;
 import cn.hutool.json.JSONUtil;
+import com.jtzx.crm.module.api.util.ReqIpUtil;
+import com.jtzx.crm.module.sys.annotation.SysLog;
+import com.jtzx.crm.module.sys.enums.SysLogStatusEnum;
+import com.jtzx.crm.module.sys.extension.SysLogAspectExtension;
+import com.jtzx.crm.module.sys.extension.impl.DefaultSysLogAspectExtension;
+import com.jtzx.crm.module.sys.model.request.AdminInsertSysLogDTO;
+import com.jtzx.crm.module.sys.model.response.IPLocationBO;
+import com.jtzx.crm.module.sys.service.SysLogService;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
@@ -31,6 +31,7 @@ import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.AfterThrowing;
 import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
 import org.springframework.http.HttpHeaders;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Component;
@@ -90,21 +91,22 @@ public class SysLogAspect {
     /**
      * 顺利运行
      */
-    @AfterReturning(pointcut = "@annotation(annotation)", returning = "ret")
-    public void returning(JoinPoint joinPoint, SysLog annotation, Object ret) {
-        if (!ArrayUtil.contains(annotation.when(), SysLog.When.SUCCESS)) {
-            // 系统日志保存时机不包含“成功时”
+    @AfterReturning(pointcut = "@annotation(com.jtzx.crm.module.sys.annotation.SysLog)", returning = "ret")
+    public void returning(JoinPoint joinPoint, Object ret) {
+        // 1. 手动在方法体内部抓取注解对象
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        SysLog annotation = signature.getMethod().getAnnotation(SysLog.class);
+
+        if (annotation == null || !ArrayUtil.contains(annotation.when(), SysLog.When.SUCCESS)) {
             return;
         }
-
         HttpServletRequest request = SpringMVCUtil.getRequest();
         AspectContext aspectContext = new AspectContext(request);
 
+        // 2. 返回值直接传 null
         if (annotation.syncSave()) {
-            // 同步保存
             saveSysLog(joinPoint, annotation, aspectContext, null, ret);
         } else {
-            // 异步保存
             saveSysLogAsync(joinPoint, annotation, aspectContext, null, ret);
         }
     }
@@ -112,12 +114,14 @@ public class SysLogAspect {
     /**
      * 出现异常
      */
-    @AfterThrowing(value = "@annotation(annotation)", throwing = "e")
-    public void throwing(JoinPoint joinPoint, SysLog annotation, Throwable e) {
-        if (!ArrayUtil.contains(annotation.when(), SysLog.When.FAILED)) {
-            // 系统日志保存时机不包含“失败时”
-            return;
-        }
+    @AfterThrowing(pointcut = "@annotation(com.jtzx.crm.module.sys.annotation.SysLog)", throwing = "e")
+    public void throwing(JoinPoint joinPoint, Throwable e) {
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        SysLog annotation = signature.getMethod().getAnnotation(SysLog.class);
+//        if (!ArrayUtil.contains(annotation.when(), SysLog.When.FAILED)) {
+//            // 系统日志保存时机不包含“失败时”
+//            return;
+//        }
 
         HttpServletRequest request = SpringMVCUtil.getRequest();
         AspectContext aspectContext = new AspectContext(request);
@@ -217,18 +221,34 @@ public class SysLogAspect {
          */
         AspectContext(HttpServletRequest request) {
             this.userContext = UserContextHolder.getUserContext();
-            //记录IP地址
-            String ip = UserContextHolder.getClientIP();
-            if (CharSequenceUtil.isEmpty(ip)) {
-                // 兜底处理,直接从request中拿
-                ip = IPUtil.getClientIPAddress(request, 0);
-            }
-            this.clientIP = ip;
+            this.clientIP = ReqIpUtil.getIpAddr(request);
             // Spring框架已经做了简单的防注入过滤
             this.userAgent = request.getHeader(HttpHeaders.USER_AGENT);
         }
     }
 
+    /**
+     * 辅助方法:判断 IP 是否为回环地址或局域网内网地址
+     */
+    private static boolean isLoopbackOrInternal(String ip) {
+        if (ip == null) return true;
+        String cleanIp = ip.trim();
+        if ("127.0.0.1".equals(cleanIp) || "0:0:0:0:0:0:0:1".equals(cleanIp) || "localhost".equalsIgnoreCase(cleanIp)) {
+            return true;
+        }
+        // 匹配内网 A/B/C 类私有地址段:10.x.x.x, 192.168.x.x, 172.16.x.x - 172.31.x.x
+        return cleanIp.startsWith("10.") || cleanIp.startsWith("192.168.") ||
+                (cleanIp.startsWith("172.") && cleanIp.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"));
+    }
+
+    // 辅助方法:判断是否为内网 IP(10.x, 172.16.x-172.31.x, 192.168.x)
+    private static boolean isInternalIp(String ip) {
+        if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) return true;
+        // 简单正则匹配常见的私有内网 IP 段
+        return ip.startsWith("10.") || ip.startsWith("192.168.") ||
+                (ip.startsWith("172.") && ip.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"));
+    }
+
     /**
      * 异步保存系统日志
      */

+ 52 - 0
src/main/java/com/jtzx/crm/aspect/extension/SysLogAspectExtensionForSmsCodeLogin.java

@@ -0,0 +1,52 @@
+package com.jtzx.crm.aspect.extension;
+
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.json.JSONUtil;
+import com.google.common.collect.Maps;
+import com.jtzx.crm.module.sys.annotation.SysLog;
+import com.jtzx.crm.module.sys.enums.SysLogStatusEnum;
+import com.jtzx.crm.module.sys.extension.impl.DefaultSysLogAspectExtension;
+import com.jtzx.crm.module.sys.model.request.AdminInsertSysLogDTO;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * SysLog 切面实现类扩展 for 登录后台用户
+ */
+@NoArgsConstructor
+@Slf4j
+public class SysLogAspectExtensionForSmsCodeLogin extends DefaultSysLogAspectExtension {
+
+    @Override
+    public void beforeSaving(AdminInsertSysLogDTO insertSysLogDTO, JoinPoint joinPoint, SysLog annotation, Throwable e, Object ret) {
+        log.info("短信发送拦截器执行");
+        String username = null;
+        Map<String, Object> map = Maps.newHashMap();
+        AtomicInteger code = new AtomicInteger(0);
+        for (Object arg : joinPoint.getArgs()) {
+            code.getAndIncrement();
+            if(code.get() == 1){
+                username = (String) arg;
+            }
+            map.put("param" + code.get(), arg);
+        }
+        insertSysLogDTO.setUsername(username);
+        insertSysLogDTO.setLogType(1);
+        if (CollUtil.isNotEmpty(map)) {
+            insertSysLogDTO.setParams(JSONUtil.toJsonStr(map));
+        }
+        if (ret != null) {
+            if (ret instanceof ApiResult apiResult) {
+                insertSysLogDTO.setStatus(SysLogStatusEnum.SUCCESS);
+                if (apiResult.getCode() != 200) {
+                    insertSysLogDTO.setStatus(SysLogStatusEnum.FAILED);
+                }
+            }
+        }
+    }
+}

+ 55 - 0
src/main/java/com/jtzx/crm/aspect/extension/SysLogAspectExtensionForSysUserAccountLogin.java

@@ -0,0 +1,55 @@
+package com.jtzx.crm.aspect.extension;
+
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.json.JSONUtil;
+import com.google.common.collect.Maps;
+import com.jtzx.crm.module.sys.annotation.SysLog;
+import com.jtzx.crm.module.sys.enums.SysLogStatusEnum;
+import com.jtzx.crm.module.sys.extension.impl.DefaultSysLogAspectExtension;
+import com.jtzx.crm.module.sys.model.request.AdminInsertSysLogDTO;
+import com.jtzx.crm.module.sys.model.request.SysUserLoginDTO;
+import lombok.NoArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * SysLog 切面实现类扩展 for 登录后台用户
+ */
+@NoArgsConstructor
+public class SysLogAspectExtensionForSysUserAccountLogin extends DefaultSysLogAspectExtension {
+
+    @Override
+    public void beforeSaving(AdminInsertSysLogDTO insertSysLogDTO, JoinPoint joinPoint, SysLog annotation, Throwable e, Object ret) {
+        String username = null;
+        Map<String, Object> map = Maps.newHashMap();
+        AtomicInteger code = new AtomicInteger(0);
+        for (Object arg : joinPoint.getArgs()) {
+            if (arg instanceof SysUserLoginDTO dto) {
+                username = dto.getUsername();
+
+            } else {
+                code.getAndIncrement();
+                if(code.get() == 1){
+                    username = (String) arg;
+                }
+                map.put("param" + code.get(), arg);
+            }
+        }
+        insertSysLogDTO.setUsername(username);
+        insertSysLogDTO.setLogType(0);
+        if (CollUtil.isNotEmpty(map)) {
+            insertSysLogDTO.setParams(JSONUtil.toJsonStr(map));
+        }
+        if (ret != null) {
+            if (ret instanceof ApiResult apiResult) {
+                insertSysLogDTO.setStatus(SysLogStatusEnum.SUCCESS);
+                if (apiResult.getCode() != 200) {
+                    insertSysLogDTO.setStatus(SysLogStatusEnum.FAILED);
+                }
+            }
+        }
+    }
+}

+ 0 - 24
src/main/java/com/jtzx/crm/aspect/extension/SysLogAspectExtensionForSysUserLogin.java

@@ -1,24 +0,0 @@
-package com.jtzx.crm.aspect.extension;
-
-import com.jtzx.crm.module.sys.annotation.SysLog;
-import com.jtzx.crm.module.sys.extension.impl.DefaultSysLogAspectExtension;
-import com.jtzx.crm.module.sys.model.request.AdminInsertSysLogDTO;
-import com.jtzx.crm.module.sys.model.request.SysUserLoginDTO;
-import lombok.NoArgsConstructor;
-import org.aspectj.lang.JoinPoint;
-
-/**
- * SysLog 切面实现类扩展 for 登录后台用户
- */
-@NoArgsConstructor
-public class SysLogAspectExtensionForSysUserLogin extends DefaultSysLogAspectExtension {
-
-    @Override
-    public void beforeSaving(AdminInsertSysLogDTO insertSysLogDTO, JoinPoint joinPoint, SysLog annotation, Throwable e, Object ret) {
-        for (Object arg : joinPoint.getArgs()) {
-            if (arg instanceof SysUserLoginDTO dto) {
-                insertSysLogDTO.setUsername(dto.getUsername());
-            }
-        }
-    }
-}

+ 49 - 0
src/main/java/com/jtzx/crm/aspect/extension/SysLogAspectExtensionForSysUserSmsCodeLogin.java

@@ -0,0 +1,49 @@
+package com.jtzx.crm.aspect.extension;
+
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.json.JSONUtil;
+import com.google.common.collect.Maps;
+import com.jtzx.crm.module.sys.annotation.SysLog;
+import com.jtzx.crm.module.sys.enums.SysLogStatusEnum;
+import com.jtzx.crm.module.sys.extension.impl.DefaultSysLogAspectExtension;
+import com.jtzx.crm.module.sys.model.request.AdminInsertSysLogDTO;
+import lombok.NoArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * SysLog 切面实现类扩展 for 登录后台用户
+ */
+@NoArgsConstructor
+public class SysLogAspectExtensionForSysUserSmsCodeLogin extends DefaultSysLogAspectExtension {
+
+    @Override
+    public void beforeSaving(AdminInsertSysLogDTO insertSysLogDTO, JoinPoint joinPoint, SysLog annotation, Throwable e, Object ret) {
+        String username = null;
+        Map<String, Object> map = Maps.newHashMap();
+        AtomicInteger code = new AtomicInteger(0);
+        for (Object arg : joinPoint.getArgs()) {
+            code.getAndIncrement();
+            if(code.get() == 1){
+                username = (String) arg;
+            }
+            map.put("param" + code.get(), arg);
+        }
+        insertSysLogDTO.setUsername(username);
+        insertSysLogDTO.setLogType(2);
+        if (CollUtil.isNotEmpty(map)) {
+            insertSysLogDTO.setParams(JSONUtil.toJsonStr(map));
+        }
+        if (ret != null) {
+            if (ret instanceof ApiResult apiResult) {
+                insertSysLogDTO.setStatus(SysLogStatusEnum.SUCCESS);
+                if (apiResult.getCode() != 200) {
+                    insertSysLogDTO.setStatus(SysLogStatusEnum.FAILED);
+                }
+            }
+        }
+    }
+}

+ 5 - 0
src/main/java/com/jtzx/crm/module/api/constant/ApiConstant.java

@@ -13,4 +13,9 @@ public final class ApiConstant {
      */
     public static final String HTTP_API_URL_PREFIX = "/admin";
 
+    /**
+     * 账号密码登陆之后用户信息在redis中保存的时长
+     */
+    public static final Integer LOGIN_USER_INFO = 1800;
+
 }

+ 35 - 0
src/main/java/com/jtzx/crm/module/api/constant/LimitConstant.java

@@ -0,0 +1,35 @@
+package com.jtzx.crm.module.api.constant;
+
+
+/**
+ * 后台管理接口常量
+ */
+public final class LimitConstant {
+    private LimitConstant() {
+    }
+
+    //登陆封禁时长
+    public static final Integer login_duration = 300;
+    //对应的提示信息
+    public static final String  login_Str = "5分钟";
+    //登陆的最大次数
+    public static final Integer login_max = 5;
+    //登陆失败多少次之后需要图形验证码 login_fail_count 不能大于 login_max,login_fail_count = 0 的话,就是永远不需要图形验证码
+    public static final Integer login_fail_count=3;
+
+
+    //短信封禁时长
+    public static final Integer smsCode_duration = 300;
+    //对应的提示信息
+    public static final String  smsCode_Str = "5分钟";
+    //短信的最大次数
+    public static final Integer smsCode_max = 3;
+
+
+    //短信登录封禁时长
+    public static final Integer smsCode_login_duration = 600;
+    //对应的提示信息
+    public static final String  smsCode_login_Str = "10分钟";
+    //短信登录的最大次数
+    public static final Integer smsCode_login_max = 3;
+}

+ 40 - 0
src/main/java/com/jtzx/crm/module/api/constant/RedisConstant.java

@@ -0,0 +1,40 @@
+package com.jtzx.crm.module.api.constant;
+
+
+/**
+ * 后台管理接口常量
+ */
+public final class RedisConstant {
+    private RedisConstant() {
+    }
+
+
+    public static final String SMS_CODE_REDIS_KEY_PREFIX = "smsCodeRateLimit:";
+    public static final String ACCOUNT_REDIS_KEY_PREFIX = "AccountRateLimit:";
+
+
+
+    public static final String SMS_CODE_PREFIX = "sms-code:";
+    public static final String LOGIN_USER_INFO_PREFIX = "userInfo:";
+
+    /**
+     * 短信验证码的长度
+     */
+    public static final Integer SMS_CODE_LENGTH = 6;
+
+    /**
+     * 短信的有效期,120秒-2分钟
+     */
+    public static final Integer SMS_CODE_DURATION = 120;//秒
+
+
+
+    //账号登陆封禁后的结束时间
+    public static final String ACCOUNT_LOGIN_FAILED_END_DATETIME_PREFIX = "accountLoginFailedEndDatetime:";
+
+    //发登陆短信封禁后结束时间
+    public static final String SMS_CODE_FAILED_END_DATETIME_PREFIX = "smsCodeFailedEndDatetime:";
+    //短信验证码封禁后结束时间
+    public static final String SMS_CODE_LOGIN_FAILED_END_DATETIME_PREFIX = "smsCodeLoginFailedEndDatetime:";
+
+}

+ 43 - 0
src/main/java/com/jtzx/crm/module/api/model/response/LoginRespDataVO.java

@@ -0,0 +1,43 @@
+package com.jtzx.crm.module.api.model.response;
+
+import com.jtzx.crm.module.sys.model.response.SysUserLoginVO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+import java.io.Serializable;
+
+@Accessors(chain = true)
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class LoginRespDataVO implements Serializable {
+
+    @Schema(description = "手机号")
+    private String mobile;
+
+    @Schema(description = "提交次数")
+    private Integer commitCount;
+
+    @Schema(description = "最多提交次数")
+    private Integer commitTopCount;
+
+    @Schema(description = "封禁结束时间")
+    private String endDateTime;
+
+    @Schema(description = "是否需要显示图形验证码,false-不,true-是")
+    private Boolean showPicCode;
+
+    @Schema(description = "图形验证码数据")
+    AdminCaptchaVO captchaVO;
+
+    @Schema(description = "token数据")
+    SysUserLoginVO tokenInfo;
+
+    @Schema(description = "登陆状态,true-通过,false-未通过")
+    public Boolean status;
+}

+ 98 - 0
src/main/java/com/jtzx/crm/module/api/ratelimit/AccountRateLimitStrategy.java

@@ -0,0 +1,98 @@
+package com.jtzx.crm.module.api.ratelimit;
+
+import cc.uncarbon.framework.core.exception.RateLimitedException;
+import cc.uncarbon.framework.ratelimit.annotation.UseRateLimit;
+import cc.uncarbon.framework.ratelimit.constant.RateLimitConstant;
+import cc.uncarbon.framework.ratelimit.stratrgy.RateLimitStrategy;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUnit;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import com.jtzx.crm.module.api.constant.LimitConstant;
+import com.jtzx.crm.module.api.constant.RedisConstant;
+import com.jtzx.crm.module.sys.model.request.SysUserLoginDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+
+/**
+ * 这个策略用于发送短信验证码限流策略,ip+手机号一起限制
+ */
+@Slf4j
+@Component
+public class AccountRateLimitStrategy extends CustomRedisBasedRateLimitStrategy implements RateLimitStrategy {
+
+    private final RedisTemplate<String, Object> stringSetRedisTemplate;
+
+    public AccountRateLimitStrategy(RedisTemplate<String, Object> objectRedisTemplate) {
+        super(objectRedisTemplate, "AccountRateLimitStrategy");
+        this.stringSetRedisTemplate = objectRedisTemplate;
+    }
+
+
+    public RateLimitedException rateLimitedExceptionSupplier(String dateTimeStr) {
+//        return new RateLimitedException(429,"发送次数已超"+ LimitConstant.login_max +"次限制,请"+LimitConstant.login_Str+"之后再试!");
+        return new RateLimitedException(429, "操作频率过快,已超" + LimitConstant.login_max + "次限制,请在" + dateTimeStr + "之后再试!");
+    }
+
+    @Override
+    public void performRateLimitCheck(UseRateLimit annotation, JoinPoint point) {
+        String username = currentClientAccount(point);
+        Object object = stringSetRedisTemplate.opsForValue()
+                .get(RedisConstant.ACCOUNT_LOGIN_FAILED_END_DATETIME_PREFIX + username);
+        Integer customDruation = null;
+        String dateTimeStr;
+        if (object != null) {
+            dateTimeStr = (String) object;
+            customDruation = Math.toIntExact(DateUtil.between(DateUtil.date(), DateUtil.parseDate(dateTimeStr), DateUnit.SECOND));
+        } else {
+            //计算结束时间
+            DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.login_duration);
+            dateTimeStr = DateUtil.formatDateTime(dateTime);
+        }
+        super.performRateLimitCheck(annotation, point, customDruation, this.rateLimitedExceptionSupplier(dateTimeStr));
+    }
+
+
+    @Override
+    protected String determineRedisKey(UseRateLimit annotation, JoinPoint point) {
+        String key = CharSequenceUtil.format("{}account:{}:{}", RedisConstant.ACCOUNT_REDIS_KEY_PREFIX, currentClientAccount(point), determineMark(annotation, point));
+        log.info("accountRedisKey--------->{}", key);
+        return key;
+    }
+
+
+    protected String currentClientAccount(JoinPoint point) {
+        try {
+            // 从 AOP 的 JoinPoint 中直接获取接口的入参列表
+            Object[] args = point.getArgs();
+            if (args != null) {
+                for (Object arg : args) {
+                    // 精准匹配你的登录 DTO 对象的类名
+                    if (arg != null && arg.getClass().getName().contains("SysUserLoginDTO")) {
+                        // 方案 A:如果引入了该 DTO 的包,可以直接强转
+                        SysUserLoginDTO dto = (SysUserLoginDTO) arg;
+                        return dto.getUsername();
+
+                        // 方案 B:为了绝对防止包引用和编译报错,用 Jackson 将其无缝转成 JsonNode 读取
+//                        ObjectMapper objectMapper = new ObjectMapper();
+//                        JsonNode jsonNode = objectMapper.valueToTree(arg);
+//                        if (jsonNode != null && jsonNode.has("username")) {
+//                            String username = jsonNode.get("username").asText();
+//                            if (CharSequenceUtil.isNotBlank(username)) {
+//                                return username;
+//                            }
+//                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("限流策略从 JoinPoint 提取用户名失败: {}", e.getMessage(), e);
+        }
+
+        // 终极兜底:如果没拿到用户名(比如其他接口也配了这个策略但没传 username),返回默认值防止限流功能崩溃
+        return RateLimitConstant.CLIENT_IP_UNKNOWN;
+    }
+}

+ 95 - 0
src/main/java/com/jtzx/crm/module/api/ratelimit/CustomRedisBasedRateLimitStrategy.java

@@ -0,0 +1,95 @@
+package com.jtzx.crm.module.api.ratelimit;
+
+import cc.uncarbon.framework.core.exception.RateLimitStrategyException;
+import cc.uncarbon.framework.core.exception.RateLimitedException;
+import cc.uncarbon.framework.ratelimit.annotation.UseRateLimit;
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.text.StrPool;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 脚手架预置的简易限流策略基类
+ */
+@RequiredArgsConstructor
+@Slf4j
+public abstract class CustomRedisBasedRateLimitStrategy {
+
+    private final RedisTemplate<String, Object> objectRedisTemplate;
+    private final String realClassName;
+
+    /**
+     * 预置限流Lua脚本
+     */
+    private static final String LUA_SCRIPT = """
+            local key = KEYS[1]
+            local count = tonumber(ARGV[1])
+            local time = tonumber(ARGV[2])
+            local current = redis.call('get', key)
+            
+            if current then
+                current = tonumber(current)
+                if current > count then
+                    return current
+                end
+                current = redis.call('incr', key)
+            else
+                current = 1
+                redis.call('set', key, current, 'EX', time)
+            end
+            
+            return current
+            """;
+
+    /**
+     * 预置限流Lua脚本的 RedisScript 类实例
+     */
+    private static final RedisScript<Long> REDIS_SCRIPT = RedisScript.of(LUA_SCRIPT, Long.class);
+
+
+    protected void performRateLimitCheck(UseRateLimit annotation, JoinPoint point, Integer customDruation,
+                                         RateLimitedException rateLimitedExceptionSupplier)
+            throws RateLimitStrategyException {
+        int duration = annotation.duration();
+        if (customDruation != null) {
+            duration = customDruation;
+        }
+        int max = annotation.max();
+        String redisKey = determineRedisKey(annotation, point);
+        Long current = objectRedisTemplate.execute(REDIS_SCRIPT, Collections.singletonList(redisKey), max, duration);
+        //新增操作保存请求次数到redis中,这和框架中没有的
+        objectRedisTemplate.opsForValue().setIfAbsent(redisKey, current, duration, TimeUnit.SECONDS);
+        if (current > max) {
+            log.info("[UseRateLimit][{}] 键名 {} 已触达限流阈值 {}", realClassName, redisKey, max);
+            throw rateLimitedExceptionSupplier;
+        }
+        log.info("[UseRateLimit][{}] 键名 {} 限流进度 {}/{}", realClassName, redisKey, current, max);
+    }
+
+    /**
+     * 确定RedisKey
+     */
+    protected abstract String determineRedisKey(UseRateLimit annotation, JoinPoint point);
+
+    /**
+     * 确定被限流方法的标识
+     */
+    protected String determineMark(UseRateLimit annotation, JoinPoint point) {
+        if (CharSequenceUtil.isNotEmpty(annotation.mark())) {
+            return annotation.mark();
+        }
+        // 使用Java方法的全限定名
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method method = signature.getMethod();
+        Class<?> targetClass = method.getDeclaringClass();
+        return targetClass.getName() + StrPool.DASHED + method.getName();
+    }
+}

+ 88 - 0
src/main/java/com/jtzx/crm/module/api/ratelimit/SmsCodeLoginRateLimitStrategy.java

@@ -0,0 +1,88 @@
+package com.jtzx.crm.module.api.ratelimit;
+
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.core.exception.RateLimitedException;
+import cc.uncarbon.framework.ratelimit.annotation.UseRateLimit;
+import cc.uncarbon.framework.ratelimit.constant.RateLimitConstant;
+import cc.uncarbon.framework.ratelimit.stratrgy.RateLimitStrategy;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUnit;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import com.jtzx.crm.module.api.constant.LimitConstant;
+import com.jtzx.crm.module.api.constant.RedisConstant;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+
+/**
+ * 这个策略用于发送短信验证码限流策略,ip+手机号一起限制
+ */
+@Slf4j
+@Component
+public class SmsCodeLoginRateLimitStrategy extends CustomRedisBasedRateLimitStrategy implements RateLimitStrategy {
+
+    private final RedisTemplate<String, Object> stringSetRedisTemplate;
+
+    public SmsCodeLoginRateLimitStrategy(RedisTemplate<String, Object> objectRedisTemplate) {
+        super(objectRedisTemplate, "SmsCodeLoginRateLimitStrategy");
+        this.stringSetRedisTemplate=objectRedisTemplate;
+    }
+
+    public RateLimitedException rateLimitedExceptionSupplier(String dateTimeStr) {
+//        return new RateLimitedException("发送次数已超"+ LimitConstant.smsCode_login_max +"次限制,请"+LimitConstant.smsCode_login_Str+"之后再试!");
+        return new RateLimitedException("操作频率过快,已超"+ LimitConstant.smsCode_login_max +"次限制,请在"+dateTimeStr+"之后再试!");
+    }
+
+    @Override
+    public void performRateLimitCheck(UseRateLimit annotation, JoinPoint point) {
+        String mobile = currentClientMobile();
+        Object object = stringSetRedisTemplate.opsForValue()
+                .get(RedisConstant.SMS_CODE_LOGIN_FAILED_END_DATETIME_PREFIX + mobile);
+        Integer customDruation = null;
+        String dateTimeStr;
+        if (object != null) {
+            dateTimeStr = (String) object;
+            customDruation = Math.toIntExact(DateUtil.between(DateUtil.date(), DateUtil.parseDate(dateTimeStr), DateUnit.SECOND));
+        }else {
+            //计算结束时间
+            DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.login_duration);
+            dateTimeStr = DateUtil.formatDateTime(dateTime);
+        }
+        super.performRateLimitCheck(annotation, point, customDruation,this.rateLimitedExceptionSupplier(dateTimeStr));
+    }
+
+
+    @Override
+    protected String determineRedisKey(UseRateLimit annotation, JoinPoint point) {
+        String redisKey = CharSequenceUtil.format("{}smsCodeLogin:{}:{}", RedisConstant.SMS_CODE_REDIS_KEY_PREFIX, currentClientMobile(), determineMark(annotation, point));
+        log.info("redisKey--------->{}", redisKey);
+        return redisKey;
+    }
+
+
+    protected String currentClientMobile() {
+        String clientIP = UserContextHolder.getClientIP();
+        if (CharSequenceUtil.isNotEmpty(clientIP)) {
+            return clientIP;
+        }
+        try {
+            ServletRequestAttributes servletRequestAttributes =
+                    (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            if (servletRequestAttributes != null) {
+//                String smsCode = servletRequestAttributes.getRequest().getParameter("smsCode");
+//                return servletRequestAttributes.getRequest().getParameter("mobile")+"-"+smsCode;
+                return servletRequestAttributes.getRequest().getParameter("mobile");
+            }
+        } catch (Exception | NoClassDefFoundError e) {
+            // 可能没有引入 helio-starter-web 依赖,忽略
+        }
+
+        // 兜底
+        return RateLimitConstant.CLIENT_IP_UNKNOWN;
+    }
+}

+ 86 - 0
src/main/java/com/jtzx/crm/module/api/ratelimit/SmsCodeRateLimitStrategy.java

@@ -0,0 +1,86 @@
+package com.jtzx.crm.module.api.ratelimit;
+
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.core.exception.RateLimitedException;
+import cc.uncarbon.framework.ratelimit.annotation.UseRateLimit;
+import cc.uncarbon.framework.ratelimit.constant.RateLimitConstant;
+import cc.uncarbon.framework.ratelimit.stratrgy.RateLimitStrategy;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUnit;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import com.jtzx.crm.module.api.constant.LimitConstant;
+import com.jtzx.crm.module.api.constant.RedisConstant;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+
+/**
+ * 这个策略用于发送短信验证码限流策略,ip+手机号一起限制
+ */
+@Slf4j
+@Component
+public class SmsCodeRateLimitStrategy extends CustomRedisBasedRateLimitStrategy implements RateLimitStrategy {
+    private final RedisTemplate<String, Object> stringSetRedisTemplate;
+
+    public SmsCodeRateLimitStrategy(RedisTemplate<String, Object> objectRedisTemplate) {
+        super(objectRedisTemplate, "SmsCodeRateLimitStrategy");
+        this.stringSetRedisTemplate = objectRedisTemplate;
+    }
+
+    public RateLimitedException rateLimitedExceptionSupplier(String dateTimeStr) {
+//        return new RateLimitedException("发送次数已超"+ LimitConstant.smsCode_max +"次限制,请"+LimitConstant.smsCode_Str+"之后再试!");
+        return new RateLimitedException("操作频率过快,已超" + LimitConstant.smsCode_max + "次限制,请在" + dateTimeStr + "之后再试!");
+    }
+
+    @Override
+    public void performRateLimitCheck(UseRateLimit annotation, JoinPoint point) {
+        String mobile = currentClientMobile();
+        Object object = stringSetRedisTemplate.opsForValue()
+                .get(RedisConstant.SMS_CODE_FAILED_END_DATETIME_PREFIX + mobile);
+        Integer customDruation = null;
+        String dateTimeStr;
+        if (object != null) {
+            dateTimeStr = (String) object;
+            customDruation = Math.toIntExact(DateUtil.between(DateUtil.date(), DateUtil.parseDate(dateTimeStr), DateUnit.SECOND));
+        } else {
+            //计算结束时间
+            DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.login_duration);
+            dateTimeStr = DateUtil.formatDateTime(dateTime);
+        }
+        super.performRateLimitCheck(annotation, point, customDruation, this.rateLimitedExceptionSupplier(dateTimeStr));
+    }
+
+
+    @Override
+    protected String determineRedisKey(UseRateLimit annotation, JoinPoint point) {
+        return CharSequenceUtil.format("{}mobile:{}:{}", RedisConstant.SMS_CODE_REDIS_KEY_PREFIX, currentClientMobile(), determineMark(annotation, point));
+    }
+
+
+    protected String currentClientMobile() {
+        String clientIP = UserContextHolder.getClientIP();
+        if (CharSequenceUtil.isNotEmpty(clientIP)) {
+            return clientIP;
+        }
+        try {
+            ServletRequestAttributes servletRequestAttributes =
+                    (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            if (servletRequestAttributes != null) {
+                // 尝试从request中得到用户的手机号
+                // 尝试从request中得到未登录用户的IP地址
+//                String ip = IPUtil.getClientIPAddress(servletRequestAttributes.getRequest(), 0);
+                return servletRequestAttributes.getRequest().getParameter("mobile");
+            }
+        } catch (Exception | NoClassDefFoundError e) {
+            // 可能没有引入 helio-starter-web 依赖,忽略
+        }
+
+        // 兜底
+        return RateLimitConstant.CLIENT_IP_UNKNOWN;
+    }
+}

+ 87 - 0
src/main/java/com/jtzx/crm/module/api/util/ReqIpUtil.java

@@ -0,0 +1,87 @@
+package com.jtzx.crm.module.api.util;
+
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.web.util.IPUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.util.List;
+
+public class ReqIpUtil {
+
+
+
+    public static String getIpAddr(HttpServletRequest request) {
+        String ip = UserContextHolder.getClientIP();
+
+        // 2. 如果为空,或者拿到的明显是本地内网回环,则用 IPUtil 兜底获取
+        if (CharSequenceUtil.isEmpty(ip) || isLoopbackOrInternal(ip)) {
+            // 先尝试通过 IPUtil 获取整个未分割的 IP 串(避免因为固定的 index=0 错失公网IP)
+            // 注意:因为工具类把 REMOTE_ADDR 排在 X-Real-IP 前面,我们先手动夹带一下核心请求头
+            ip = request.getHeader("X-Forwarded-For");
+            if (CharSequenceUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+                ip = request.getHeader("X-Real-IP");
+            }
+
+            // 如果上面两个核心头都没拿到,再让 IPUtil 去遍历它那一堆头
+            if (CharSequenceUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
+                ip = IPUtil.getClientIPAddress(request, 0);
+            }
+        }
+
+        // 3. 【核心修复】深度解析获取到的 IP,确保拿到的是公网 IP
+        if (CharSequenceUtil.isNotEmpty(ip)) {
+            // 如果包含逗号(说明是多层代理返回的 IP 链)
+            if (CharSequenceUtil.contains(ip, ",")) {
+                List<String> ipList = CharSequenceUtil.split(ip, ",");
+                // 遍历 IP 链,找到第一个真正的公网 IP
+                String publicIp = null;
+                for (String singleIp : ipList) {
+                    String cleanIp = CharSequenceUtil.cleanBlank(singleIp);
+                    if (CharSequenceUtil.isNotEmpty(cleanIp) && !isLoopbackOrInternal(cleanIp) && !"unknown".equalsIgnoreCase(cleanIp)) {
+                        publicIp = cleanIp;
+                        break; // 找到第一个外网 IP,跳出循环
+                    }
+                }
+                // 如果找到了公网 IP 就用它,否则降级取原本的第一个
+                if (publicIp != null) {
+                    ip = publicIp;
+                } else {
+                    ip = CharSequenceUtil.cleanBlank(CollUtil.get(ipList, 0));
+                }
+            } else if (isLoopbackOrInternal(ip)) {
+                // 如果不是逗号分隔,但本身是个内网 IP,且代理传了 X-Real-IP,做最后一次尝试
+                String realIp = request.getHeader("X-Real-IP");
+                if (CharSequenceUtil.isNotEmpty(realIp) && !isLoopbackOrInternal(realIp) && !"unknown".equalsIgnoreCase(realIp)) {
+                    ip = realIp.trim();
+                }
+            }
+        }
+        return ip;
+    }
+
+
+
+    /**
+     * 辅助方法:判断 IP 是否为回环地址或局域网内网地址
+     */
+    private static boolean isLoopbackOrInternal(String ip) {
+        if (ip == null) return true;
+        String cleanIp = ip.trim();
+        if ("127.0.0.1".equals(cleanIp) || "0:0:0:0:0:0:0:1".equals(cleanIp) || "localhost".equalsIgnoreCase(cleanIp)) {
+            return true;
+        }
+        // 匹配内网 A/B/C 类私有地址段:10.x.x.x, 192.168.x.x, 172.16.x.x - 172.31.x.x
+        return cleanIp.startsWith("10.") || cleanIp.startsWith("192.168.") ||
+                (cleanIp.startsWith("172.") && cleanIp.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"));
+    }
+
+    // 辅助方法:判断是否为内网 IP(10.x, 172.16.x-172.31.x, 192.168.x)
+    private static boolean isInternalIp(String ip) {
+        if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) return true;
+        // 简单正则匹配常见的私有内网 IP 段
+        return ip.startsWith("10.") || ip.startsWith("192.168.") ||
+                (ip.startsWith("172.") && ip.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"));
+    }
+}

+ 203 - 19
src/main/java/com/jtzx/crm/module/api/web/auth/AdminAuthController.java

@@ -1,30 +1,58 @@
 package com.jtzx.crm.module.api.web.auth;
 
 
-import com.jtzx.crm.aspect.extension.SysLogAspectExtensionForSysUserLogin;
+import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.context.TenantContextHolder;
 import cc.uncarbon.framework.core.context.UserContext;
 import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.ratelimit.annotation.UseRateLimit;
 import cc.uncarbon.framework.web.model.response.ApiResult;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.spring.SpringMVCUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.lang.Dict;
+import cn.hutool.core.util.StrUtil;
+import com.google.common.collect.Maps;
+import com.jtzx.crm.aspect.extension.SysLogAspectExtensionForSmsCodeLogin;
+import com.jtzx.crm.aspect.extension.SysLogAspectExtensionForSysUserAccountLogin;
+import com.jtzx.crm.aspect.extension.SysLogAspectExtensionForSysUserSmsCodeLogin;
 import com.jtzx.crm.helper.CaptchaHelper;
 import com.jtzx.crm.helper.RolePermissionCacheHelper;
 import com.jtzx.crm.module.api.constant.ApiConstant;
+import com.jtzx.crm.module.api.constant.LimitConstant;
+import com.jtzx.crm.module.api.constant.RedisConstant;
 import com.jtzx.crm.module.api.model.interior.AdminCaptchaContainer;
 import com.jtzx.crm.module.api.model.response.AdminCaptchaVO;
+import com.jtzx.crm.module.api.model.response.LoginRespDataVO;
+import com.jtzx.crm.module.api.ratelimit.AccountRateLimitStrategy;
+import com.jtzx.crm.module.api.ratelimit.SmsCodeLoginRateLimitStrategy;
+import com.jtzx.crm.module.api.ratelimit.SmsCodeRateLimitStrategy;
 import com.jtzx.crm.module.api.util.CrmAdminStpUtil;
+import com.jtzx.crm.module.api.util.ReqIpUtil;
+import com.jtzx.crm.module.sms.facade.SmsFacade;
 import com.jtzx.crm.module.sys.annotation.SysLog;
 import com.jtzx.crm.module.sys.model.request.SysUserLoginDTO;
 import com.jtzx.crm.module.sys.model.response.SysUserLoginBO;
 import com.jtzx.crm.module.sys.model.response.SysUserLoginVO;
 import com.jtzx.crm.module.sys.service.SysUserService;
-import cn.dev33.satoken.annotation.SaCheckLogin;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.Pattern;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.comm.utils.SmsUtils;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.LinkedHashMap;
+import java.util.concurrent.TimeUnit;
+
 
 @Tag(name = "后台管理-鉴权接口")
 @RequestMapping(value = ApiConstant.HTTP_API_URL_PREFIX + "/api/v1")
@@ -39,35 +67,160 @@ public class AdminAuthController {
 
     private final CaptchaHelper captchaHelper;
 
+    private final RedisTemplate<String, Object> stringSetRedisTemplate;
 
-    @SysLog(value = "登录后台用户", syncSave = true, extension = SysLogAspectExtensionForSysUserLogin.class, queryIPLocation = true)
-    @Operation(summary = "登录")
-    @PostMapping(value = "/auth/login")
-    public ApiResult<SysUserLoginVO> login(@RequestBody @Valid SysUserLoginDTO dto) {
-        // 登录验证码核验;前端项目搜索关键词「 Helio: 登录验证码」
-        // AdminApiErrorEnum.CAPTCHA_VALIDATE_FAILED.assertTrue(captchaHelper.validate(dto.getCaptchaId(), dto.getCaptchaAnswer()))
+    private final SmsFacade smsFacade;
+
+    /**
+     * 账号密码登陆,输错5次,账号封禁 1800秒(半个小时),目前测试用 300 秒-5分钟,输错超3次,需要填写动态验证码
+     * 这一步的登陆,不会返回token,登陆信息会存入redis 中 半个小时,半小时之内没有后续操作,就自动失效,后续其它操作都不能进行
+     * @param dto
+     * @return
+     */
+    @SysLog(value = "第一步后台用户账号密码登录", extension = SysLogAspectExtensionForSysUserAccountLogin.class, queryIPLocation = true)
+    @Operation(summary = "第一步账号密码登录")
+    @UseRateLimit(duration = 300, max = 5, strategy = AccountRateLimitStrategy.class, mark = "AccountLogin")
+    @PostMapping(value = "/auth/accountlogin")
+    public ApiResult<LoginRespDataVO> login(@RequestBody @Valid SysUserLoginDTO dto) {
+        //限流时redis中的key
+        String key = RedisConstant.ACCOUNT_REDIS_KEY_PREFIX + "account:" + dto.getUsername() + ":AccountLogin";
+        LoginRespDataVO resultData = this.getResultData(null, key);
+        resultData.setCommitTopCount(LimitConstant.login_max);
+        //计算封禁结束时间
+        DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.login_duration);
+        resultData.setEndDateTime(DateUtil.formatDateTime(dateTime));
+        //封禁结束时间存入redis中
+        stringSetRedisTemplate.opsForValue()
+                .set(RedisConstant.ACCOUNT_LOGIN_FAILED_END_DATETIME_PREFIX + dto.getUsername(),
+                        resultData.getEndDateTime(),
+                        LimitConstant.login_duration,
+                        TimeUnit.SECONDS);
 
         // RPC调用, 失败抛异常, 成功返回用户信息
         SysUserLoginBO userInfo = sysUserService.adminLogin(dto);
+        if (userInfo == null) {
+            resultData.setStatus(false);
+            //前两次不返回图形验证码,如果第三次还失败,就返回图形验证码
+            if (LimitConstant.login_fail_count > 0 && resultData.getCommitCount() >= LimitConstant.login_fail_count) {
+                resultData.setShowPicCode(true);
+                //返回图形验证码
+                resultData.setCaptchaVO(new AdminCaptchaVO(captchaHelper.generate()));
+                if (resultData.getCommitCount() > LimitConstant.login_fail_count) {
+                    if (StrUtil.isEmpty(dto.getCaptchaId()) || StrUtil.isEmpty(dto.getCaptchaAnswer())) {
+                        return ApiResult.data("图形验证码未填写", resultData).setCode(406);
+                    }
+                    // 登录验证码核验
+                    if (!captchaHelper.validate(dto.getCaptchaId(), dto.getCaptchaAnswer())) {
+                        return ApiResult.data("图形验证码错误", resultData).setCode(406);
+                    }
+                }
+            }
+            return ApiResult.data("账号或密码错误", resultData).setCode(406);
+        }
+
+        if (StrUtil.isEmpty(userInfo.getPhoneNo())) {
+            resultData.setStatus(false);
+            return ApiResult.data("账号未设置手机号", resultData).setCode(406);
+        }
+        //设置手机号
+        resultData.setMobile(userInfo.getPhoneNo());
+        //记住我
+        userInfo.setRememberMe(dto.getRememberMe());
+
+        //用户数据存入redis,有效期为 5 分钟,超时未进行 发送手机验证码,将无效
+        stringSetRedisTemplate.opsForValue()
+                .set(RedisConstant.LOGIN_USER_INFO_PREFIX + userInfo.getPhoneNo(), userInfo, ApiConstant.LOGIN_USER_INFO, TimeUnit.SECONDS);
+        return ApiResult.data("登录成功", resultData);
+    }
+
+    /**
+     * 发送登陆验证码,5分钟最多发3次
+     * @param mobile
+     * @return
+     */
+    @SysLog(value = "第二步后台用户登录发送登录短信验证码", extension = SysLogAspectExtensionForSmsCodeLogin.class, queryIPLocation = true)
+    @Operation(summary = "第二步发送登录短信验证码")
+    @GetMapping(value = "/auth/sms/smsCode")
+    @UseRateLimit(duration = 300, max = 3, strategy = SmsCodeRateLimitStrategy.class)
+    public ApiResult<String> smsCode(@RequestParam String username,
+                                     @Pattern(message = "手机号格式有误", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO)
+                                     @RequestParam(name = "mobile") String mobile) {
+
+        //计算封禁结束时间
+        DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.smsCode_duration);
+        String formatDateTime = DateUtil.formatDateTime(dateTime);
+        //封禁结束时间存入redis中
+        stringSetRedisTemplate.opsForValue()
+                .set(RedisConstant.SMS_CODE_FAILED_END_DATETIME_PREFIX + mobile,
+                        formatDateTime,
+                        LimitConstant.smsCode_duration,
+                        TimeUnit.SECONDS);
+        //账号密码redis中存储的用户信息
+        SysUserLoginBO userInfo = this.getLoginUserInfo(mobile);
+        String smsCode = SmsUtils.getRandomInt(RedisConstant.SMS_CODE_LENGTH);
+        log.info("smsCode--------->{}", smsCode);
+        LinkedHashMap<String, String> messages = Maps.newLinkedHashMap();
+        messages.put("code", smsCode);
+        SmsResponse smsResponse = SmsFactory.getSmsBlend().sendMessage(mobile, messages);
+        if (smsResponse.isSuccess()) {
+            smsFacade.set(RedisConstant.SMS_CODE_PREFIX + mobile, smsCode, RedisConstant.SMS_CODE_DURATION);
+            return ApiResult.data("发送成功");
+        }
+        return ApiResult.fail(406, "发送失败");
+    }
+
+    @SysLog(value = "第三步后台用户手机验证码登录", extension = SysLogAspectExtensionForSysUserSmsCodeLogin.class, queryIPLocation = true)
+    @Operation(summary = "第三步验证码登录")
+    @UseRateLimit(duration = 600, max = 3, strategy = SmsCodeLoginRateLimitStrategy.class, mark = "smsCodeLogin")
+    @GetMapping(value = "/auth/smsCodeLogin")
+    public ApiResult<LoginRespDataVO> smsCodeLogin(@RequestParam String username,
+                                                   @Pattern(message = "手机号格式有误", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO) @RequestParam String mobile,
+                                                   @RequestParam String smsCode) {
+
+        //账号密码redis中存储的用户信息
+        SysUserLoginBO userInfo = this.getLoginUserInfo(mobile);
+
+        //限流时redis中的key
+        String key = RedisConstant.SMS_CODE_REDIS_KEY_PREFIX + "smsCodeLogin:" + mobile + ":smsCodeLogin";
+        LoginRespDataVO resultData = this.getResultData(null, key);
+        resultData.setCommitTopCount(LimitConstant.smsCode_login_max);
+        //计算封禁结束时间
+        DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.smsCode_login_duration);
+        resultData.setEndDateTime(DateUtil.formatDateTime(dateTime));
+        //封禁结束时间存入redis中
+        stringSetRedisTemplate.opsForValue()
+                .set(RedisConstant.SMS_CODE_LOGIN_FAILED_END_DATETIME_PREFIX + mobile,
+                        resultData.getEndDateTime(),
+                        LimitConstant.smsCode_login_duration,
+                        TimeUnit.SECONDS);
+        Object cacheData = smsFacade.get(RedisConstant.SMS_CODE_PREFIX + userInfo.getPhoneNo());
+        if (cacheData == null) {
+            resultData.setStatus(false);
+            return ApiResult.data("验证码错误", resultData).setCode(406);
+        }
+        String code = (String) cacheData;
+        if (!code.equals(smsCode)) {
+            resultData.setStatus(false);
+            return ApiResult.data("验证码错误", resultData).setCode(406);
+        }
 
         // 构造用户上下文
         UserContext userContext = UserContext.builder()
                 .userId(userInfo.getId())
                 .userName(userInfo.getUsername())
                 .userPhoneNo(userInfo.getPhoneNo())
-                .userTypeStr("ADMIN_USER")
-                .extraData(null)
+                .userTypeStr("GroupManager")//分组组长
+                .extraData(Dict.parse(userInfo))//用户信息保存在扩展数据中
                 .rolesIds(userInfo.getRoleIds())
                 .roles(userInfo.getRoles())
+                .clientIP(ReqIpUtil.getIpAddr(SpringMVCUtil.getRequest()))//获取用户登陆id
                 .build();
-
+        if (CollUtil.isNotEmpty(userInfo.getRoles()) && userInfo.getRoles().contains("Admin")) {
+            userContext.setUserTypeStr("Admin");//管理员
+        }
         // 将用户ID注册到 SA-Token ,并附加一些业务字段
-        CrmAdminStpUtil.login(userInfo.getId(), dto.getRememberMe());
+        CrmAdminStpUtil.login(userInfo.getId(), userInfo.getRememberMe());
         CrmAdminStpUtil.getSession().set(UserContext.CAMEL_NAME, userContext);
-
-        // 更新角色-权限缓存
-        rolePermissionCacheHelper.putCache(userInfo.getRoleIdPermissionMap());
-
         // 返回登录token
         SysUserLoginVO tokenInfo = SysUserLoginVO.builder()
                 .tokenName(CrmAdminStpUtil.getTokenName())
@@ -75,8 +228,8 @@ public class AdminAuthController {
                 .roles(userInfo.getRoles())
                 .permissions(userInfo.getPermissions())
                 .build();
-
-        return ApiResult.data("登录成功", tokenInfo);
+        resultData.setTokenInfo(tokenInfo);
+        return ApiResult.data("登录成功", resultData);
     }
 
     @SaCheckLogin(type = CrmAdminStpUtil.TYPE)
@@ -90,7 +243,38 @@ public class AdminAuthController {
         return ApiResult.success();
     }
 
-    @Operation(summary = "获取验证码")
+
+    public SysUserLoginBO getLoginUserInfo(String mobile) {
+        //获取redis中的用户信息
+        Object object = stringSetRedisTemplate.opsForValue().get(RedisConstant.LOGIN_USER_INFO_PREFIX + mobile);
+        if (object == null) {
+            throw new BusinessException(407, "登录已超时,需重新登陆");
+        }
+        SysUserLoginBO userInfo = (SysUserLoginBO) object;
+        if (!userInfo.getPhoneNo().equals(mobile)) {
+            throw new BusinessException(406, "手机号错误");
+        }
+        return userInfo;
+    }
+
+    public LoginRespDataVO getResultData(String mobile, String key) {
+        LoginRespDataVO resultData = LoginRespDataVO.builder().build();
+        resultData.setMobile(mobile);
+        resultData.setShowPicCode(false);
+        resultData.setStatus(true);
+        resultData.setCommitCount(0);
+        if (key != null) {
+            Object object = stringSetRedisTemplate.opsForValue().get(key);
+            if (object != null) {
+                Integer count = (Integer) object;
+                resultData.setCommitCount(count);
+            }
+        }
+
+        return resultData;
+    }
+
+    @Operation(summary = "重新获取图形验证码(不是短信验证码)")
     @GetMapping(value = "/auth/captcha")
     public ApiResult<AdminCaptchaVO> captcha() {
         // 核验方法:captchaHelper.validate

+ 0 - 61
src/main/java/com/jtzx/crm/module/api/web/oss/AdminOssFileInfoController.java

@@ -1,61 +0,0 @@
-package com.jtzx.crm.module.api.web.oss;
-
-import cc.uncarbon.framework.core.constant.HelioConstant;
-import cc.uncarbon.framework.core.page.PageParam;
-import cc.uncarbon.framework.core.page.PageResult;
-import cc.uncarbon.framework.web.model.request.IdsDTO;
-import cc.uncarbon.framework.web.model.response.ApiResult;
-import com.jtzx.crm.module.api.constant.ApiConstant;
-import com.jtzx.crm.module.oss.model.request.AdminListOssFileInfoDTO;
-import com.jtzx.crm.module.oss.model.response.OssFileInfoBO;
-import com.jtzx.crm.module.oss.service.OssFileInfoService;
-import com.jtzx.crm.module.api.util.CrmAdminStpUtil;
-import cn.dev33.satoken.annotation.SaCheckLogin;
-import cn.dev33.satoken.annotation.SaCheckPermission;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Operation;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.*;
-
-import jakarta.validation.Valid;
-
-
-@SaCheckLogin(type = CrmAdminStpUtil.TYPE)
-@Tag(name = "后台管理-上传文件信息管理接口")
-@RequestMapping(value = ApiConstant.HTTP_API_URL_PREFIX + "/api/v1")
-@RequiredArgsConstructor
-@RestController
-@Slf4j
-public class AdminOssFileInfoController {
-
-    // 功能权限串前缀
-    private static final String PERMISSION_PREFIX = "OssFileInfo:";
-
-    private final OssFileInfoService ossFileInfoService;
-
-
-    @SaCheckPermission(type = CrmAdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
-    @Operation(summary = "分页列表")
-    @GetMapping(value = "/oss/file/infos")
-    public ApiResult<PageResult<OssFileInfoBO>> list(PageParam pageParam, AdminListOssFileInfoDTO dto) {
-        return ApiResult.data(ossFileInfoService.adminList(pageParam, dto));
-    }
-
-    @SaCheckPermission(type = CrmAdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
-    @Operation(summary = "详情")
-    @GetMapping(value = "/oss/file/infos/{id}")
-    public ApiResult<OssFileInfoBO> getById(@PathVariable Long id) {
-        return ApiResult.data(ossFileInfoService.getOneById(id, true));
-    }
-
-    @SaCheckPermission(type = CrmAdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.DELETE)
-    @Operation(summary = "删除")
-    @DeleteMapping(value = "/oss/file/infos")
-    public ApiResult<Void> delete(@RequestBody @Valid IdsDTO<Long> dto) {
-        ossFileInfoService.adminDelete(dto.getIds());
-
-        return ApiResult.success();
-    }
-
-}

+ 15 - 16
src/main/java/com/jtzx/crm/module/api/web/oss/AdminOssUploadDownloadController.java

@@ -2,6 +2,13 @@ package com.jtzx.crm.module.api.web.oss;
 
 import cc.uncarbon.framework.core.exception.BusinessException;
 import cc.uncarbon.framework.web.model.response.ApiResult;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.io.file.FileNameUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.http.Header;
 import com.jtzx.crm.module.api.constant.ApiConstant;
 import com.jtzx.crm.module.api.enums.AdminApiErrorEnum;
 import com.jtzx.crm.module.api.util.CrmAdminStpUtil;
@@ -12,17 +19,10 @@ import com.jtzx.crm.module.oss.model.response.OssFileDownloadReplyBO;
 import com.jtzx.crm.module.oss.model.response.OssFileInfoBO;
 import com.jtzx.crm.module.oss.model.response.OssFileUploadResultVO;
 import com.jtzx.crm.module.oss.util.UploadFileChecker;
-import cn.dev33.satoken.annotation.SaCheckLogin;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.text.CharSequenceUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.crypto.digest.DigestUtil;
-import cn.hutool.http.Header;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.MediaType;
@@ -48,15 +48,13 @@ public class AdminOssUploadDownloadController {
     @PostMapping(value = "/oss/files")
     // 约束:登录后才能上传   👇 后台管理对应的鉴权工具类
     @SaCheckLogin(type = CrmAdminStpUtil.TYPE)
-    public ApiResult<OssFileUploadResultVO> upload(
-            @RequestPart MultipartFile file, @RequestPart(required = false) @Valid UploadFileAttributeDTO attr,
-            HttpServletRequest request
-    ) throws IOException {
+    public ApiResult<OssFileUploadResultVO> upload(@RequestPart MultipartFile file,
+                                                   HttpServletRequest request) throws IOException {
         AdminApiErrorEnum.UPLOAD_FILE_NOT_EXIST.assertNotNull(file);
         UploadFileCheckResultEnum checkResult = UploadFileChecker.check(file,
                 // 默认只能上传1024MB之内的文件,且约束后缀名
                 UploadFileChecker.FILE_SIZE_1MB * 1024,
-                new String[]{"jpg", "png", "webp", "gif", "xlsx"});
+                new String[]{"jpg","jpeg", "png", "webp", "gif", "xlsx", "xls", "pdf"});
         if (checkResult.isNotOK()) {
             throw new BusinessException(checkResult.getLabel());
         }
@@ -67,10 +65,14 @@ public class AdminOssUploadDownloadController {
         String md5 = DigestUtil.md5Hex(file.getBytes());
         OssFileInfoBO bo = ossUploadDownloadFacade.findByHash(md5);
         if (bo == null) {
-
             /*
             2. 普通上传
              */
+            UploadFileAttributeDTO attr = new UploadFileAttributeDTO();
+            String suffix = FileNameUtil.getSuffix(file.getOriginalFilename());
+            if (CharSequenceUtil.isNotBlank(suffix)) {
+                attr.setClassified(suffix + "类型文件");
+            }
             attr = ObjectUtil.defaultIfNull(attr, UploadFileAttributeDTO::new);
             attr
                     .setOriginalFilename(file.getOriginalFilename())
@@ -91,19 +93,16 @@ public class AdminOssUploadDownloadController {
     // @SaCheckLogin(type = AdminStpUtil.TYPE)
     public void download(@PathVariable Long id, HttpServletResponse response) throws IOException {
         OssFileDownloadReplyBO reply = ossUploadDownloadFacade.downloadById(id);
-
         if (reply.isRedirect2DirectUrl()) {
             // 302重定向
             response.sendRedirect(reply.getDirectUrl());
             return;
         }
-
         // 普通下载
         response.setHeader(Header.CONTENT_TYPE.getValue(), MediaType.APPLICATION_OCTET_STREAM_VALUE);
         response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
         String downFileName = URLEncoder.encode(reply.getStorageFilename(), StandardCharsets.UTF_8);
         response.setHeader(Header.CONTENT_DISPOSITION.getValue(), "attachment;filename=" + downFileName);
-
         IoUtil.write(response.getOutputStream(), false, reply.getFileBytes());
     }
 

+ 70 - 0
src/main/java/com/jtzx/crm/module/api/web/sms/SmsController.java

@@ -0,0 +1,70 @@
+package com.jtzx.crm.module.api.web.sms;
+
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.ratelimit.annotation.UseRateLimit;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import com.google.common.collect.Maps;
+import com.jtzx.crm.module.api.constant.ApiConstant;
+import com.jtzx.crm.module.api.constant.RedisConstant;
+import com.jtzx.crm.module.api.ratelimit.SmsCodeRateLimitStrategy;
+import com.jtzx.crm.module.sms.facade.SmsFacade;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.Pattern;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.comm.utils.SmsUtils;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.LinkedHashMap;
+
+@Tag(name = "后台管理-发送短信接口")
+@RequestMapping(value = ApiConstant.HTTP_API_URL_PREFIX + "/api/v1")
+@RequiredArgsConstructor
+@RestController
+@Slf4j
+public class SmsController {
+
+
+    private final SmsFacade smsFacade;
+
+    private final RedisTemplate<String, Object> stringSetRedisTemplate;
+
+
+    /**
+     * mark: 限流标识文本,会拼接至Redis Key中;空文本则会使用Java方法的全限定名
+     * duration: 限流时长,单位=秒
+     * max: 在限流时长内,最多可请求多少次
+     * strategy: 限流策略
+     * - RateLimitGlobalStrategy: 全局限流(默认值)
+     * - RateLimitByUserIdStrategy: 根据当前用户ID限流
+     * - RateLimitByClientIPStrategy: 根据客户端IP限流
+     * - SendSmsCodeRateLimitStrategy
+     *
+     * @param mobile
+     * @return
+     */
+    @Operation(summary = "获取短信验证码")
+    @GetMapping(value = "/sms/captcha")
+    @UseRateLimit(duration = 300, max = 3, strategy = SmsCodeRateLimitStrategy.class)
+    public ApiResult<String> captcha(@Pattern(message = "手机号格式有误", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO)
+                                        @RequestParam(name = "mobile") String mobile) {
+        String smsCode = SmsUtils.getRandomInt(RedisConstant.SMS_CODE_LENGTH);
+        log.info("smsCode--------->{}", smsCode);
+        LinkedHashMap<String, String> messages = Maps.newLinkedHashMap();
+        messages.put("code", smsCode);
+        SmsResponse smsResponse = SmsFactory.getSmsBlend().sendMessage(mobile, messages);
+        if (smsResponse.isSuccess()) {
+            smsFacade.set(RedisConstant.SMS_CODE_PREFIX + mobile, smsCode, RedisConstant.SMS_CODE_DURATION);
+            return ApiResult.data("发送成功");
+        }
+        return ApiResult.fail(406, "发送失败");
+    }
+}

+ 1 - 1
src/main/java/com/jtzx/crm/module/oss/enums/UploadFileCheckResultEnum.java

@@ -17,7 +17,7 @@ public enum UploadFileCheckResultEnum implements HelioBaseEnum<Integer> {
     TOO_MANY_FILES(402, "欲上传的文件数量超出限制"),
     TOO_LARGE_FILE_SIZE(403, "欲上传的文件大小超出限制"),
     EMPTY_FILE(404, "欲上传的文件为空"),
-    ILLEGAL_FILE_SUFFIX(405, "欲上传的文件类型超出限制"),;
+    ILLEGAL_FILE_SUFFIX(405, "欲上传的文件类型不支持"),;
 
     @EnumValue
     private final Integer value;

+ 1 - 2
src/main/java/com/jtzx/crm/module/oss/model/request/UploadFileAttributeDTO.java

@@ -18,9 +18,8 @@ import java.io.Serializable;
 @Data
 public class UploadFileAttributeDTO implements Serializable {
 
-    @Schema(description = "文件类别", example = "id_card=身份证 driver_license=驾驶证")
+    @Schema(description = "文件类别", example = "id_card=身份证 driver_license=驾驶证", hidden = true)
     private String classified;
-
     /*
     以下字段为内部使用
      */

+ 38 - 0
src/main/java/com/jtzx/crm/module/sms/facade/SmsFacade.java

@@ -0,0 +1,38 @@
+package com.jtzx.crm.module.sms.facade;
+
+public interface SmsFacade {
+
+    /**
+     * 存储
+     *
+     * @param key       键
+     * @param value     值
+     * @param cacheTime 缓存时间(单位:秒)
+     */
+    void set(String key, Object value, long cacheTime);
+
+    /**
+     * 存储
+     *
+     * @param key   键
+     * @param value 值
+     */
+    void set(String key, Object value);
+
+    /**
+     * 读取
+     *
+     * @param key 键
+     * @return 值
+     */
+    Object get(String key);
+
+    /**
+     *  remove
+     * <p> 根据key移除缓存
+     * @param key 缓存键
+     * @return 被删除的value
+     * @author :Wind
+     */
+    Object remove(String key);
+}

+ 35 - 0
src/main/java/com/jtzx/crm/module/sms/facade/impl/SmsFacadeImpl.java

@@ -0,0 +1,35 @@
+package com.jtzx.crm.module.sms.facade.impl;
+
+import com.jtzx.crm.module.sms.facade.SmsFacade;
+import com.jtzx.crm.module.sms.service.SmsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SmsFacadeImpl implements SmsFacade {
+
+    private final SmsService smsService;
+
+    @Override
+    public void set(String key, Object value, long cacheTime) {
+        smsService.set(key, value, cacheTime);
+    }
+
+    @Override
+    public void set(String key, Object value) {
+        smsService.set(key, value);
+    }
+
+    @Override
+    public Object get(String key) {
+        return smsService.get(key);
+    }
+
+    @Override
+    public Object remove(String key) {
+        return smsService.remove(key);
+    }
+}

+ 44 - 0
src/main/java/com/jtzx/crm/module/sms/service/SmsService.java

@@ -0,0 +1,44 @@
+package com.jtzx.crm.module.sms.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.sms4j.api.dao.SmsDao;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 自定义短信存储,使用redis存储
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SmsService implements SmsDao {
+
+    private final RedisTemplate<String, Object> stringSetRedisTemplate;
+
+    @Override
+    public void set(String key, Object value, long cacheTime) {
+        stringSetRedisTemplate.opsForValue().set(key, value, cacheTime, TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void set(String key, Object value) {
+        stringSetRedisTemplate.opsForValue().set(key, value);
+    }
+
+    @Override
+    public Object get(String key) {
+        return stringSetRedisTemplate.opsForValue().get(key);
+    }
+
+    @Override
+    public Object remove(String key) {
+        return stringSetRedisTemplate.delete(key);
+    }
+
+    @Override
+    public void clean() {
+    }
+}

+ 4 - 0
src/main/java/com/jtzx/crm/module/sys/constant/SysConstant.java

@@ -22,11 +22,15 @@ public final class SysConstant {
      * 超级管理员角色ID
      */
     public static final Long SUPER_ADMIN_ROLE_ID = 1L;
+    public static final Long ADMIN_ROLE_ID = 2L;
+    public static final Long GROUPMANAGER_ROLE_ID = 3L;
 
     /**
      * 超级管理员角色值(固定)
      */
     public static final String SUPER_ADMIN_ROLE_VALUE = "SuperAdmin";
+    public static final String ADMIN_ROLE_VALUE = "Admin";
+    public static final String GROUP_MANAGER_ROLE_VALUE = "GroupManager";
 
     /**
      * 租户管理员角色值

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysDataDictClassifiedEntity.java

@@ -47,4 +47,6 @@ public class SysDataDictClassifiedEntity extends HelioBaseEntity<Long> {
     @TableField(value = "description")
     private String description;
 
+    @TableField("del_flag")
+    private Integer delFlag;
 }

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysDataDictItemEntity.java

@@ -59,4 +59,6 @@ public class SysDataDictItemEntity extends HelioBaseEntity<Long> {
     @TableField(value = "description")
     private String description;
 
+    @TableField("del_flag")
+    private Integer delFlag;
 }

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysDeptEntity.java

@@ -49,4 +49,6 @@ public class SysDeptEntity extends HelioBaseEntity<Long> {
 	@TableField(value = "status")
 	private EnabledStatusEnum status;
 
+	@TableField("del_flag")
+	private Integer delFlag;
 }

+ 6 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysLogEntity.java

@@ -77,4 +77,10 @@ public class SysLogEntity extends HelioBaseEntity<Long> {
 	@TableField(value = "ip_location_district_name")
 	private String ipLocationDistrictName;
 
+	@Schema(description = "日志类型,0-登陆")
+	@TableField("log_type")
+	private Integer logType;
+
+	@TableField("del_flag")
+	private Integer delFlag;
 }

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysMenuEntity.java

@@ -62,4 +62,6 @@ public class SysMenuEntity extends HelioBaseEntity<Long> {
 	@TableField(value = "external_link")
 	private String externalLink;
 
+	@TableField("del_flag")
+	private Integer delFlag;
 }

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysParamEntity.java

@@ -36,4 +36,6 @@ public class SysParamEntity extends HelioBaseEntity<Long> {
     @TableField(value = "description")
     private String description;
 
+    @TableField("del_flag")
+    private Integer delFlag;
 }

+ 15 - 6
src/main/java/com/jtzx/crm/module/sys/entity/SysRoleEntity.java

@@ -1,9 +1,9 @@
 package com.jtzx.crm.module.sys.entity;
 
 import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
-import com.jtzx.crm.module.sys.constant.SysConstant;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.jtzx.crm.module.sys.constant.SysConstant;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -33,18 +33,27 @@ public class SysRoleEntity extends HelioBaseEntity<Long> {
 	@TableField(value = "value")
 	private String value;
 
+	@Schema(description = "角色类型,0-超级管理员,1-管理员,2-项目组长")
+	@TableField(value = "role_type")
+	private Integer roleType;
+
+	@TableField("del_flag")
+	private Integer delFlag;
+
 	/**
-	 * 角色实例可被视为超级管理员
+	 * 管理员
 	 */
-	public boolean isSuperAdmin() {
+	public boolean isAdmin() {
 		return SysConstant.SUPER_ADMIN_ROLE_ID.equals(getId()) || SysConstant.SUPER_ADMIN_ROLE_VALUE.equalsIgnoreCase(getValue());
 	}
 
+
 	/**
-	 * 角色实例可被视为租户管理员
+	 * 分组组长
+	 * @return
 	 */
-	public boolean isTenantAdmin() {
-		return SysConstant.TENANT_ADMIN_ROLE_VALUE.equalsIgnoreCase(getValue());
+	public boolean isGroupManager() {
+		return SysConstant.GROUPMANAGER_ROLE_ID.equals(getId()) || SysConstant.GROUP_MANAGER_ROLE_VALUE.equalsIgnoreCase(getValue());
 	}
 
 }

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysRoleMenuRelationEntity.java

@@ -32,4 +32,6 @@ public class SysRoleMenuRelationEntity extends HelioNoTenantBaseEntity<Long> {
 	@TableField(value = "menu_id")
 	private Long menuId;
 
+	@TableField("del_flag")
+	private Integer delFlag;
 }

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysTenantEntity.java

@@ -41,4 +41,6 @@ public class SysTenantEntity extends HelioBaseEntity<Long> {
     @TableField(value = "remark")
     private String remark;
 
+    @TableField("del_flag")
+    private Integer delFlag;
 }

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysUserDeptRelationEntity.java

@@ -32,4 +32,6 @@ public class SysUserDeptRelationEntity extends HelioNoTenantBaseEntity<Long> {
 	@TableField(value = "dept_id")
 	private Long deptId;
 
+	@TableField("del_flag")
+	private Integer delFlag;
 }

+ 31 - 3
src/main/java/com/jtzx/crm/module/sys/entity/SysUserEntity.java

@@ -2,12 +2,12 @@ package com.jtzx.crm.module.sys.entity;
 
 import cc.uncarbon.framework.core.enums.GenderEnum;
 import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
-import com.jtzx.crm.module.sys.enums.SysUserStatusEnum;
-import com.jtzx.crm.module.sys.model.request.AdminUpdateCurrentSysUserAvatarDTO;
-import com.jtzx.crm.module.sys.model.request.AdminUpdateCurrentSysUserInfoDTO;
 import cn.hutool.core.annotation.Alias;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.jtzx.crm.module.sys.enums.SysUserStatusEnum;
+import com.jtzx.crm.module.sys.model.request.AdminUpdateCurrentSysUserAvatarDTO;
+import com.jtzx.crm.module.sys.model.request.AdminUpdateCurrentSysUserInfoDTO;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -73,6 +73,34 @@ public class SysUserEntity extends HelioBaseEntity<Long> {
 	private String avatarUrl;
 
 
+	@Schema(description = "用户名称")
+	@TableField(value = "user_name")
+	private String userName;
+
+	@Schema(description = "公海跟进状态开关,0-关,1-开")
+	@TableField(value = "public_follow_status")
+	private Integer publicFollowStatus;
+
+	@Schema(description = "历史跟进状态开关,0-关,1-开")
+	@TableField(value = "history_follow_status")
+	private Integer historyFollowStatus;
+
+	@Schema(description = "持有上限")
+	@TableField(value = "hold_top_count")
+	private Integer holdTopCount;
+
+	@Schema(description = "日退上限")
+	@TableField(value = "day_back_top_count")
+	private Integer dayBackTopCount;
+
+	@Schema(description = "退回公海天数")
+	@TableField(value = "back_public_day_count")
+	private Integer backPublicDayCount;
+
+
+	@TableField("del_flag")
+	private Integer delFlag;
+
 	public static SysUserEntity of(AdminUpdateCurrentSysUserInfoDTO dto) {
 		SysUserEntity ret = new SysUserEntity();
 		ret.setNickname(dto.getNickname())

+ 10 - 0
src/main/java/com/jtzx/crm/module/sys/entity/SysUserRoleRelationEntity.java

@@ -32,4 +32,14 @@ public class SysUserRoleRelationEntity extends HelioNoTenantBaseEntity<Long> {
 	@TableField(value = "role_id")
 	private Long roleId;
 
+	@Schema(description = "角色标识")
+	@TableField(value = "role_value")
+	private String roleValue;
+
+	@Schema(description = "分组表主id,管理员不存这个数据")
+	@TableField(value = "group_id")
+	private Long groupId;
+
+	@TableField("del_flag")
+	private Integer delFlag;
 }

+ 1 - 1
src/main/java/com/jtzx/crm/module/sys/mapper/xml/SysUserMapper.xml

@@ -6,7 +6,7 @@
         SELECT *
         FROM sys_user
         WHERE del_flag = 0
-            AND pin = #{pin}
+          AND pin = #{pin}
         LIMIT 1
     </select>
 

+ 5 - 11
src/main/java/com/jtzx/crm/module/sys/model/interior/UserRoleContainer.java

@@ -23,26 +23,20 @@ public class UserRoleContainer {
     private final List<SysRoleEntity> relatedRoles;
 
     /**
-     * 为超级管理员
+     * 管理员
      */
-    private final boolean superAdmin;
-
-    /**
-     * 为租户管理员
-     */
-    private final boolean tenantAdmin;
+    private final boolean admin;
 
     /**
      * 非级管理员or租户管理员
      */
-    private final boolean notAnyAdmin;
+    private final boolean groupManager;
 
 
     public UserRoleContainer(Set<Long> relatedRoleIds, List<SysRoleEntity> relatedRoles) {
         this.relatedRoleIds = relatedRoleIds;
         this.relatedRoles = relatedRoles;
-        this.superAdmin = relatedRoles.stream().anyMatch(SysRoleEntity::isSuperAdmin);
-        this.tenantAdmin = relatedRoles.stream().anyMatch(SysRoleEntity::isTenantAdmin);
-        this.notAnyAdmin = !this.superAdmin && !this.tenantAdmin;
+        this.admin = relatedRoles.stream().anyMatch(SysRoleEntity::isAdmin);
+        this.groupManager = relatedRoles.stream().anyMatch(SysRoleEntity::isGroupManager);
     }
 }

+ 2 - 0
src/main/java/com/jtzx/crm/module/sys/model/request/AdminInsertSysLogDTO.java

@@ -58,4 +58,6 @@ public class AdminInsertSysLogDTO implements Serializable {
     @Schema(description = "IP地址属地-县级行政区名")
     private String ipLocationDistrictName;
 
+    @Schema(description = "日志类型,0-登陆")
+    private Integer logType;
 }

+ 3 - 5
src/main/java/com/jtzx/crm/module/sys/model/request/SysUserLoginDTO.java

@@ -2,7 +2,6 @@ package com.jtzx.crm.module.sys.model.request;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -33,12 +32,11 @@ public class SysUserLoginDTO implements Serializable {
     @NotBlank(message = "密码不能为空")
     private String password;
 
-    @Schema(description = "记住我", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "记住我不能为空")
+    @Schema(description = "记住我")
     private Boolean rememberMe;
 
-    @Schema(description = "租户ID(可选,启用多租户后有效)")
-    private Long tenantId;
+//    @Schema(description = "租户ID(可选,启用多租户后有效)")
+//    private Long tenantId;
 
     @Schema(description = "验证码唯一标识(可选)")
     private String captchaId;

+ 7 - 3
src/main/java/com/jtzx/crm/module/sys/model/response/SysUserLoginBO.java

@@ -1,6 +1,5 @@
 package com.jtzx.crm.module.sys.model.response;
 
-import cc.uncarbon.framework.core.context.TenantContext;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -25,6 +24,8 @@ import java.util.Set;
 @Data
 public class SysUserLoginBO implements Serializable {
 
+    public static final String CAMEL_NAME = "sysUserLoginBO";
+
     @Schema(description = "用户ID")
     private Long id;
 
@@ -49,7 +50,10 @@ public class SysUserLoginBO implements Serializable {
     @Schema(description = "角色ID-对应权限名 Map")
     private Map<Long, Set<String>> roleIdPermissionMap;
 
-    @Schema(description = "关联租户上下文")
-    private TenantContext tenantContext;
+    @Schema(description = "记住我",hidden = true)
+    private Boolean rememberMe;
+
+//    @Schema(description = "关联租户上下文")
+//    private TenantContext tenantContext;
 
 }

+ 5 - 5
src/main/java/com/jtzx/crm/module/sys/service/SysDeptService.java

@@ -125,11 +125,11 @@ public class SysDeptService {
     public List<SysDeptBO> adminSelectOptions(boolean inferiorsOnly) {
         if (inferiorsOnly) {
             UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer();
-            if (currentUser.isNotAnyAdmin()) {
-                // 非管理员才会限制,只能看到本部门及以下
-                UserDeptContainer deptContainer = getCurrentUserDeptContainer(true);
-                return entityList2BOs(deptContainer.getVisibleDepts());
-            }
+//            if (currentUser.isNotAnyAdmin()) {
+//                // 非管理员才会限制,只能看到本部门及以下
+//                UserDeptContainer deptContainer = getCurrentUserDeptContainer(true);
+//                return entityList2BOs(deptContainer.getVisibleDepts());
+//            }
         }
         // 能看所有
         return adminList();

+ 1 - 1
src/main/java/com/jtzx/crm/module/sys/service/SysMenuService.java

@@ -172,7 +172,7 @@ public class SysMenuService {
                 roleId -> {
                     Set<String> permissions;
 
-                    if (SysConstant.SUPER_ADMIN_ROLE_ID.equals(roleId)) {
+                    if (SysConstant.SUPER_ADMIN_ROLE_ID.equals(roleId) || SysConstant.ADMIN_ROLE_ID.equals(roleId)) {
                         // 超级管理员读取所有权限,不管有没有被禁用
                         permissions = sysMenuMapper.selectList(null).stream()
                                 .map(SysMenuEntity::getPermission)

+ 39 - 28
src/main/java/com/jtzx/crm/module/sys/service/SysRoleService.java

@@ -6,20 +6,23 @@ import cc.uncarbon.framework.core.exception.BusinessException;
 import cc.uncarbon.framework.core.function.StreamFunction;
 import cc.uncarbon.framework.core.page.PageParam;
 import cc.uncarbon.framework.core.page.PageResult;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.text.CharSequenceUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.jtzx.crm.module.sys.constant.SysConstant;
 import com.jtzx.crm.module.sys.entity.SysRoleEntity;
+import com.jtzx.crm.module.sys.entity.SysUserRoleRelationEntity;
 import com.jtzx.crm.module.sys.enums.SysErrorEnum;
 import com.jtzx.crm.module.sys.mapper.SysRoleMapper;
+import com.jtzx.crm.module.sys.mapper.SysUserRoleRelationMapper;
 import com.jtzx.crm.module.sys.model.interior.UserRoleContainer;
 import com.jtzx.crm.module.sys.model.request.AdminBindRoleMenuRelationDTO;
 import com.jtzx.crm.module.sys.model.request.AdminInsertOrUpdateSysRoleDTO;
 import com.jtzx.crm.module.sys.model.request.AdminListSysRoleDTO;
 import com.jtzx.crm.module.sys.model.response.SysRoleBO;
-import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.text.CharSequenceUtil;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -38,7 +41,8 @@ import java.util.stream.Collectors;
 public class SysRoleService {
 
     private final SysRoleMapper sysRoleMapper;
-    private final SysUserRoleRelationService sysUserRoleRelationService;
+    //    private final SysUserRoleRelationService sysUserRoleRelationService;
+    private final SysUserRoleRelationMapper sysUserRoleRelationMapper;
     private final SysRoleMenuRelationService sysRoleMenuRelationService;
     private final SysMenuService sysMenuService;
 
@@ -199,19 +203,25 @@ public class SysRoleService {
      * @return 失败返回空 map
      */
     public Map<Long, String> getRoleMapByUserId(Long userId) {
-        Set<Long> roleIds = sysUserRoleRelationService.listRoleIdsByUserId(userId);
 
-        if (CollUtil.isEmpty(roleIds)) {
+        List<SysUserRoleRelationEntity> relationEntityList =
+                sysUserRoleRelationMapper.selectList(
+                        new LambdaQueryWrapper<>(SysUserRoleRelationEntity.class)
+                                .select(
+                                        SysUserRoleRelationEntity::getRoleId,
+                                        SysUserRoleRelationEntity::getRoleValue
+                                )
+                                .eq(SysUserRoleRelationEntity::getUserId, userId)
+                );
+
+        if (CollUtil.isEmpty(relationEntityList)) {
             return Collections.emptyMap();
         }
 
         // 根据角色Ids取 map
-        return sysRoleMapper.selectList(
-                new QueryWrapper<SysRoleEntity>()
-                        .lambda()
-                        .select(SysRoleEntity::getId, SysRoleEntity::getValue)
-                        .in(SysRoleEntity::getId, roleIds)
-        ).stream().collect(Collectors.toMap(SysRoleEntity::getId, SysRoleEntity::getValue, StreamFunction.ignoredThrowingMerger()));
+        return relationEntityList
+                .stream()
+                .collect(Collectors.toMap(SysUserRoleRelationEntity::getRoleId, SysUserRoleRelationEntity::getRoleValue, StreamFunction.ignoredThrowingMerger()));
     }
 
     /**
@@ -227,7 +237,15 @@ public class SysRoleService {
      * 仅内部使用
      */
     protected UserRoleContainer getSpecifiedUserRoleContainer(Long specifiedUserId) {
-        Set<Long> userRoleIds = sysUserRoleRelationService.listRoleIdsByUserId(specifiedUserId);
+        if (specifiedUserId == null) {
+            throw new IllegalArgumentException("userId不能为空");
+        }
+        Set<Long> userRoleIds = sysUserRoleRelationMapper.selectList(
+                new QueryWrapper<SysUserRoleRelationEntity>()
+                        .lambda()
+                        .select(SysUserRoleRelationEntity::getRoleId)
+                        .eq(SysUserRoleRelationEntity::getUserId, specifiedUserId)
+        ).stream().map(SysUserRoleRelationEntity::getRoleId).collect(Collectors.toSet());
         List<SysRoleEntity> userRoles = null;
         if (CollUtil.isNotEmpty(userRoleIds)) {
             userRoles = sysRoleMapper.selectBatchIds(userRoleIds);
@@ -248,13 +266,10 @@ public class SysRoleService {
     protected Set<Long> determineInvisibleRoleIds() {
         UserRoleContainer currentUser = getCurrentUserRoleContainer();
         // 超级管理员:不限制
-        if (currentUser.isSuperAdmin()) {
+        if (currentUser.isAdmin()) {
             return new HashSet<>();
         }
-        // 租户管理员:列表中不显示超级管理员角色
-        if (currentUser.isTenantAdmin()) {
-            return CollUtil.newHashSet(SysConstant.SUPER_ADMIN_ROLE_ID);
-        }
+
         // 普通角色:列表中不显示超级管理员、租户管理员角色
         Set<Long> ret = sysRoleMapper.selectList(
                 new QueryWrapper<SysRoleEntity>()
@@ -380,7 +395,7 @@ public class SysRoleService {
         if (isUpdating) {
             SysRoleEntity existingRole = sysRoleMapper.selectById(dto.getId());
             SysErrorEnum.INVALID_ID.assertNotNull(existingRole);
-            if (existingRole.isSuperAdmin() || existingRole.isTenantAdmin()) {
+            if (existingRole.isAdmin()) {
                 // 原来角色编码为SuperAdmin或Admin的,不能被改变
                 throw new BusinessException(SysErrorEnum.ROLE_VALUE_CANNOT_BE, existingRole.getValue());
             }
@@ -397,11 +412,7 @@ public class SysRoleService {
 
         List<SysRoleEntity> existingEntityList = sysRoleMapper.selectBatchIds(ids);
         for (SysRoleEntity item : existingEntityList) {
-            if (item.isSuperAdmin()) {
-                throw new BusinessException(SysErrorEnum.CANNOT_DELETE_SUPER_ADMIN_ROLE);
-            }
-
-            if (item.isTenantAdmin()) {
+            if (item.isAdmin()) {
                 throw new BusinessException(SysErrorEnum.CANNOT_DELETE_TENANT_ADMIN_ROLE);
             }
         }
@@ -429,11 +440,11 @@ public class SysRoleService {
 
         // 有且只有当前用户为超级管理员,才可以为租户管理员绑定菜单
         SysRoleEntity targetRole = sysRoleMapper.selectById(dto.getRoleId());
-        if (targetRole.isTenantAdmin() && !currentUser.isSuperAdmin()) {
+        if (!targetRole.isAdmin()) {
             throw new BusinessException(SysErrorEnum.CANNOT_BIND_MENUS_FOR_TENANT_ADMIN_ROLE);
         }
 
-        if (CollUtil.isNotEmpty(dto.getMenuIds()) && !currentUser.isSuperAdmin()) {
+        if (CollUtil.isNotEmpty(dto.getMenuIds()) && !currentUser.isAdmin()) {
             // 超级管理员之外的角色,都需要校验自身菜单范围是否满足输入值
             Set<Long> visibleMenuIds = sysRoleMenuRelationService.listMenuIdsByRoleIds(currentUser.getRelatedRoleIds());
             if (!CollUtil.containsAll(visibleMenuIds, dto.getMenuIds())) {

+ 26 - 27
src/main/java/com/jtzx/crm/module/sys/service/SysUserService.java

@@ -38,7 +38,6 @@ import org.springframework.transaction.annotation.Transactional;
 import java.math.BigInteger;
 import java.time.LocalDateTime;
 import java.util.*;
-import java.util.stream.Collectors;
 
 
 /**
@@ -199,15 +198,15 @@ public class SysUserService {
         // 不要直接提示“账号不存在”或“密码不正确”,避免撞库攻击
         SysUserEntity sysUserEntity = this.getUserByPin(dto.getUsername());
         if (sysUserEntity == null) {
-            throw new BusinessException(SysErrorEnum.INCORRECT_PIN_OR_PWD);
+            return null;
         }
 
         if (!PwdUtil.encrypt(dto.getPassword(), sysUserEntity.getSalt()).equals(sysUserEntity.getPwd())) {
-            throw new BusinessException(SysErrorEnum.INCORRECT_PIN_OR_PWD);
+            return null;
         }
 
         if (SysUserStatusEnum.BANNED == sysUserEntity.getStatus()) {
-            throw new BusinessException(SysErrorEnum.BANNED_USER);
+            return null;
         }
 
         /*
@@ -221,19 +220,19 @@ public class SysUserService {
 
         Map<Long, String> roleMap = sysRoleService.getRoleMapByUserId(sysUserBO.getId());
 
-        Map<Long, Set<String>> roleIdPermissionMap = sysMenuService.getRoleIdPermissionMap(roleMap.keySet());
+//        Map<Long, Set<String>> roleIdPermissionMap = sysMenuService.getRoleIdPermissionMap(roleMap.keySet());
 
         // 包装返回体;有的字段类型不一致, 单独转换
         SysUserLoginBO ret = new SysUserLoginBO();
         BeanUtil.copyProperties(sysUserBO, ret);
 
-        Set<String> permissions = roleIdPermissionMap.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
+//        Set<String> permissions = roleIdPermissionMap.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
 
         ret
                 .setRoleIds(new HashSet<>(roleMap.keySet()))
-                .setRoles(new ArrayList<>(roleMap.values()))
-                .setPermissions(permissions)
-                .setRoleIdPermissionMap(roleIdPermissionMap);
+                .setRoles(new ArrayList<>(roleMap.values()));
+//                .setPermissions(permissions)
+//                .setRoleIdPermissionMap(roleIdPermissionMap);
 
         return ret;
     }
@@ -513,7 +512,7 @@ public class SysUserService {
      */
     private void preUpdateCheck(Long specifiedUserId, @Nullable SysUserStatusEnum statusEnum) {
         UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer();
-        if (currentUser.isSuperAdmin()) {
+        if (currentUser.isAdmin()) {
             // 超级管理员除禁用自己外为所欲为
             if (statusEnum == SysUserStatusEnum.BANNED && Objects.equals(specifiedUserId, UserContextHolder.getUserId())) {
                 throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER);
@@ -528,11 +527,11 @@ public class SysUserService {
 
         // 目标是超级管理员or租户管理员时,均不能编辑
         UserRoleContainer specifiedUser = sysRoleService.getSpecifiedUserRoleContainer(specifiedUserId);
-        if (specifiedUser.isSuperAdmin() || specifiedUser.isTenantAdmin()) {
+        if (specifiedUser.isGroupManager()) {
             throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER);
         }
 
-        dataScopeCheck(Collections.singleton(specifiedUserId));
+//        dataScopeCheck(Collections.singleton(specifiedUserId));
         // 暂未实现角色层级,一律平级
     }
 
@@ -547,13 +546,13 @@ public class SysUserService {
 
         // 目标是超级管理员时,不能删除
         List<UserRoleContainer> specifiedUsers = ids.stream().map(sysRoleService::getSpecifiedUserRoleContainer).toList();
-        if (specifiedUsers.stream().anyMatch(UserRoleContainer::isSuperAdmin)) {
+        if (specifiedUsers.stream().anyMatch(UserRoleContainer::isAdmin)) {
             throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER);
         }
 
         // 只有超级管理员可以删租户管理员用户
         UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer();
-        if (specifiedUsers.stream().anyMatch(UserRoleContainer::isTenantAdmin) && !currentUser.isSuperAdmin()) {
+        if (!currentUser.isGroupManager()) {
             throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER);
         }
 
@@ -569,7 +568,7 @@ public class SysUserService {
         UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer();
         // 是否对自己操作
         boolean selfFlag = Objects.equals(dto.getUserId(), UserContextHolder.getUserId());
-        if (currentUser.isSuperAdmin()) {
+        if (currentUser.isAdmin()) {
             // 超级管理员不能去掉自己的超级管理员角色
             if (selfFlag && !CollUtil.contains(dto.getRoleIds(), SysConstant.SUPER_ADMIN_ROLE_ID)) {
                 throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER);
@@ -588,7 +587,7 @@ public class SysUserService {
 
         // 目标已经是超级管理员or租户管理员时,均不能绑定
         UserRoleContainer specifiedUser = sysRoleService.getSpecifiedUserRoleContainer(dto.getUserId());
-        if (specifiedUser.isSuperAdmin() || specifiedUser.isTenantAdmin()) {
+        if (specifiedUser.isAdmin()) {
             throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER);
         }
 
@@ -604,9 +603,9 @@ public class SysUserService {
      * 拆分子方法以降低Cognitive Complexity
      */
     private void currentUserNotSuperAdmin(AdminBindUserRoleRelationDTO dto, UserRoleContainer currentUser) {
-        if (CollUtil.isNotEmpty(dto.getRoleIds()) && !currentUser.isSuperAdmin()) {
+        if (CollUtil.isNotEmpty(dto.getRoleIds()) && !currentUser.isAdmin()) {
             boolean overRoles = !CollUtil.containsAll(currentUser.getRelatedRoleIds(), dto.getRoleIds());
-            if (overRoles && currentUser.isNotAnyAdmin()) {
+            if (overRoles && currentUser.isGroupManager()) {
                 // 普通用户超自身角色授予了;如果当前用户拥有新角色的所有菜单,那么也放行
                 Set<Long> grantedMenuIds = sysRoleMenuRelationService.listMenuIdsByRoleIds(currentUser.getRelatedRoleIds());
                 Set<Long> needMenuIds = sysRoleMenuRelationService.listMenuIdsByRoleIds(dto.getRoleIds());
@@ -615,15 +614,15 @@ public class SysUserService {
                 }
             }
 
-            if (currentUser.isTenantAdmin()) {
-                // 超自身权限,但作为租户管理员有额外情况
-                Set<Long> invisibleRoleIds = sysRoleService.determineInvisibleRoleIds();
-                // 除非超越了可见角色IDs授予 or 想要授予用户租户管理员角色,否则不管
-                invisibleRoleIds.addAll(currentUser.getRelatedRoleIds());
-                if (CollUtil.containsAny(invisibleRoleIds, dto.getRoleIds())) {
-                    throw new BusinessException(SysErrorEnum.BEYOND_AUTHORITY_BIND_ROLES);
-                }
-            }
+//            if (currentUser.isTenantAdmin()) {
+//                // 超自身权限,但作为租户管理员有额外情况
+//                Set<Long> invisibleRoleIds = sysRoleService.determineInvisibleRoleIds();
+//                // 除非超越了可见角色IDs授予 or 想要授予用户租户管理员角色,否则不管
+//                invisibleRoleIds.addAll(currentUser.getRelatedRoleIds());
+//                if (CollUtil.containsAny(invisibleRoleIds, dto.getRoleIds())) {
+//                    throw new BusinessException(SysErrorEnum.BEYOND_AUTHORITY_BIND_ROLES);
+//                }
+//            }
         }
     }
 

+ 2 - 1
src/main/resources/application-dev.yml

@@ -100,4 +100,5 @@ logging:
   level:
     # 指定日志级别,开发、测试环境建议为 DEBUG,生产环境建议为 INFO
     cc.uncarbon.framework: DEBUG
-    com.jtzx.com: DEBUG
+    com.jtzx.com: DEBUG
+    org.dromara.sms4j: DEBUG

+ 1 - 0
src/main/resources/application-prod.yml

@@ -97,3 +97,4 @@ logging:
     # 指定日志级别,开发、测试环境建议为 DEBUG,生产环境建议为 INFO
     cc.uncarbon.framework: INFO
     com.jtzx.com: INFO
+    org.dromara.sms4j: DEBUG

+ 2 - 1
src/main/resources/application-test.yml

@@ -96,4 +96,5 @@ logging:
   level:
     # 指定日志级别,开发、测试环境建议为 DEBUG,生产环境建议为 INFO
     cc.uncarbon.framework: DEBUG
-    com.jtzx.com: DEBUG
+    com.jtzx.com: DEBUG
+    org.dromara.sms4j: DEBUG

+ 10 - 7
src/main/resources/application.yml

@@ -62,7 +62,10 @@ helio:
       # SpringMVC 404
       - /error
       # C端登录接口
-      - /app/api/v1/auth/login
+      - /admin/api/v1/auth/accountlogin
+      - /admin/api/v1/auth/sms/smsCode
+      - /admin/api/v1/auth/smsCodeLogin
+      - /admin/api/v1/sms/captcha
   tenant:
     # 是否开启多租户,详情请见文档【进阶使用-多租户】
     enabled: false
@@ -138,8 +141,8 @@ dromara:
     aliyun-oss:
       - platform: aliyun-oss-1 # 存储平台标识
         enable-storage: true  # 启用存储
-        access-key: LTAI5t6sfxUSFPq1W3VAsd49
-        secret-key: VcDk6aOLEdWTu7yBa8tLFgo2j2FB79
+        access-key: LTAI5t6gL945yTHCP8LjwkKn
+        secret-key: VVv9as0Rab0RCFNnhGQ2U3AQEeGuu0
         end-point: oss-accelerate.aliyuncs.com
         bucket-name: jtzx001
         domain: https://jtzx001.oss-accelerate.aliyuncs.com/ # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/
@@ -157,18 +160,18 @@ sms:
       #厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
       supplier: alibaba
       #您的accessKey
-      access-key-id: LTAI5t6sfxUSFPq1W3VAsd49
+      access-key-id: LTAI5t6gL945yTHCP8LjwkKn
       #您的accessKeySecret
-      access-key-secret: VcDk6aOLEdWTu7yBa8tLFgo2j2FB79
+      access-key-secret: VVv9as0Rab0RCFNnhGQ2U3AQEeGuu0
       #您的短信签名
       signature: 北京京投助校科技
-      template-name: 验证码短信
+#      template-name: 验证码短信
       #模板ID(也叫templateCode) 非必须配置,如果使用sendMessage的快速发送需此配置
       template-id: SMS_318655743
       #配置标识名称 如果你使用的yml进行配置,则blends下层配置的就是这个,可为空,如果你使用的接口类配置,则需要设置值
       #需要注意的是,不同的配置之间config-id不能重复,否则会发生配置丢失的问题
       config-id: jtzx-crm
       #短信自动重试间隔时间  默认五秒
-      retry-interval: 5
+      retry-interval: 7
       # 短信重试次数,默认0次不重试,如果你需要短信重试则根据自己的需求修改值即可
       max-retries: 3