64
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -170,28 +170,28 @@ ps:核心功能已经实现,正在对接微信小程序中... | ||||
| ### 后端 | ||||
|  | ||||
| | 框架                                                                                         | 说明                   | 版本      | 学习指南                                                           | | ||||
| |---------------------------------------------------------------------------------------------|-----------------------|-----------|----------------------------------------------------------------| | ||||
| | [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架             | 2.6.10    | [文档](https://github.com/YunaiV/SpringBoot-Labs)                | | ||||
| | [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器             | 5.7      |                                                                | | ||||
| | [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件     | 1.2.11    | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | | ||||
| | [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包       | 3.5.2    | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         | | ||||
| | [Dynamic Datasource](https://dynamic-datasource.com/)                                       | 动态数据源               | 3.5.0    | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | | ||||
| | [Redis](https://redis.io/)                                                                  | key-value 数据库        | 5.0      |                                                                | | ||||
| | [Redisson](https://github.com/redisson/redisson)                                            | Redis 客户端            | 3.17.4   | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao)           | | ||||
| | [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架               | 5.3.20    | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao)               | | ||||
| | [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架         | 5.6.5    | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) | | ||||
| | [Hibernate Validator](https://github.com/hibernate/hibernate-validator)                     | 参数校验组件             | 6.2.3    | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao)      | | ||||
| | [Flowable](https://github.com/flowable/flowable-engine)                                     | 工作流引擎               | 6.7.0    | [文档](https://doc.iocoder.cn/bpm/)                                                     | | ||||
| | [Quartz](https://github.com/quartz-scheduler)                                               | 任务调度组件             | 2.3.2    | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao)             | | ||||
| | [Knife4j](https://gitee.com/xiaoym/knife4j)                                                 | Swagger 增强 UI 实现    | 3.0.3    | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao)         | | ||||
| | [Resilience4j](https://github.com/resilience4j/resilience4j)                                | 服务保障组件             | 1.7.1    | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao)    | | ||||
| | [SkyWalking](https://skywalking.apache.org/)                                                | 分布式应用追踪系统        | 8.5.0    | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao)      | | ||||
| | [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin)                       | Spring Boot 监控平台    | 2.6.7    | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao)           | | ||||
| | [Jackson](https://github.com/FasterXML/jackson)                                             | JSON 工具库             | 2.13.3   |                                                                | | ||||
| | [MapStruct](https://mapstruct.org/)                                                         | Java Bean 转换         | 1.4.1    | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao)       | | ||||
| | [Lombok](https://projectlombok.org/)                                                        | 消除冗长的 Java 代码     | 1.16.14  | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao)          | | ||||
| | [JUnit](https://junit.org/junit5/)                                                          | Java 单元测试框架        | 5.8.2    | -                                                              | | ||||
| | [Mockito](https://github.com/mockito/mockito)                                               | Java Mock 框架         | 4.0.0    | -                                                              | | ||||
| |---------------------------------------------------------------------------------------------|-----------------------|---------|----------------------------------------------------------------| | ||||
| | [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架             | 2.6.10  | [文档](https://github.com/YunaiV/SpringBoot-Labs)                | | ||||
| | [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器             | 5.7     |                                                                | | ||||
| | [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件     | 1.2.11  | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | | ||||
| | [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包       | 3.5.2   | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         | | ||||
| | [Dynamic Datasource](https://dynamic-datasource.com/)                                       | 动态数据源               | 3.5.0   | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | | ||||
| | [Redis](https://redis.io/)                                                                  | key-value 数据库        | 5.0     |                                                                | | ||||
| | [Redisson](https://github.com/redisson/redisson)                                            | Redis 客户端            | 3.17.4  | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao)           | | ||||
| | [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架               | 5.3.20  | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao)               | | ||||
| | [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架         | 5.6.5   | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) | | ||||
| | [Hibernate Validator](https://github.com/hibernate/hibernate-validator)                     | 参数校验组件             | 6.2.3   | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao)      | | ||||
| | [Flowable](https://github.com/flowable/flowable-engine)                                     | 工作流引擎               | 6.7.2   | [文档](https://doc.iocoder.cn/bpm/)                                                     | | ||||
| | [Quartz](https://github.com/quartz-scheduler)                                               | 任务调度组件             | 2.3.2   | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao)             | | ||||
| | [Knife4j](https://gitee.com/xiaoym/knife4j)                                                 | Swagger 增强 UI 实现    | 3.0.3   | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao)         | | ||||
| | [Resilience4j](https://github.com/resilience4j/resilience4j)                                | 服务保障组件             | 1.7.1   | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao)    | | ||||
| | [SkyWalking](https://skywalking.apache.org/)                                                | 分布式应用追踪系统        | 8.5.0   | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao)      | | ||||
| | [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin)                       | Spring Boot 监控平台    | 2.6.7   | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao)           | | ||||
| | [Jackson](https://github.com/FasterXML/jackson)                                             | JSON 工具库             | 2.13.3  |                                                                | | ||||
| | [MapStruct](https://mapstruct.org/)                                                         | Java Bean 转换         | 1.4.1   | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao)       | | ||||
| | [Lombok](https://projectlombok.org/)                                                        | 消除冗长的 Java 代码     | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao)          | | ||||
| | [JUnit](https://junit.org/junit5/)                                                          | Java 单元测试框架        | 5.8.2   | -                                                              | | ||||
| | [Mockito](https://github.com/mockito/mockito)                                               | Java Mock 框架         | 4.0.0   | -                                                              | | ||||
|  | ||||
| ### [管理后台 Vue2 前端](./yudao-ui-admin) | ||||
|  | ||||
| @@ -202,16 +202,16 @@ ps:核心功能已经实现,正在对接微信小程序中... | ||||
|  | ||||
| ### [管理后台 Vue3 前端](./yudao-ui-admin-vue3) | ||||
|  | ||||
| | 框架                                                                  | 说明               | 版本     | | ||||
| |----------------------------------------------------------------------|------------------|--------| | ||||
| | [Vue](https://staging-cn.vuejs.org/)                                 | Vue 框架           | 3.2.37 | | ||||
| | [Vite](https://cn.vitejs.dev//)                                      | 开发与构建工具          | 3.0.4  | | ||||
| | [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus     | 2.2.12 | | ||||
| | [TypeScript](https://www.typescriptlang.org/docs/)                   | TypeScript       | 4.7.4  | | ||||
| | [pinia](https://pinia.vuejs.org/)                                    | Vue 存储库 替代 vuex5 | 2.0.17 | | ||||
| | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化              | 9.2.0  | | ||||
| | [windicss](https://cn.windicss.org/)                                 | 下一代工具优先的 CSS 框架  | 3.5.6  | | ||||
| | [iconify](https://icon-sets.iconify.design/)                         | 在线图标库            | 2.2.1  | | ||||
| | 框架                                                                  | 说明              | 版本     | | ||||
| |----------------------------------------------------------------------|-----------------|--------| | ||||
| | [Vue](https://staging-cn.vuejs.org/)                                 | Vue 框架          | 3.2.37 | | ||||
| | [Vite](https://cn.vitejs.dev//)                                      | 开发与构建工具         | 3.0.4  | | ||||
| | [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus    | 2.2.12 | | ||||
| | [TypeScript](https://www.typescriptlang.org/docs/)                   | TypeScript      | 4.7.4  | | ||||
| | [pinia](https://pinia.vuejs.org/)                                    | vuex5           | 2.0.17 | | ||||
| | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化             | 9.2.2  | | ||||
| | [windicss](https://cn.windicss.org/)                                 | 下一代工具优先的 CSS 框架 | 3.5.6  | | ||||
| | [iconify](https://icon-sets.iconify.design/)                         | 在线图标库           | 2.2.1  | | ||||
|  | ||||
| ### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp) | ||||
|  | ||||
|   | ||||
							
								
								
									
										7
									
								
								sql/mysql/update.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| -- ---------------------------- | ||||
| -- 升级SQL文件,全新安装只需要执行ruoyi-vue-pro.sql文件即可 | ||||
| -- 1.6.2 ==> 1.6.3 | ||||
| -- ---------------------------- | ||||
| -- 积木报表菜单 | ||||
| INSERT INTO `system_menu` VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'chart', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0'); | ||||
| INSERT INTO `system_menu` VALUES (1282, '积木报表', '', 2, 1, 1281, 'jm-report', '#', 'visualization/jm/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-10 20:33:26', b'0'); | ||||
| @@ -41,14 +41,14 @@ | ||||
|         <jedis-mock.version>0.1.16</jedis-mock.version> | ||||
|         <mockito-inline.version>4.0.0</mockito-inline.version> | ||||
|         <!-- Bpm 工作流相关 --> | ||||
|         <flowable.version>6.7.0</flowable.version> | ||||
|         <flowable.version>6.7.2</flowable.version> | ||||
|         <!-- 工具类相关 --> | ||||
|         <jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version> | ||||
|         <lombok.version>1.18.20</lombok.version> | ||||
|         <mapstruct.version>1.4.1.Final</mapstruct.version> | ||||
|         <hutool.version>5.7.22</hutool.version> | ||||
|         <hutool.version>5.8.5</hutool.version> | ||||
|         <easyexcel.verion>3.1.1</easyexcel.verion> | ||||
|         <velocity.version>2.2</velocity.version> | ||||
|         <velocity.version>2.3</velocity.version> | ||||
|         <screw.version>1.0.5</screw.version> | ||||
| 		<fastjson.version>1.2.83</fastjson.version> | ||||
|         <guava.version>30.1.1-jre</guava.version> | ||||
| @@ -57,11 +57,12 @@ | ||||
|         <commons-net.version>3.8.0</commons-net.version> | ||||
|         <jsch.version>0.1.55</jsch.version> | ||||
|         <tika-core.version>2.4.1</tika-core.version> | ||||
|         <aj-captcha.version>1.3.0</aj-captcha.version> | ||||
|         <!-- 三方云服务相关 --> | ||||
|         <minio.version>8.2.2</minio.version> | ||||
|         <aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version> | ||||
|         <aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version> | ||||
|         <tencentcloud-sdk-java.version>3.1.471</tencentcloud-sdk-java.version> | ||||
|         <aliyun-java-sdk-core.version>4.6.0</aliyun-java-sdk-core.version> | ||||
|         <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version> | ||||
|         <tencentcloud-sdk-java.version>3.1.561</tencentcloud-sdk-java.version> | ||||
|         <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version> | ||||
|         <justauth.version>1.4.0</justauth.version> | ||||
|         <jimureport.version>1.5.2</jimureport.version> | ||||
| @@ -130,6 +131,11 @@ | ||||
|                 <artifactId>yudao-spring-boot-starter-biz-error-code</artifactId> | ||||
|                 <version>${revision}</version> | ||||
|             </dependency> | ||||
|             <dependency> | ||||
|                 <groupId>cn.iocoder.boot</groupId> | ||||
|                 <artifactId>yudao-spring-boot-starter-captcha</artifactId> | ||||
|                 <version>${revision}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- Spring 核心 --> | ||||
|             <dependency> | ||||
| @@ -452,6 +458,12 @@ | ||||
|                 <version>${tika-core.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <dependency> | ||||
|                 <groupId>com.anji-plus</groupId> | ||||
|                 <artifactId>spring-boot-starter-captcha</artifactId> | ||||
|                 <version>${aj-captcha.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <dependency> | ||||
|                 <groupId>org.apache.velocity</groupId> | ||||
|                 <artifactId>velocity-engine-core</artifactId> | ||||
|   | ||||
| @@ -39,6 +39,7 @@ | ||||
|         <module>yudao-spring-boot-starter-biz-error-code</module> | ||||
|  | ||||
|         <module>yudao-spring-boot-starter-flowable</module> | ||||
|         <module>yudao-spring-boot-starter-captcha</module> | ||||
|     </modules> | ||||
|  | ||||
|     <artifactId>yudao-framework</artifactId> | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package cn.iocoder.yudao.framework.common.util.collection; | ||||
|  | ||||
| import cn.hutool.core.collection.CollectionUtil; | ||||
| import cn.hutool.core.collection.IterUtil; | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
|  | ||||
| import java.util.Collection; | ||||
| @@ -44,7 +45,7 @@ public class ArrayUtils { | ||||
|         if (CollectionUtil.isEmpty(from)) { | ||||
|             return (T[]) (new Object[0]); | ||||
|         } | ||||
|         return ArrayUtil.toArray(from, (Class<T>) CollectionUtil.getElementType(from.iterator())); | ||||
|         return ArrayUtil.toArray(from, (Class<T>) IterUtil.getElementType(from.iterator())); | ||||
|     } | ||||
|  | ||||
|     public static <T> T get(T[] array, int index) { | ||||
|   | ||||
| @@ -17,16 +17,15 @@ import java.util.regex.Pattern; | ||||
|  */ | ||||
| public class ValidationUtils { | ||||
|  | ||||
|     private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[189]))\\d{8}$"); | ||||
|  | ||||
|     private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); | ||||
|  | ||||
|     private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*"); | ||||
|  | ||||
|     public static boolean isMobile(String mobile) { | ||||
|         if (StrUtil.length(mobile) != 11) { | ||||
|             return false; | ||||
|         } | ||||
|         // TODO 芋艿,后面完善手机校验 | ||||
|         return true; | ||||
|         return StringUtils.hasText(mobile) | ||||
|                 && PATTERN_MOBILE.matcher(mobile).matches(); | ||||
|     } | ||||
|  | ||||
|     public static boolean isURL(String url) { | ||||
|   | ||||
| @@ -27,7 +27,7 @@ public class DictFrameworkUtils { | ||||
|     /** | ||||
|      * 针对 {@link #getDictDataLabel(String, String)} 的缓存 | ||||
|      */ | ||||
|     private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> getDictDataCache = CacheUtils.buildAsyncReloadingCache( | ||||
|     private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( | ||||
|             Duration.ofMinutes(1L), // 过期时间 1 分钟 | ||||
|             new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() { | ||||
|  | ||||
| @@ -41,7 +41,7 @@ public class DictFrameworkUtils { | ||||
|     /** | ||||
|      * 针对 {@link #parseDictDataValue(String, String)} 的缓存 | ||||
|      */ | ||||
|     private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> parseDictDataCache = CacheUtils.buildAsyncReloadingCache( | ||||
|     private static final LoadingCache<KeyValue<String, String>, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( | ||||
|             Duration.ofMinutes(1L), // 过期时间 1 分钟 | ||||
|             new CacheLoader<KeyValue<String, String>, DictDataRespDTO>() { | ||||
|  | ||||
| @@ -59,12 +59,12 @@ public class DictFrameworkUtils { | ||||
|  | ||||
|     @SneakyThrows | ||||
|     public static String getDictDataLabel(String dictType, String value) { | ||||
|         return getDictDataCache.get(new KeyValue<>(dictType, value)).getLabel(); | ||||
|         return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel(); | ||||
|     } | ||||
|  | ||||
|     @SneakyThrows | ||||
|     public static String parseDictDataValue(String dictType, String label) { | ||||
|         return parseDictDataCache.get(new KeyValue<>(dictType, label)).getValue(); | ||||
|         return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -52,12 +52,18 @@ | ||||
|         <dependency> | ||||
|             <groupId>com.alipay.sdk</groupId> | ||||
|             <artifactId>alipay-sdk-java</artifactId> | ||||
|             <version>4.17.9.ALL</version> | ||||
|             <version>4.31.72.ALL</version> | ||||
|             <exclusions> | ||||
|                 <exclusion> | ||||
|                     <groupId>org.bouncycastle</groupId> | ||||
|                     <artifactId>bcprov-jdk15on</artifactId> | ||||
|                 </exclusion> | ||||
|             </exclusions> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>com.github.binarywang</groupId> | ||||
|             <artifactId>weixin-java-pay</artifactId> | ||||
|             <version>4.1.9.B</version> | ||||
|             <version>4.3.8.B</version> | ||||
|         </dependency> | ||||
|         <!-- TODO 芋艿:清理 --> | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package cn.iocoder.yudao.framework.pay.core.client.impl; | ||||
|  | ||||
| import cn.hutool.extra.validation.ValidationUtil; | ||||
| import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping; | ||||
| import cn.iocoder.yudao.framework.pay.core.client.PayClient; | ||||
| import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; | ||||
| @@ -10,6 +9,8 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO; | ||||
| import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
|  | ||||
| import javax.validation.Validation; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; | ||||
|  | ||||
| /** | ||||
| @@ -79,7 +80,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen | ||||
|  | ||||
|     @Override | ||||
|     public final PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { | ||||
|         ValidationUtil.validate(reqDTO); | ||||
|         Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO); | ||||
|         // 执行短信发送 | ||||
|         PayCommonResult<?> result; | ||||
|         try { | ||||
|   | ||||
| @@ -53,9 +53,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest { | ||||
|                 "lrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZ" + | ||||
|                 "ikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); | ||||
|  | ||||
|     // TODO @tina:= 前后要有空格哈 | ||||
|     @InjectMocks | ||||
|     AlipayQrPayClient client=new AlipayQrPayClient(10L,config); | ||||
|     AlipayQrPayClient client = new AlipayQrPayClient(10L,config); | ||||
|  | ||||
|     @Mock | ||||
|     private DefaultAlipayClient defaultAlipayClient; | ||||
|   | ||||
| @@ -35,12 +35,12 @@ | ||||
|         <dependency> | ||||
|             <groupId>com.github.binarywang</groupId> | ||||
|             <artifactId>wx-java-mp-spring-boot-starter</artifactId> | ||||
|             <version>4.3.4.B</version> | ||||
|             <version>4.3.8.B</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>com.github.binarywang</groupId> | ||||
|             <artifactId>wx-java-miniapp-spring-boot-starter</artifactId> | ||||
|             <version>4.3.4.B</version> | ||||
|             <version>4.3.8.B</version> | ||||
|         </dependency> | ||||
|         <!-- TODO 芋艿:清理 --> | ||||
|     </dependencies> | ||||
|   | ||||
							
								
								
									
										39
									
								
								yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,39 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||
|          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
|          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
|     <parent> | ||||
|         <groupId>cn.iocoder.boot</groupId> | ||||
|         <artifactId>yudao-framework</artifactId> | ||||
|         <version>${revision}</version> | ||||
|     </parent> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
|     <artifactId>yudao-spring-boot-starter-captcha</artifactId> | ||||
|     <packaging>jar</packaging> | ||||
|  | ||||
|     <name>${project.artifactId}</name> | ||||
|     <description>验证码拓展 | ||||
|         1. 基于 aj-captcha 实现滑块验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/ | ||||
|     </description> | ||||
|  | ||||
|     <dependencies> | ||||
|         <!-- Spring 核心 --> | ||||
|         <dependency> | ||||
|             <groupId>org.springframework.boot</groupId> | ||||
|             <artifactId>spring-boot-starter</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- DB 相关 --> | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.boot</groupId> | ||||
|             <artifactId>yudao-spring-boot-starter-redis</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- 验证码相关 --> | ||||
|         <dependency> | ||||
|             <groupId>com.anji-plus</groupId> | ||||
|             <artifactId>spring-boot-starter-captcha</artifactId> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
|  | ||||
| </project> | ||||
| @@ -0,0 +1,25 @@ | ||||
| package cn.iocoder.yudao.framework.captcha.config; | ||||
|  | ||||
| import cn.hutool.core.util.ClassUtil; | ||||
| import cn.iocoder.yudao.framework.captcha.core.enums.CaptchaRedisKeyConstants; | ||||
| import cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl; | ||||
| import com.anji.captcha.service.CaptchaCacheService; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
| import org.springframework.data.redis.core.StringRedisTemplate; | ||||
|  | ||||
| @Configuration | ||||
| public class YudaoCaptchaConfiguration { | ||||
|  | ||||
|     static { | ||||
|         // 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到 | ||||
|         // 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举 | ||||
|         ClassUtil.loadClass(CaptchaRedisKeyConstants.class.getName()); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     public CaptchaCacheService captchaCacheService(StringRedisTemplate stringRedisTemplate) { | ||||
|         return new RedisCaptchaServiceImpl(stringRedisTemplate); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,28 @@ | ||||
| package cn.iocoder.yudao.framework.captcha.core.enums; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; | ||||
| import com.anji.captcha.model.vo.PointVO; | ||||
| import org.redisson.api.RLock; | ||||
|  | ||||
| import java.time.Duration; | ||||
| import java.util.Map; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH; | ||||
| import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING; | ||||
|  | ||||
| /** | ||||
|  * 验证码 Redis Key 枚举类 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| public interface CaptchaRedisKeyConstants { | ||||
|  | ||||
|     RedisKeyDefine AJ_CAPTCHA_REQ_LIMIT = new RedisKeyDefine("验证码的请求限流", | ||||
|             "AJ.CAPTCHA.REQ.LIMIT-%s-%s", | ||||
|             STRING, Integer.class, Duration.ofSeconds(60)); // 例如说:验证失败 5 次,get 接口锁定 | ||||
|  | ||||
|     RedisKeyDefine AJ_CAPTCHA_RUNNING = new RedisKeyDefine("验证码的坐标", | ||||
|             "RUNNING:CAPTCHA:%s", // AbstractCaptchaService.REDIS_CAPTCHA_KEY | ||||
|             STRING, PointVO.class, Duration.ofSeconds(120)); // {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5} | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,54 @@ | ||||
| package cn.iocoder.yudao.framework.captcha.core.service; | ||||
|  | ||||
| import com.anji.captcha.service.CaptchaCacheService; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.NoArgsConstructor; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.data.redis.core.StringRedisTemplate; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.util.concurrent.TimeUnit; | ||||
|  | ||||
| /** | ||||
|  * 基于 Redis 实现验证码的存储 | ||||
|  * | ||||
|  * @author 星语 | ||||
|  */ | ||||
| @NoArgsConstructor // 保证 aj-captcha 的 SPI 创建 | ||||
| @AllArgsConstructor | ||||
| public class RedisCaptchaServiceImpl implements CaptchaCacheService { | ||||
|  | ||||
|     @Resource // 保证 aj-captcha 的 SPI 创建时的注入 | ||||
|     private StringRedisTemplate stringRedisTemplate; | ||||
|  | ||||
|     @Override | ||||
|     public String type() { | ||||
|         return "redis"; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void set(String key, String value, long expiresInSeconds) { | ||||
|         stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean exists(String key) { | ||||
|         return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void delete(String key) { | ||||
|         stringRedisTemplate.delete(key); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String get(String key) { | ||||
|         return stringRedisTemplate.opsForValue().get(key); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Long increment(String key, long val) { | ||||
|         return stringRedisTemplate.opsForValue().increment(key,val); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| /** | ||||
|  * 验证码拓展 | ||||
|  * 1. 基于 aj-captcha 实现滑块验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/ | ||||
|  * | ||||
|  * @author 星语 | ||||
|  */ | ||||
| package cn.iocoder.yudao.framework.captcha; | ||||
| @@ -0,0 +1 @@ | ||||
| cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl | ||||
| @@ -0,0 +1,2 @@ | ||||
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ | ||||
|   cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration | ||||
| After Width: | Height: | Size: 17 KiB | 
| After Width: | Height: | Size: 28 KiB | 
| After Width: | Height: | Size: 25 KiB | 
| After Width: | Height: | Size: 27 KiB | 
| After Width: | Height: | Size: 24 KiB | 
| After Width: | Height: | Size: 19 KiB | 
| After Width: | Height: | Size: 21 KiB | 
| After Width: | Height: | Size: 30 KiB | 
| After Width: | Height: | Size: 26 KiB | 
| After Width: | Height: | Size: 2.1 KiB | 
| After Width: | Height: | Size: 1.6 KiB | 
| After Width: | Height: | Size: 1.6 KiB | 
| After Width: | Height: | Size: 1.6 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 1.6 KiB | 
| After Width: | Height: | Size: 1.6 KiB | 
| After Width: | Height: | Size: 2.2 KiB | 
| After Width: | Height: | Size: 2.2 KiB | 
| After Width: | Height: | Size: 2.2 KiB | 
| After Width: | Height: | Size: 24 KiB | 
| After Width: | Height: | Size: 27 KiB | 
| After Width: | Height: | Size: 28 KiB | 
| After Width: | Height: | Size: 23 KiB | 
| After Width: | Height: | Size: 25 KiB | 
| After Width: | Height: | Size: 16 KiB | 
| After Width: | Height: | Size: 23 KiB | 
| After Width: | Height: | Size: 26 KiB | 
| After Width: | Height: | Size: 27 KiB | 
| After Width: | Height: | Size: 29 KiB | 
| @@ -16,11 +16,11 @@ import java.util.Set; | ||||
|  */ | ||||
| public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler<Object> { | ||||
|  | ||||
|     private static final TypeReference<Set<Long>> typeReference = new TypeReference<Set<Long>>(){}; | ||||
|     private static final TypeReference<Set<Long>> TYPE_REFERENCE = new TypeReference<Set<Long>>(){}; | ||||
|  | ||||
|     @Override | ||||
|     protected Object parse(String json) { | ||||
|         return JsonUtils.parseObject(json, typeReference); | ||||
|         return JsonUtils.parseObject(json, TYPE_REFERENCE); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -11,18 +11,18 @@ public class RedisKeyRegistry { | ||||
|     /** | ||||
|      * Redis RedisKeyDefine 数组 | ||||
|      */ | ||||
|     private static final List<RedisKeyDefine> defines = new ArrayList<>(); | ||||
|     private static final List<RedisKeyDefine> DEFINES = new ArrayList<>(); | ||||
|  | ||||
|     public static void add(RedisKeyDefine define) { | ||||
|         defines.add(define); | ||||
|         DEFINES.add(define); | ||||
|     } | ||||
|  | ||||
|     public static List<RedisKeyDefine> list() { | ||||
|         return defines; | ||||
|         return DEFINES; | ||||
|     } | ||||
|  | ||||
|     public static int size() { | ||||
|         return defines.size(); | ||||
|         return DEFINES.size(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -81,7 +81,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap | ||||
|  | ||||
|     /** | ||||
|      * 配置 URL 的安全配置 | ||||
|      * | ||||
|      * <p> | ||||
|      * anyRequest          |   匹配所有请求路径 | ||||
|      * access              |   SpringEl表达式结果为true时可以访问 | ||||
|      * anonymous           |   匿名可以访问 | ||||
| @@ -109,8 +109,8 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap | ||||
|                 .headers().frameOptions().disable().and() | ||||
|                 // 一堆自定义的 Spring Security 处理器 | ||||
|                 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) | ||||
|                     .accessDeniedHandler(accessDeniedHandler); | ||||
|                 // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 | ||||
|                 .accessDeniedHandler(accessDeniedHandler); | ||||
|         // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 | ||||
|  | ||||
|         // 获得 @PermitAll 带来的 URL 列表,免登录 | ||||
|         Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations(); | ||||
| @@ -118,23 +118,25 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap | ||||
|         httpSecurity | ||||
|                 // ①:全局共享规则 | ||||
|                 .authorizeRequests() | ||||
|                     // 1.1 静态资源,可匿名访问 | ||||
|                     .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() | ||||
|                     // 1.2 设置 @PermitAll 无需认证 | ||||
|                     .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() | ||||
|                     .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() | ||||
|                     .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() | ||||
|                     .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() | ||||
|                     // 1.3 基于 yudao.security.permit-all-urls 无需认证 | ||||
|                     .antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() | ||||
|                     // 1.4 设置 App API 无需认证 | ||||
|                     .antMatchers(buildAppApi("/**")).permitAll() | ||||
|                 // 1.1 静态资源,可匿名访问 | ||||
|                 .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() | ||||
|                 // 1.2 设置 @PermitAll 无需认证 | ||||
|                 .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() | ||||
|                 .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() | ||||
|                 .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() | ||||
|                 .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() | ||||
|                 // 1.3 基于 yudao.security.permit-all-urls 无需认证 | ||||
|                 .antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() | ||||
|                 // 1.4 设置 App API 无需认证 | ||||
|                 .antMatchers(buildAppApi("/**")).permitAll() | ||||
|                 // 1.5 验证码captcha 允许匿名访问 | ||||
|                 .antMatchers("/captcha/get", "/captcha/check").permitAll() | ||||
|                 // ②:每个项目的自定义规则 | ||||
|                 .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 | ||||
|                         authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) | ||||
|                 // ③:兜底规则,必须认证 | ||||
|                 .authorizeRequests() | ||||
|                     .anyRequest().authenticated() | ||||
|                 .anyRequest().authenticated() | ||||
|         ; | ||||
|  | ||||
|         // 添加 Token Filter | ||||
|   | ||||
| @@ -17,19 +17,19 @@ public class TransmittableThreadLocalSecurityContextHolderStrategy implements Se | ||||
|     /** | ||||
|      * 使用 TransmittableThreadLocal 作为上下文 | ||||
|      */ | ||||
|     private static final ThreadLocal<SecurityContext> contextHolder = new TransmittableThreadLocal<>(); | ||||
|     private static final ThreadLocal<SecurityContext> CONTEXT_HOLDER = new TransmittableThreadLocal<>(); | ||||
|  | ||||
|     @Override | ||||
|     public void clearContext() { | ||||
|         contextHolder.remove(); | ||||
|         CONTEXT_HOLDER.remove(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public SecurityContext getContext() { | ||||
|         SecurityContext ctx = contextHolder.get(); | ||||
|         SecurityContext ctx = CONTEXT_HOLDER.get(); | ||||
|         if (ctx == null) { | ||||
|             ctx = createEmptyContext(); | ||||
|             contextHolder.set(ctx); | ||||
|             CONTEXT_HOLDER.set(ctx); | ||||
|         } | ||||
|         return ctx; | ||||
|     } | ||||
| @@ -37,7 +37,7 @@ public class TransmittableThreadLocalSecurityContextHolderStrategy implements Se | ||||
|     @Override | ||||
|     public void setContext(SecurityContext context) { | ||||
|         Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); | ||||
|         contextHolder.set(context); | ||||
|         CONTEXT_HOLDER.set(context); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|   | ||||
| @@ -57,7 +57,7 @@ public class BpmMessageServiceImpl implements BpmMessageService { | ||||
|         templateParams.put("taskName", reqDTO.getTaskName()); | ||||
|         templateParams.put("startUserNickname", reqDTO.getStartUserNickname()); | ||||
|         templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); | ||||
|         smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(), | ||||
|         smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(), | ||||
|                 BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams)); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -31,7 +31,7 @@ public class CodegenBuilder { | ||||
|      * 字段名与 {@link CodegenColumnListConditionEnum} 的默认映射 | ||||
|      * 注意,字段的匹配以后缀的方式 | ||||
|      */ | ||||
|     private static final Map<String, CodegenColumnListConditionEnum> columnListOperationConditionMappings = | ||||
|     private static final Map<String, CodegenColumnListConditionEnum> COLUMN_LIST_OPERATION_CONDITION_MAPPINGS = | ||||
|             MapUtil.<String, CodegenColumnListConditionEnum>builder() | ||||
|                     .put("name", CodegenColumnListConditionEnum.LIKE) | ||||
|                     .put("time", CodegenColumnListConditionEnum.BETWEEN) | ||||
| @@ -42,7 +42,7 @@ public class CodegenBuilder { | ||||
|      * 字段名与 {@link CodegenColumnHtmlTypeEnum} 的默认映射 | ||||
|      * 注意,字段的匹配以后缀的方式 | ||||
|      */ | ||||
|     private static final Map<String, CodegenColumnHtmlTypeEnum> columnHtmlTypeMappings = | ||||
|     private static final Map<String, CodegenColumnHtmlTypeEnum> COLUMN_HTML_TYPE_MAPPINGS = | ||||
|             MapUtil.<String, CodegenColumnHtmlTypeEnum>builder() | ||||
|                     .put("status", CodegenColumnHtmlTypeEnum.RADIO) | ||||
|                     .put("sex", CodegenColumnHtmlTypeEnum.RADIO) | ||||
| @@ -143,7 +143,7 @@ public class CodegenBuilder { | ||||
|         column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) | ||||
|                 && !column.getPrimaryKey()); // 对于主键,列表过滤不需要传递 | ||||
|         // 处理 listOperationCondition 字段 | ||||
|         columnListOperationConditionMappings.entrySet().stream() | ||||
|         COLUMN_LIST_OPERATION_CONDITION_MAPPINGS.entrySet().stream() | ||||
|                 .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) | ||||
|                 .findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition())); | ||||
|         if (column.getListOperationCondition() == null) { | ||||
| @@ -155,7 +155,7 @@ public class CodegenBuilder { | ||||
|  | ||||
|     private void processColumnUI(CodegenColumnDO column) { | ||||
|         // 基于后缀进行匹配 | ||||
|         columnHtmlTypeMappings.entrySet().stream() | ||||
|         COLUMN_HTML_TYPE_MAPPINGS.entrySet().stream() | ||||
|                 .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) | ||||
|                 .findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType())); | ||||
|         // 如果是 Boolean 类型时,设置为 radio 类型. | ||||
|   | ||||
| @@ -12,8 +12,7 @@ public interface ErrorCodeConstants { | ||||
|     // ========== AUTH 模块 1002000000 ========== | ||||
|     ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确"); | ||||
|     ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用"); | ||||
|     ErrorCode AUTH_LOGIN_CAPTCHA_NOT_FOUND = new ErrorCode(1002000003, "验证码不存在"); | ||||
|     ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确"); | ||||
|     ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确,原因:{}"); | ||||
|     ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定"); | ||||
|     ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1002000006, "Token 已经过期"); | ||||
|     ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1002000007, "手机号不存在"); | ||||
|   | ||||
| @@ -97,6 +97,11 @@ | ||||
|             <artifactId>yudao-spring-boot-starter-excel</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>cn.iocoder.boot</groupId> | ||||
|             <artifactId>yudao-spring-boot-starter-captcha</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|     </dependencies> | ||||
|  | ||||
| </project> | ||||
|   | ||||
| @@ -55,7 +55,6 @@ public class AuthController { | ||||
|     private PermissionService permissionService; | ||||
|     @Resource | ||||
|     private SocialUserService socialUserService; | ||||
|  | ||||
|     @Resource | ||||
|     private SecurityProperties securityProperties; | ||||
|  | ||||
|   | ||||
| @@ -35,13 +35,11 @@ public class AuthLoginReqVO { | ||||
|  | ||||
|     // ========== 图片验证码相关 ========== | ||||
|  | ||||
|     @ApiModelProperty(value = "验证码", required = true, example = "1024", notes = "验证码开启时,需要传递") | ||||
|     @ApiModelProperty(value = "验证码", required = true, | ||||
|             example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==", | ||||
|             notes = "验证码开启时,需要传递") | ||||
|     @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) | ||||
|     private String code; | ||||
|  | ||||
|     @ApiModelProperty(value = "验证码的唯一标识", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62", notes = "验证码开启时,需要传递") | ||||
|     @NotEmpty(message = "唯一标识不能为空", groups = CodeEnableGroup.class) | ||||
|     private String uuid; | ||||
|     private String captchaVerification; | ||||
|  | ||||
|     // ========== 绑定社交登录时,需要传递如下参数 ========== | ||||
|  | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| ### 请求 /captcha/get-image 接口 => 成功 | ||||
| GET {{baseUrl}}/system/captcha/get-image | ||||
| tenant-id: {{adminTenentId}} | ||||
| @@ -1,32 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.controller.admin.common; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
| import cn.iocoder.yudao.module.system.service.common.CaptchaService; | ||||
| import io.swagger.annotations.Api; | ||||
| import io.swagger.annotations.ApiOperation; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import javax.annotation.security.PermitAll; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||||
|  | ||||
| @Api(tags = "管理后台 - 验证码") | ||||
| @RestController | ||||
| @RequestMapping("/system/captcha") | ||||
| public class CaptchaController { | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaService captchaService; | ||||
|  | ||||
|     @GetMapping("/get-image") | ||||
|     @PermitAll | ||||
|     @ApiOperation("生成图片验证码") | ||||
|     public CommonResult<CaptchaImageRespVO> getCaptchaImage() { | ||||
|         return success(captchaService.getCaptchaImage()); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,27 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.controller.admin.common.vo; | ||||
|  | ||||
| import io.swagger.annotations.ApiModel; | ||||
| import io.swagger.annotations.ApiModelProperty; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Builder; | ||||
| import lombok.Data; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| @ApiModel("管理后台 - 验证码图片 Response VO") | ||||
| @Data | ||||
| @Builder | ||||
| @NoArgsConstructor | ||||
| @AllArgsConstructor | ||||
| public class CaptchaImageRespVO { | ||||
|  | ||||
|     @ApiModelProperty(value = "是否开启", required = true, example = "true", notes = "如果为 false,则关闭验证码功能") | ||||
|     private Boolean enable; | ||||
|  | ||||
|     @ApiModelProperty(value = "uuid", example = "1b3b7d00-83a8-4638-9e37-d67011855968", | ||||
|             notes = "enable = true 时,非空!通过该 uuid 作为该验证码的标识") | ||||
|     private String uuid; | ||||
|  | ||||
|     @ApiModelProperty(value = "图片", notes = "enable = true 时,非空!验证码的图片内容,使用 Base64 编码") | ||||
|     private String img; | ||||
|  | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.convert.common; | ||||
|  | ||||
| import cn.hutool.captcha.AbstractCaptcha; | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
| import org.mapstruct.Mapper; | ||||
| import org.mapstruct.factory.Mappers; | ||||
|  | ||||
| @Mapper | ||||
| public interface CaptchaConvert { | ||||
|  | ||||
|     CaptchaConvert INSTANCE = Mappers.getMapper(CaptchaConvert.class); | ||||
|  | ||||
|     default CaptchaImageRespVO convert(String uuid, AbstractCaptcha captcha) { | ||||
|         return CaptchaImageRespVO.builder().uuid(uuid).img(captcha.getImageBase64()).build(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.framework.captcha.config; | ||||
|  | ||||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||||
| import org.springframework.context.annotation.Configuration; | ||||
|  | ||||
| @Configuration | ||||
| @EnableConfigurationProperties(CaptchaProperties.class) | ||||
| public class CaptchaConfig { | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.framework.captcha.config; | ||||
|  | ||||
| import lombok.Data; | ||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
|  | ||||
| import javax.validation.constraints.NotNull; | ||||
| import java.time.Duration; | ||||
|  | ||||
| @ConfigurationProperties(prefix = "yudao.captcha") | ||||
| @Validated | ||||
| @Data | ||||
| public class CaptchaProperties { | ||||
|  | ||||
|     private static final Boolean ENABLE_DEFAULT = true; | ||||
|  | ||||
|     /** | ||||
|      * 是否开启 | ||||
|      * 注意,这里仅仅是后端 Server 是否校验,暂时不控制前端的逻辑 | ||||
|      */ | ||||
|     private Boolean enable = ENABLE_DEFAULT; | ||||
|     /** | ||||
|      * 验证码的过期时间 | ||||
|      */ | ||||
|     @NotNull(message = "验证码的过期时间不为空") | ||||
|     private Duration timeout; | ||||
|     /** | ||||
|      * 验证码的高度 | ||||
|      */ | ||||
|     @NotNull(message = "验证码的高度不能为空") | ||||
|     private Integer height; | ||||
|     /** | ||||
|      * 验证码的宽度 | ||||
|      */ | ||||
|     @NotNull(message = "验证码的宽度不能为空") | ||||
|     private Integer width; | ||||
|  | ||||
| } | ||||
| @@ -1,4 +0,0 @@ | ||||
| /** | ||||
|  * 基于 Hutool captcha 库,实现验证码功能 | ||||
|  */ | ||||
| package cn.iocoder.yudao.module.system.framework.captcha; | ||||
| @@ -17,14 +17,17 @@ import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; | ||||
| import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; | ||||
| import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants; | ||||
| import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; | ||||
| import cn.iocoder.yudao.module.system.service.common.CaptchaService; | ||||
| import cn.iocoder.yudao.module.system.service.logger.LoginLogService; | ||||
| import cn.iocoder.yudao.module.system.service.member.MemberService; | ||||
| import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; | ||||
| import cn.iocoder.yudao.module.system.service.social.SocialUserService; | ||||
| import cn.iocoder.yudao.module.system.service.user.AdminUserService; | ||||
| import com.anji.captcha.model.common.ResponseModel; | ||||
| import com.anji.captcha.model.vo.CaptchaVO; | ||||
| import com.anji.captcha.service.CaptchaService; | ||||
| import com.google.common.annotations.VisibleForTesting; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| @@ -47,8 +50,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|     @Resource | ||||
|     private AdminUserService userService; | ||||
|     @Resource | ||||
|     private CaptchaService captchaService; | ||||
|     @Resource | ||||
|     private LoginLogService loginLogService; | ||||
|     @Resource | ||||
|     private OAuth2TokenService oauth2TokenService; | ||||
| @@ -56,13 +57,19 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|     private SocialUserService socialUserService; | ||||
|     @Resource | ||||
|     private MemberService memberService; | ||||
|  | ||||
|     @Resource | ||||
|     private Validator validator; | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaService captchaService; | ||||
|     @Resource | ||||
|     private SmsCodeApi smsCodeApi; | ||||
|  | ||||
|     /** | ||||
|      * 验证码的开关,默认为 true | ||||
|      */ | ||||
|     @Value("${yudao.captcha.enable:true}") | ||||
|     private Boolean captchaEnable; | ||||
|  | ||||
|     @Override | ||||
|     public AdminUserDO authenticate(String username, String password) { | ||||
|         final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; | ||||
| @@ -86,7 +93,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|  | ||||
|     @Override | ||||
|     public AuthLoginRespVO login(AuthLoginReqVO reqVO) { | ||||
|         // 判断验证码是否正确 | ||||
|         // 校验验证码 | ||||
|         verifyCaptcha(reqVO); | ||||
|  | ||||
|         // 使用账号密码,进行登录 | ||||
| @@ -97,7 +104,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|             socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), | ||||
|                     reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); | ||||
|         } | ||||
|  | ||||
|         // 创建 Token 令牌,记录登录日志 | ||||
|         return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); | ||||
|     } | ||||
| @@ -127,32 +133,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|         return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); | ||||
|     } | ||||
|  | ||||
|     @VisibleForTesting | ||||
|     void verifyCaptcha(AuthLoginReqVO reqVO) { | ||||
|         // 如果验证码关闭,则不进行校验 | ||||
|         if (!captchaService.isCaptchaEnable()) { | ||||
|             return; | ||||
|         } | ||||
|         // 校验验证码 | ||||
|         ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class); | ||||
|         // 验证码不存在 | ||||
|         final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; | ||||
|         String code = captchaService.getCaptchaCode(reqVO.getUuid()); | ||||
|         if (code == null) { | ||||
|             // 创建登录失败日志(验证码不存在) | ||||
|             createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_NOT_FOUND); | ||||
|             throw exception(AUTH_LOGIN_CAPTCHA_NOT_FOUND); | ||||
|         } | ||||
|         // 验证码不正确 | ||||
|         if (!code.equals(reqVO.getCode())) { | ||||
|             // 创建登录失败日志(验证码不正确) | ||||
|             createLoginLog(null, reqVO.getUsername(), logTypeEnum, LoginResultEnum.CAPTCHA_CODE_ERROR); | ||||
|             throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR); | ||||
|         } | ||||
|         // 正确,所以要删除下验证码 | ||||
|         captchaService.deleteCaptchaCode(reqVO.getUuid()); | ||||
|     } | ||||
|  | ||||
|     private void createLoginLog(Long userId, String username, | ||||
|                                 LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { | ||||
|         // 插入登录日志 | ||||
| @@ -197,6 +177,25 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||||
|         return AuthConvert.INSTANCE.convert(accessTokenDO); | ||||
|     } | ||||
|  | ||||
|     @VisibleForTesting | ||||
|     void verifyCaptcha(AuthLoginReqVO reqVO) { | ||||
|         // 如果验证码关闭,则不进行校验 | ||||
|         if (!captchaEnable) { | ||||
|             return; | ||||
|         } | ||||
|         // 校验验证码 | ||||
|         ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class); | ||||
|         CaptchaVO captchaVO = new CaptchaVO(); | ||||
|         captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification()); | ||||
|         ResponseModel response = captchaService.verification(captchaVO); | ||||
|         // 验证不通过 | ||||
|         if (!response.isSuccess()) { | ||||
|             // 创建登录失败日志(验证码不正确) | ||||
|             createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR); | ||||
|             throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { | ||||
|         // 插入登陆日志 | ||||
|         createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); | ||||
|   | ||||
| @@ -1,39 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.service.common; | ||||
|  | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
|  | ||||
| /** | ||||
|  * 验证码 Service 接口 | ||||
|  */ | ||||
| public interface CaptchaService { | ||||
|  | ||||
|     /** | ||||
|      * 获得验证码图片 | ||||
|      * | ||||
|      * @return 验证码图片 | ||||
|      */ | ||||
|     CaptchaImageRespVO getCaptchaImage(); | ||||
|  | ||||
|     /** | ||||
|      * 是否开启图片验证码 | ||||
|      * | ||||
|      * @return 是否 | ||||
|      */ | ||||
|     Boolean isCaptchaEnable(); | ||||
|  | ||||
|     /** | ||||
|      * 获得 uuid 对应的验证码 | ||||
|      * | ||||
|      * @param uuid 验证码编号 | ||||
|      * @return 验证码 | ||||
|      */ | ||||
|     String getCaptchaCode(String uuid); | ||||
|  | ||||
|     /** | ||||
|      * 删除 uuid 对应的验证码 | ||||
|      * | ||||
|      * @param uuid 验证码编号 | ||||
|      */ | ||||
|     void deleteCaptchaCode(String uuid); | ||||
|  | ||||
| } | ||||
| @@ -1,65 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.service.common; | ||||
|  | ||||
| import cn.hutool.captcha.CaptchaUtil; | ||||
| import cn.hutool.captcha.CircleCaptcha; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import cn.iocoder.yudao.module.system.convert.common.CaptchaConvert; | ||||
| import cn.iocoder.yudao.module.system.framework.captcha.config.CaptchaProperties; | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
| import cn.iocoder.yudao.module.system.dal.redis.common.CaptchaRedisDAO; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
|  | ||||
| /** | ||||
|  * 验证码 Service 实现类 | ||||
|  */ | ||||
| @Service | ||||
| public class CaptchaServiceImpl implements CaptchaService { | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaProperties captchaProperties; | ||||
|  | ||||
|     /** | ||||
|      * 验证码是否开关 | ||||
|      * | ||||
|      * 虽然 {@link CaptchaProperties#getEnable()} 有该属性,但是 Apollo 在 Spring Boot 下无法刷新 @ConfigurationProperties 注解, | ||||
|      * 所以暂时只能这么处理~ | ||||
|      */ | ||||
|     @Value("${yudao.captcha.enable}") | ||||
|     private Boolean enable; | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaRedisDAO captchaRedisDAO; | ||||
|  | ||||
|     @Override | ||||
|     public CaptchaImageRespVO getCaptchaImage() { | ||||
|         if (!Boolean.TRUE.equals(enable)) { | ||||
|             return CaptchaImageRespVO.builder().enable(enable).build(); | ||||
|         } | ||||
|         // 生成验证码 | ||||
|         CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(captchaProperties.getWidth(), captchaProperties.getHeight()); | ||||
|         // 缓存到 Redis 中 | ||||
|         String uuid = IdUtil.fastSimpleUUID(); | ||||
|         captchaRedisDAO.set(uuid, captcha.getCode(), captchaProperties.getTimeout()); | ||||
|         // 返回 | ||||
|         return CaptchaConvert.INSTANCE.convert(uuid, captcha).setEnable(enable); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Boolean isCaptchaEnable() { | ||||
|         return enable; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getCaptchaCode(String uuid) { | ||||
|         return captchaRedisDAO.get(uuid); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void deleteCaptchaCode(String uuid) { | ||||
|         captchaRedisDAO.delete(uuid); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -11,13 +11,12 @@ import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; | ||||
| import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; | ||||
| import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; | ||||
| import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; | ||||
| import cn.iocoder.yudao.module.system.service.common.CaptchaService; | ||||
| import cn.iocoder.yudao.module.system.service.logger.LoginLogService; | ||||
| import cn.iocoder.yudao.module.system.service.member.MemberService; | ||||
| import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; | ||||
| import cn.iocoder.yudao.module.system.service.social.SocialUserService; | ||||
| import cn.iocoder.yudao.module.system.service.user.AdminUserService; | ||||
| import org.junit.jupiter.api.BeforeEach; | ||||
| import com.anji.captcha.service.CaptchaService; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.springframework.boot.test.mock.mockito.MockBean; | ||||
| import org.springframework.context.annotation.Import; | ||||
| @@ -57,11 +56,6 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { | ||||
|     @MockBean | ||||
|     private Validator validator; | ||||
|  | ||||
|     @BeforeEach | ||||
|     public void setUp() { | ||||
|         when(captchaService.isCaptchaEnable()).thenReturn(true); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testAuthenticate_success() { | ||||
|         // 准备参数 | ||||
| @@ -138,82 +132,82 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testCaptcha_success() { | ||||
|         // 准备参数 | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
| //    @Test | ||||
| //    public void testCaptcha_success() { | ||||
| //        // 准备参数 | ||||
| //        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
| // | ||||
| //        // mock 验证码正确 | ||||
| //        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); | ||||
| // | ||||
| //        // 调用 | ||||
| //        authService.verifyCaptcha(reqVO); | ||||
| //        // 断言 | ||||
| //        verify(captchaService).deleteCaptchaCode(reqVO.getUuid()); | ||||
| //    } | ||||
| // | ||||
| //    @Test | ||||
| //    public void testCaptcha_notFound() { | ||||
| //        // 准备参数 | ||||
| //        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
| // | ||||
| //        // 调用, 并断言异常 | ||||
| //        assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND); | ||||
| //        // 校验调用参数 | ||||
| //        verify(loginLogService, times(1)).createLoginLog( | ||||
| //            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
| //                    && o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult())) | ||||
| //        ); | ||||
| //    } | ||||
|  | ||||
|         // mock 验证码正确 | ||||
|         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); | ||||
| //    @Test | ||||
| //    public void testCaptcha_codeError() { | ||||
| //        // 准备参数 | ||||
| //        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
| // | ||||
| //        // mock 验证码不正确 | ||||
| //        String code = randomString(); | ||||
| //        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code); | ||||
| // | ||||
| //        // 调用, 并断言异常 | ||||
| //        assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR); | ||||
| //        // 校验调用参数 | ||||
| //        verify(loginLogService).createLoginLog( | ||||
| //            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
| //                    && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult())) | ||||
| //        ); | ||||
| //    } | ||||
|  | ||||
|         // 调用 | ||||
|         authService.verifyCaptcha(reqVO); | ||||
|         // 断言 | ||||
|         verify(captchaService).deleteCaptchaCode(reqVO.getUuid()); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testCaptcha_notFound() { | ||||
|         // 准备参数 | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
|  | ||||
|         // 调用, 并断言异常 | ||||
|         assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND); | ||||
|         // 校验调用参数 | ||||
|         verify(loginLogService, times(1)).createLoginLog( | ||||
|             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
|                     && o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult())) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testCaptcha_codeError() { | ||||
|         // 准备参数 | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); | ||||
|  | ||||
|         // mock 验证码不正确 | ||||
|         String code = randomString(); | ||||
|         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code); | ||||
|  | ||||
|         // 调用, 并断言异常 | ||||
|         assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR); | ||||
|         // 校验调用参数 | ||||
|         verify(loginLogService).createLoginLog( | ||||
|             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
|                     && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult())) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testLogin_success() { | ||||
|         // 准备参数 | ||||
|         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> | ||||
|                 o.setUsername("test_username").setPassword("test_password")); | ||||
|  | ||||
|         // mock 验证码正确 | ||||
|         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); | ||||
|         // mock user 数据 | ||||
|         AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") | ||||
|                 .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); | ||||
|         when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); | ||||
|         // mock password 匹配 | ||||
|         when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); | ||||
|         // mock 缓存登录用户到 Redis | ||||
|         OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) | ||||
|                 .setUserType(UserTypeEnum.ADMIN.getValue())); | ||||
|         when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) | ||||
|                 .thenReturn(accessTokenDO); | ||||
|  | ||||
|         // 调用, 并断言异常 | ||||
|         AuthLoginRespVO loginRespVO = authService.login(reqVO); | ||||
|         assertPojoEquals(accessTokenDO, loginRespVO); | ||||
|         // 校验调用参数 | ||||
|         verify(loginLogService).createLoginLog( | ||||
|             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
|                     && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) | ||||
|                     && o.getUserId().equals(user.getId())) | ||||
|         ); | ||||
|     } | ||||
| //    @Test | ||||
| //    public void testLogin_success() { | ||||
| //        // 准备参数 | ||||
| //        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> | ||||
| //                o.setUsername("test_username").setPassword("test_password")); | ||||
| // | ||||
| //        // mock 验证码正确 | ||||
| //        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode()); | ||||
| //        // mock user 数据 | ||||
| //        AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") | ||||
| //                .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); | ||||
| //        when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); | ||||
| //        // mock password 匹配 | ||||
| //        when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); | ||||
| //        // mock 缓存登录用户到 Redis | ||||
| //        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) | ||||
| //                .setUserType(UserTypeEnum.ADMIN.getValue())); | ||||
| //        when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) | ||||
| //                .thenReturn(accessTokenDO); | ||||
| // | ||||
| //        // 调用, 并断言异常 | ||||
| //        AuthLoginRespVO loginRespVO = authService.login(reqVO); | ||||
| //        assertPojoEquals(accessTokenDO, loginRespVO); | ||||
| //        // 校验调用参数 | ||||
| //        verify(loginLogService).createLoginLog( | ||||
| //            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ||||
| //                    && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) | ||||
| //                    && o.getUserId().equals(user.getId())) | ||||
| //        ); | ||||
| //    } | ||||
|  | ||||
|     @Test | ||||
|     public void testLogout_success() { | ||||
|   | ||||
| @@ -1,65 +0,0 @@ | ||||
| package cn.iocoder.yudao.module.system.service.common; | ||||
|  | ||||
| import cn.iocoder.yudao.module.system.controller.admin.common.vo.CaptchaImageRespVO; | ||||
| import cn.iocoder.yudao.module.system.dal.redis.common.CaptchaRedisDAO; | ||||
| import cn.iocoder.yudao.module.system.framework.captcha.config.CaptchaProperties; | ||||
| import cn.iocoder.yudao.framework.test.core.ut.BaseRedisUnitTest; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.springframework.context.annotation.Import; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
|  | ||||
| @Import({CaptchaServiceImpl.class, CaptchaProperties.class, CaptchaRedisDAO.class}) | ||||
| public class CaptchaServiceTest extends BaseRedisUnitTest { | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaServiceImpl captchaService; | ||||
|  | ||||
|     @Resource | ||||
|     private CaptchaRedisDAO captchaRedisDAO; | ||||
|     @Resource | ||||
|     private CaptchaProperties captchaProperties; | ||||
|  | ||||
|     @Test | ||||
|     public void testGetCaptchaImage() { | ||||
|         // 调用 | ||||
|         CaptchaImageRespVO respVO = captchaService.getCaptchaImage(); | ||||
|         // 断言 | ||||
|         assertNotNull(respVO.getUuid()); | ||||
|         assertNotNull(respVO.getImg()); | ||||
|         String captchaCode = captchaRedisDAO.get(respVO.getUuid()); | ||||
|         assertNotNull(captchaCode); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testGetCaptchaCode() { | ||||
|         // 准备参数 | ||||
|         String uuid = randomString(); | ||||
|         String code = randomString(); | ||||
|         // mock 数据 | ||||
|         captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout()); | ||||
|  | ||||
|         // 调用 | ||||
|         String resultCode = captchaService.getCaptchaCode(uuid); | ||||
|         // 断言 | ||||
|         assertEquals(code, resultCode); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testDeleteCaptchaCode() { | ||||
|         // 准备参数 | ||||
|         String uuid = randomString(); | ||||
|         String code = randomString(); | ||||
|         // mock 数据 | ||||
|         captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout()); | ||||
|  | ||||
|         // 调用 | ||||
|         captchaService.deleteCaptchaCode(uuid); | ||||
|         // 断言 | ||||
|         assertNull(captchaRedisDAO.get(uuid)); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -46,25 +46,25 @@ spring: | ||||
|         master: | ||||
|           name: ruoyi-vue-pro | ||||
|           url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 | ||||
| #          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 | ||||
| #          url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例 | ||||
| #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 | ||||
| #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例 | ||||
|           #          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 | ||||
|           #          url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例 | ||||
|           #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 | ||||
|           #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例 | ||||
|           username: root | ||||
|           password: 123456 | ||||
| #          username: sa | ||||
| #          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W | ||||
|         #          username: sa | ||||
|         #          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W | ||||
|         slave: # 模拟从库,可根据自己需要修改 | ||||
|           name: ruoyi-vue-pro | ||||
|           url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?allowMultiQueries=true&useUnicode=true&useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 | ||||
| #          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 | ||||
| #          url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例 | ||||
| #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 | ||||
| #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例 | ||||
|           #          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 | ||||
|           #          url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例 | ||||
|           #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 | ||||
|           #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例 | ||||
|           username: root | ||||
|           password: 123456 | ||||
| #          username: sa | ||||
| #          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W | ||||
|   #          username: sa | ||||
|   #          password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W | ||||
|  | ||||
|   # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 | ||||
|   redis: | ||||
|   | ||||
| @@ -57,6 +57,25 @@ mybatis-plus: | ||||
|       logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) | ||||
|   type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject | ||||
|  | ||||
| --- #################### 验证码相关配置 #################### | ||||
|  | ||||
| aj: | ||||
|   captcha: | ||||
|     jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径 | ||||
|     pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径 | ||||
|     cache-type: redis # 缓存 local/redis... | ||||
|     cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 | ||||
|     timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 | ||||
|     type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 | ||||
|     water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode | ||||
|     interference-options: 2 # 滑动干扰项(0/1/2) | ||||
|     req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false | ||||
|     req-get-lock-limit: 5 # 验证失败5次,get接口锁定 | ||||
|     req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔 | ||||
|     req-get-minute-limit: 30 # get 接口一分钟内请求数限制 | ||||
|     req-check-minute-limit: 60 # check 接口一分钟内请求数限制 | ||||
|     req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制 | ||||
|  | ||||
| --- #################### 芋道相关配置 #################### | ||||
|  | ||||
| yudao: | ||||
| @@ -75,9 +94,7 @@ yudao: | ||||
|     version: ${yudao.info.version} | ||||
|     base-package: ${yudao.info.base-package} | ||||
|   captcha: | ||||
|     timeout: 5m | ||||
|     width: 160 | ||||
|     height: 60 | ||||
|     enable: true # 验证码的开关,默认为 true;注意,优先读取数据库 infra_config 的 yudao.captcha.enable,所以请从数据库修改,可能需要重启项目 | ||||
|   codegen: | ||||
|     base-package: ${yudao.info.base-package} | ||||
|     db-schemas: ${spring.datasource.dynamic.datasource.master.name} | ||||
| @@ -92,12 +109,12 @@ yudao: | ||||
|     enable: true | ||||
|     ignore-urls: | ||||
|       - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号 | ||||
|       - /admin-api/system/captcha/get-image # 获取图片验证码,和租户无关 | ||||
|       - /captcha/get # 获取图片验证码,和租户无关 | ||||
|       - /captcha/check # 校验图片验证码,和租户无关 | ||||
|       - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 | ||||
|       - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 | ||||
|       - /app-api/pay/order/notify/* # 支付回调通知,不携带租户编号 | ||||
| #      - /jmreport/list | ||||
|       - /jmreport/* | ||||
|       - /jmreport/* # 积木报表,无法携带租户编号 | ||||
|     ignore-tables: | ||||
|       - system_tenant | ||||
|       - system_tenant_package | ||||
|   | ||||
| @@ -1,47 +1,60 @@ | ||||
| import request from '@/utils/request' | ||||
|  | ||||
| // 登录方法 | ||||
| export function login(username, password, code, uuid) { | ||||
|   const data = { | ||||
|     username, | ||||
|     password, | ||||
|     code, | ||||
|     uuid | ||||
|   } | ||||
|   return request({ | ||||
|     url: '/system/auth/login', | ||||
|     headers: { | ||||
|       isToken: false | ||||
|     }, | ||||
|     'method': 'post', | ||||
|     'data': data | ||||
|   }) | ||||
| export function login(username, password, captchaVerification) { | ||||
| 	const data = { | ||||
| 		username, | ||||
| 		password, | ||||
| 		captchaVerification | ||||
| 	} | ||||
| 	return request({ | ||||
| 		url: '/system/auth/login', | ||||
| 		headers: { | ||||
| 			isToken: false | ||||
| 		}, | ||||
| 		'method': 'POST', | ||||
| 		'data': data | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // 获取用户详细信息 | ||||
| export function getInfo() { | ||||
|   return request({ | ||||
|     url: '/system/auth/get-permission-info', | ||||
|     'method': 'get' | ||||
|   }) | ||||
| 	return request({ | ||||
| 		url: '/system/auth/get-permission-info', | ||||
| 		'method': 'GET' | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // 退出方法 | ||||
| export function logout() { | ||||
|   return request({ | ||||
|     url: '/system/auth/logout', | ||||
|     'method': 'post' | ||||
|   }) | ||||
| 	return request({ | ||||
| 		url: '/system/auth/logout', | ||||
| 		'method': 'POST' | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // 获取验证码 | ||||
| export function getCodeImg() { | ||||
|   return request({ | ||||
|     url: '/system/captcha/get-image', | ||||
|     headers: { | ||||
|       isToken: false | ||||
|     }, | ||||
|     method: 'get', | ||||
|     timeout: 20000 | ||||
|   }) | ||||
| export function getCaptcha(data) { | ||||
| 	return request({ | ||||
| 		url: '/captcha/get', | ||||
| 		headers: { | ||||
| 			isToken: false, | ||||
| 			isTenant: false | ||||
| 		}, | ||||
| 		method: 'POST', | ||||
| 		'data': data | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // 验证验证码 | ||||
| export function checkCaptcha(data) { | ||||
| 	return request({ | ||||
| 		url: '/captcha/check', | ||||
| 		headers: { | ||||
| 			isToken: false, | ||||
| 			isTenant: false | ||||
| 		}, | ||||
| 		method: 'POST', | ||||
| 		'data': data | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export function updateUserPwd(oldPassword, newPassword) { | ||||
|   } | ||||
|   return request({ | ||||
|     url: '/system/user/profile/update-password', | ||||
|     method: 'put', | ||||
|     method: 'PUT', | ||||
|     params: data | ||||
|   }) | ||||
| } | ||||
| @@ -18,7 +18,7 @@ export function updateUserPwd(oldPassword, newPassword) { | ||||
| export function getUserProfile() { | ||||
|   return request({ | ||||
|     url: '/system/user/profile/get', | ||||
|     method: 'get' | ||||
|     method: 'GET' | ||||
|   }) | ||||
| } | ||||
|  | ||||
| @@ -26,7 +26,7 @@ export function getUserProfile() { | ||||
| export function updateUserProfile(data) { | ||||
|   return request({ | ||||
|     url: '/system/user/profile/update', | ||||
|     method: 'put', | ||||
|     method: 'PUT', | ||||
|     data: data | ||||
|   }) | ||||
| } | ||||
| @@ -35,7 +35,7 @@ export function updateUserProfile(data) { | ||||
| export function uploadAvatar(data) { | ||||
|   return upload({ | ||||
|     url: '/system/user/profile/update-avatar', | ||||
|     method: 'put', | ||||
|     method: 'PUT', | ||||
|     name: data.name, | ||||
|     filePath: data.filePath | ||||
|   }) | ||||
|   | ||||
							
								
								
									
										469
									
								
								yudao-ui-admin-uniapp/components/verifition/Verify.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										14
									
								
								yudao-ui-admin-uniapp/components/verifition/utils/ase.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| import CryptoJS from 'crypto-js' | ||||
| /** | ||||
|  * @word 要加密的内容 | ||||
|  * @keyWord String  服务器随机返回的关键字 | ||||
|  *  */ | ||||
| export function aesEncrypt(word, keyWord = "XwKsGlMcdPMEhR1B") { | ||||
| 	var key = CryptoJS.enc.Utf8.parse(keyWord); | ||||
| 	var srcs = CryptoJS.enc.Utf8.parse(word); | ||||
| 	var encrypted = CryptoJS.AES.encrypt(srcs, key, { | ||||
| 		mode: CryptoJS.mode.ECB, | ||||
| 		padding: CryptoJS.pad.Pkcs7 | ||||
| 	}); | ||||
| 	return encrypted.toString(); | ||||
| } | ||||
							
								
								
									
										17
									
								
								yudao-ui-admin-uniapp/components/verifition/utils/request.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| import config from '@/config' | ||||
| const baseUrl = config.baseUrl | ||||
| export const myRequest = (option = {}) => { | ||||
| 	return new Promise((reslove, reject) => { | ||||
| 		uni.request({ | ||||
| 			url: baseUrl + option.url, | ||||
| 			data: option.data, | ||||
| 			method: option.method || "GET", | ||||
| 			success: (result) => { | ||||
| 				reslove(result) | ||||
| 			}, | ||||
| 			fail: (error) => { | ||||
| 				reject(error) | ||||
| 			} | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| @@ -1,7 +1,8 @@ | ||||
| // 应用全局配置 | ||||
| module.exports = { | ||||
|   // baseUrl: 'http://localhost:8080', | ||||
|   baseUrl: 'http://localhost:48080/admin-api', | ||||
|   baseUrl: 'http://localhost:48080', | ||||
|   baseApi: '/admin-api', | ||||
|   // 应用信息 | ||||
|   appInfo: { | ||||
|     // 应用名称 | ||||
|   | ||||
							
								
								
									
										5
									
								
								yudao-ui-admin-uniapp/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| { | ||||
|   "dependencies": { | ||||
|     "crypto-js": "^4.0.0" | ||||
|   } | ||||
| } | ||||
| @@ -1,182 +1,168 @@ | ||||
| <template> | ||||
|   <view class="normal-login-container"> | ||||
|     <view class="logo-content align-center justify-center flex"> | ||||
|       <image style="width: 100rpx;height: 100rpx;" :src="globalConfig.appInfo.logo" mode="widthFix"> | ||||
|       </image> | ||||
|       <text class="title">芋道移动端登录</text> | ||||
|     </view> | ||||
|     <view class="login-form-content"> | ||||
|       <view class="input-item flex align-center"> | ||||
|         <view class="iconfont icon-user icon"></view> | ||||
|         <input v-model="loginForm.username" class="input" type="text" placeholder="请输入账号" maxlength="30" /> | ||||
|       </view> | ||||
|       <view class="input-item flex align-center"> | ||||
|         <view class="iconfont icon-password icon"></view> | ||||
|         <input v-model="loginForm.password" type="password" class="input" placeholder="请输入密码" maxlength="20" /> | ||||
|       </view> | ||||
|       <view class="input-item flex align-center" v-if="captchaEnabled"> | ||||
|         <view class="iconfont icon-code icon"></view> | ||||
|         <input v-model="loginForm.code" class="input" placeholder="请输入验证码" maxlength="5" /> | ||||
|         <image :src="codeUrl" @click="getCode" class="login-code-img"></image> | ||||
|       </view> | ||||
|       <view class="action-btn"> | ||||
|         <button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button> | ||||
|       </view> | ||||
|     </view> | ||||
| 	<view class="normal-login-container"> | ||||
| 		<view class="logo-content align-center justify-center flex"> | ||||
| 			<image style="width: 100rpx;height: 100rpx;" :src="globalConfig.appInfo.logo" mode="widthFix"> | ||||
| 			</image> | ||||
| 			<text class="title">芋道移动端登录</text> | ||||
| 		</view> | ||||
| 		<view class="login-form-content"> | ||||
| 			<view class="input-item flex align-center"> | ||||
| 				<view class="iconfont icon-user icon"></view> | ||||
| 				<input v-model="loginForm.username" class="input" type="text" placeholder="请输入账号" maxlength="30" /> | ||||
| 			</view> | ||||
| 			<view class="input-item flex align-center"> | ||||
| 				<view class="iconfont icon-password icon"></view> | ||||
| 				<input v-model="loginForm.password" type="password" class="input" placeholder="请输入密码" maxlength="20" /> | ||||
| 			</view> | ||||
| 			<Verify @success="pwdLogin" :mode="'pop'" :captchaType="'blockPuzzle'" | ||||
| 				:imgSize="{ width: '330px', height: '155px' }" ref="verify"></Verify> | ||||
| 			<view class="action-btn"> | ||||
| 				<button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button> | ||||
| 			</view> | ||||
| 		</view> | ||||
|  | ||||
|     <view class="xieyi text-center"> | ||||
|       <text class="text-grey1">登录即代表同意</text> | ||||
|       <text @click="handleUserAgrement" class="text-blue">《用户协议》</text> | ||||
|       <text @click="handlePrivacy" class="text-blue">《隐私协议》</text> | ||||
|     </view> | ||||
|   </view> | ||||
| 		<view class="xieyi text-center"> | ||||
| 			<text class="text-grey1">登录即代表同意</text> | ||||
| 			<text @click="handleUserAgrement" class="text-blue">《用户协议》</text> | ||||
| 			<text @click="handlePrivacy" class="text-blue">《隐私协议》</text> | ||||
| 		</view> | ||||
| 	</view> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|   import { getCodeImg } from '@/api/login' | ||||
| 	import Verify from "@/components/verifition/Verify" | ||||
|  | ||||
|   export default { | ||||
|     data() { | ||||
|       return { | ||||
|         codeUrl: "", | ||||
|         captchaEnabled: true, | ||||
|         globalConfig: getApp().globalData.config, | ||||
|         loginForm: { | ||||
|           username: "admin", | ||||
|           password: "admin123", | ||||
|           code: "", | ||||
|           uuid: '' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     created() { | ||||
|       this.getCode() | ||||
|     }, | ||||
|     methods: { | ||||
|       // 隐私协议 | ||||
|       handlePrivacy() { | ||||
|         let site = this.globalConfig.appInfo.agreements[0] | ||||
|         this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`) | ||||
|       }, | ||||
|       // 用户协议 | ||||
|       handleUserAgrement() { | ||||
|         let site = this.globalConfig.appInfo.agreements[1] | ||||
|         this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`) | ||||
|       }, | ||||
|       // 获取图形验证码 | ||||
|       getCode() { | ||||
|         getCodeImg().then(res => { | ||||
|           res = res.data; | ||||
|           this.captchaEnabled = res.enable; | ||||
| 	export default { | ||||
| 		name: 'Login', | ||||
| 		components: { | ||||
| 			Verify | ||||
| 		}, | ||||
| 		data() { | ||||
| 			return { | ||||
| 				captchaEnabled: true, // 验证码开关 TODO 芋艿:需要抽到配置里 | ||||
| 				globalConfig: getApp().globalData.config, | ||||
| 				loginForm: { | ||||
| 					username: "admin", | ||||
| 					password: "admin123", | ||||
| 					captchaVerification: "" | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		methods: { | ||||
| 			// 隐私协议 | ||||
| 			handlePrivacy() { | ||||
| 				let site = this.globalConfig.appInfo.agreements[0] | ||||
| 				this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`) | ||||
| 			}, | ||||
| 			// 用户协议 | ||||
| 			handleUserAgrement() { | ||||
| 				let site = this.globalConfig.appInfo.agreements[1] | ||||
| 				this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`) | ||||
| 			}, | ||||
| 			// 登录方法 | ||||
| 			async handleLogin(params) { | ||||
| 				if (this.loginForm.username === "") { | ||||
| 					this.$modal.msgError("请输入您的账号") | ||||
| 				} else if (this.loginForm.password === "") { | ||||
| 					this.$modal.msgError("请输入您的密码") | ||||
| 				} else { | ||||
|           // 显示验证码 | ||||
|           if (this.captchaEnabled) { | ||||
|             this.codeUrl = "data:image/gif;base64," + res.img; | ||||
|             this.loginForm.uuid = res.uuid; | ||||
|             this.$refs.verify.show() | ||||
|           } else { // 直接登录 | ||||
|             await this.pwdLogin({}) | ||||
|           } | ||||
|         }) | ||||
|       }, | ||||
|       // 登录方法 | ||||
|       async handleLogin() { | ||||
|         if (this.loginForm.username === "") { | ||||
|           this.$modal.msgError("请输入您的账号") | ||||
|         } else if (this.loginForm.password === "") { | ||||
|           this.$modal.msgError("请输入您的密码") | ||||
|         } else if (this.loginForm.code === "" && this.captchaEnabled) { | ||||
|           this.$modal.msgError("请输入验证码") | ||||
|         } else { | ||||
|           this.$modal.loading("登录中,请耐心等待...") | ||||
|           this.pwdLogin() | ||||
|         } | ||||
|       }, | ||||
|       // 密码登录 | ||||
|       async pwdLogin() { | ||||
|         this.$store.dispatch('Login', this.loginForm).then(() => { | ||||
|           this.$modal.closeLoading() | ||||
|           this.loginSuccess() | ||||
|         }).catch(() => { | ||||
|           if (this.captchaEnabled) { | ||||
|             this.getCode() | ||||
|           } | ||||
|         }) | ||||
|       }, | ||||
|       // 登录成功后,处理函数 | ||||
|       loginSuccess(result) { | ||||
|         // 设置用户信息 | ||||
|         this.$store.dispatch('GetInfo').then(res => { | ||||
|           this.$tab.reLaunch('/pages/index') | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 				} | ||||
| 			}, | ||||
| 			// 密码登录 | ||||
| 			async pwdLogin(captchaParams) { | ||||
|         this.$modal.loading("登录中,请耐心等待...") | ||||
|         // 执行登录 | ||||
|         this.loginForm.captchaVerification = captchaParams.captchaVerification | ||||
| 				this.$store.dispatch('Login', this.loginForm).then(() => { | ||||
| 					this.$modal.closeLoading() | ||||
| 					this.loginSuccess() | ||||
| 				}) | ||||
| 			}, | ||||
| 			// 登录成功后,处理函数 | ||||
| 			loginSuccess(result) { | ||||
| 				// 设置用户信息 | ||||
| 				this.$store.dispatch('GetInfo').then(res => { | ||||
| 					this.$tab.reLaunch('/pages/index') | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <style lang="scss"> | ||||
|   page { | ||||
|     background-color: #ffffff; | ||||
|   } | ||||
| 	page { | ||||
| 		background-color: #ffffff; | ||||
| 	} | ||||
|  | ||||
|   .normal-login-container { | ||||
|     width: 100%; | ||||
| 	.normal-login-container { | ||||
| 		width: 100%; | ||||
|  | ||||
|     .logo-content { | ||||
|       width: 100%; | ||||
|       font-size: 21px; | ||||
|       text-align: center; | ||||
|       padding-top: 15%; | ||||
| 		.logo-content { | ||||
| 			width: 100%; | ||||
| 			font-size: 21px; | ||||
| 			text-align: center; | ||||
| 			padding-top: 15%; | ||||
|  | ||||
|       image { | ||||
|         border-radius: 4px; | ||||
|       } | ||||
| 			image { | ||||
| 				border-radius: 4px; | ||||
| 			} | ||||
|  | ||||
|       .title { | ||||
|         margin-left: 10px; | ||||
|       } | ||||
|     } | ||||
| 			.title { | ||||
| 				margin-left: 10px; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|     .login-form-content { | ||||
|       text-align: center; | ||||
|       margin: 20px auto; | ||||
|       margin-top: 15%; | ||||
|       width: 80%; | ||||
| 		.login-form-content { | ||||
| 			text-align: center; | ||||
| 			margin: 20px auto; | ||||
| 			margin-top: 15%; | ||||
| 			width: 80%; | ||||
|  | ||||
|       .input-item { | ||||
|         margin: 20px auto; | ||||
|         background-color: #f5f6f7; | ||||
|         height: 45px; | ||||
|         border-radius: 20px; | ||||
| 			.input-item { | ||||
| 				margin: 20px auto; | ||||
| 				background-color: #f5f6f7; | ||||
| 				height: 45px; | ||||
| 				border-radius: 20px; | ||||
|  | ||||
|         .icon { | ||||
|           font-size: 38rpx; | ||||
|           margin-left: 10px; | ||||
|           color: #999; | ||||
|         } | ||||
| 				.icon { | ||||
| 					font-size: 38rpx; | ||||
| 					margin-left: 10px; | ||||
| 					color: #999; | ||||
| 				} | ||||
|  | ||||
|         .input { | ||||
|           width: 100%; | ||||
|           font-size: 14px; | ||||
|           line-height: 20px; | ||||
|           text-align: left; | ||||
|           padding-left: 15px; | ||||
|         } | ||||
| 				.input { | ||||
| 					width: 100%; | ||||
| 					font-size: 14px; | ||||
| 					line-height: 20px; | ||||
| 					text-align: left; | ||||
| 					padding-left: 15px; | ||||
| 				} | ||||
|  | ||||
|       } | ||||
| 			} | ||||
|  | ||||
|       .login-btn { | ||||
|         margin-top: 40px; | ||||
|         height: 45px; | ||||
|       } | ||||
| 			.login-btn { | ||||
| 				margin-top: 40px; | ||||
| 				height: 45px; | ||||
| 			} | ||||
|  | ||||
|       .xieyi { | ||||
|         color: #333; | ||||
|         margin-top: 20px; | ||||
|       } | ||||
|     } | ||||
| 			.xieyi { | ||||
| 				color: #333; | ||||
| 				margin-top: 20px; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|     .easyinput { | ||||
|       width: 100%; | ||||
|     } | ||||
|   } | ||||
| 		.easyinput { | ||||
| 			width: 100%; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   .login-code-img { | ||||
|     height: 45px; | ||||
|   } | ||||
| 	.login-code-img { | ||||
| 		height: 45px; | ||||
| 	} | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								yudao-ui-admin-uniapp/static/images/default.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
| @@ -42,10 +42,9 @@ const user = { | ||||
|     Login({ commit }, userInfo) { | ||||
|       const username = userInfo.username.trim() | ||||
|       const password = userInfo.password | ||||
|       const code = userInfo.code | ||||
|       const uuid = userInfo.uuid | ||||
|       const captchaVerification = userInfo.captchaVerification | ||||
|       return new Promise((resolve, reject) => { | ||||
|         login(username, password, code, uuid).then(res => { | ||||
|         login(username, password, captchaVerification).then(res => { | ||||
|           res = res.data; | ||||
|           // 设置 token | ||||
|           setToken(res) | ||||
| @@ -83,7 +82,6 @@ const user = { | ||||
|     LogOut({ commit, state }) { | ||||
|       return new Promise((resolve, reject) => { | ||||
|         logout(state.token).then(() => { | ||||
|           commit('SET_TOKEN', '') | ||||
|           commit('SET_ROLES', []) | ||||
|           commit('SET_PERMISSIONS', []) | ||||
|           removeToken() | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import errorCode from '@/utils/errorCode' | ||||
| import { toast, showConfirm, tansParams } from '@/utils/common' | ||||
|  | ||||
| let timeout = 10000 | ||||
| const baseUrl = config.baseUrl | ||||
| const baseUrl = config.baseUrl + config.baseApi; | ||||
|  | ||||
| const request = config => { | ||||
|   // 是否需要设置 token | ||||
|   | ||||
| @@ -9,3 +9,6 @@ VITE_OPEN=true | ||||
|  | ||||
| # 租户开关 | ||||
| VITE_APP_TENANT_ENABLE=true | ||||
|  | ||||
| # 验证码的开关 | ||||
| VITE_APP_CAPTCHA_ENABLE=false | ||||
|   | ||||
| @@ -10,7 +10,6 @@ | ||||
|     <img src="https://img.shields.io/badge/-Prettier-ef9421?logo=Prettier&logoColor=white" alt="Prettier"> | ||||
|     <img src="https://img.shields.io/badge/-Less-1D365D?logo=less&logoColor=white" alt="Less"> | ||||
|     <img src="https://img.shields.io/badge/-Wind%20CSS-06B6D4?logo=Tailwind%20CSS&logoColor=white" alt="Taiwind"> | ||||
|     <img src="" alt=""> | ||||
| </p> | ||||
| ## 介绍 | ||||
|  | ||||
|   | ||||
| @@ -26,12 +26,13 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@iconify/iconify": "^2.2.1", | ||||
|     "@vueuse/core": "^9.0.2", | ||||
|     "@vueuse/core": "^9.1.0", | ||||
|     "@wangeditor/editor": "^5.1.14", | ||||
|     "@wangeditor/editor-for-vue": "^5.1.10", | ||||
|     "@zxcvbn-ts/core": "^2.0.3", | ||||
|     "@zxcvbn-ts/core": "^2.0.4", | ||||
|     "animate.css": "^4.1.1", | ||||
|     "axios": "^0.27.2", | ||||
|     "crypto-js": "^4.1.1", | ||||
|     "dayjs": "^1.11.4", | ||||
|     "echarts": "^5.3.3", | ||||
|     "echarts-wordcloud": "^2.0.0", | ||||
| @@ -48,7 +49,7 @@ | ||||
|     "url": "^0.11.0", | ||||
|     "vue": "3.2.37", | ||||
|     "vue-cropper": "^1.0.3", | ||||
|     "vue-i18n": "9.2.0", | ||||
|     "vue-i18n": "9.2.2", | ||||
|     "vue-router": "^4.1.3", | ||||
|     "vue-types": "^4.2.1", | ||||
|     "web-storage-cache": "^1.1.1" | ||||
| @@ -56,17 +57,17 @@ | ||||
|   "devDependencies": { | ||||
|     "@commitlint/cli": "^17.0.3", | ||||
|     "@commitlint/config-conventional": "^17.0.3", | ||||
|     "@iconify/json": "^2.1.86", | ||||
|     "@intlify/vite-plugin-vue-i18n": "^5.0.1", | ||||
|     "@purge-icons/generated": "^0.8.1", | ||||
|     "@iconify/json": "^2.1.89", | ||||
|     "@intlify/vite-plugin-vue-i18n": "^6.0.0", | ||||
|     "@purge-icons/generated": "^0.9.0", | ||||
|     "@types/intro.js": "^5.1.0", | ||||
|     "@types/lodash-es": "^4.17.6", | ||||
|     "@types/node": "^18.6.3", | ||||
|     "@types/node": "^18.6.5", | ||||
|     "@types/nprogress": "^0.2.0", | ||||
|     "@types/qrcode": "^1.4.2", | ||||
|     "@types/qs": "^6.9.7", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.32.0", | ||||
|     "@typescript-eslint/parser": "^5.32.0", | ||||
|     "@typescript-eslint/eslint-plugin": "^5.33.0", | ||||
|     "@typescript-eslint/parser": "^5.33.0", | ||||
|     "@vitejs/plugin-vue": "^3.0.1", | ||||
|     "@vitejs/plugin-vue-jsx": "^2.0.0", | ||||
|     "autoprefixer": "^10.4.8", | ||||
| @@ -78,7 +79,7 @@ | ||||
|     "less": "^4.1.3", | ||||
|     "lint-staged": "^13.0.3", | ||||
|     "plop": "^3.1.1", | ||||
|     "postcss": "^8.4.14", | ||||
|     "postcss": "^8.4.16", | ||||
|     "postcss-html": "^1.5.0", | ||||
|     "postcss-less": "^6.0.0", | ||||
|     "prettier": "^2.7.1", | ||||
| @@ -91,16 +92,16 @@ | ||||
|     "stylelint-config-standard": "^26.0.0", | ||||
|     "stylelint-order": "^5.0.0", | ||||
|     "typescript": "4.7.4", | ||||
|     "unplugin-vue-define-options": "^0.7.1", | ||||
|     "vite": "3.0.4", | ||||
|     "unplugin-vue-define-options": "^0.7.3", | ||||
|     "vite": "3.0.5", | ||||
|     "vite-plugin-compression": "^0.5.1", | ||||
|     "vite-plugin-eslint": "^1.7.0", | ||||
|     "vite-plugin-html": "^3.2.0", | ||||
|     "vite-plugin-purge-icons": "^0.8.2", | ||||
|     "vite-plugin-purge-icons": "^0.9.0", | ||||
|     "vite-plugin-style-import": "^2.0.0", | ||||
|     "vite-plugin-svg-icons": "^2.0.1", | ||||
|     "vite-plugin-windicss": "^1.8.7", | ||||
|     "vue-tsc": "^0.39.4", | ||||
|     "vue-tsc": "^0.39.5", | ||||
|     "windicss": "^3.5.6" | ||||
|   }, | ||||
|   "engines": { | ||||
|   | ||||
| @@ -18,11 +18,6 @@ export interface SmsLoginVO { | ||||
|   code: string | ||||
| } | ||||
|  | ||||
| // 获取验证码 | ||||
| export const getCodeImgApi = () => { | ||||
|   return request.get({ url: '/system/captcha/get-image' }) | ||||
| } | ||||
|  | ||||
| // 登录 | ||||
| export const loginApi = (data: UserLoginVO) => { | ||||
|   return request.post({ url: '/system/auth/login', data }) | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| export type UserLoginVO = { | ||||
|   username: string | ||||
|   password: string | ||||
|   code: string | ||||
|   uuid: string | ||||
|   captchaVerification: string | ||||
| } | ||||
|  | ||||
| export type TokenType = { | ||||
|   | ||||
| @@ -24,6 +24,6 @@ export const updateUserPwdApi = (oldPassword: string, newPassword: string) => { | ||||
| } | ||||
|  | ||||
| // 用户头像上传 | ||||
| export const uploadAvatarApi = (params) => { | ||||
|   return request.upload({ url: '/system/user/profile/update-avatar', params }) | ||||
| export const uploadAvatarApi = (data) => { | ||||
|   return request.upload({ url: '/system/user/profile/update-avatar', data: data }) | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ const props = defineProps({ | ||||
|     required: true | ||||
|   }, | ||||
|   value: { | ||||
|     type: [String, Number] as PropType<string | number>, | ||||
|     type: [String, Number, Boolean] as PropType<string | number | boolean>, | ||||
|     required: true | ||||
|   } | ||||
| }) | ||||
|   | ||||
| @@ -8,6 +8,8 @@ import { ElMessage } from 'element-plus' | ||||
| import { useLocaleStore } from '@/store/modules/locale' | ||||
| import { getAccessToken, getTenantId } from '@/utils/auth' | ||||
|  | ||||
| type InsertFnType = (url: string, alt: string, href: string) => void | ||||
|  | ||||
| const localeStore = useLocaleStore() | ||||
|  | ||||
| const currentLocale = computed(() => localeStore.getCurrentLocale) | ||||
| @@ -85,29 +87,58 @@ const editorConfig = computed((): IEditorConfig => { | ||||
|         ['uploadImage']: { | ||||
|           server: import.meta.env.VITE_UPLOAD_URL, | ||||
|           // 单个文件的最大体积限制,默认为 2M | ||||
|           maxFileSize: 2 * 1024 * 1024, | ||||
|           maxFileSize: 5 * 1024 * 1024, | ||||
|           // 最多可上传几个文件,默认为 100 | ||||
|           maxNumberOfFiles: 10, | ||||
|           // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 [] | ||||
|           allowedFileTypes: ['image/*'], | ||||
|  | ||||
|           // 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。 | ||||
|           meta: {}, | ||||
|           meta: { updateSupport: 0 }, | ||||
|           // 将 meta 拼接到 url 参数中,默认 false | ||||
|           metaWithUrl: false, | ||||
|           metaWithUrl: true, | ||||
|  | ||||
|           // 自定义增加 http  header | ||||
|           headers: { | ||||
|             Accept: 'image/*', | ||||
|             Accept: '*', | ||||
|             Authorization: 'Bearer ' + getAccessToken(), | ||||
|             'tenant-id': getTenantId() | ||||
|           }, | ||||
|  | ||||
|           // 跨域是否传递 cookie ,默认为 false | ||||
|           withCredentials: false, | ||||
|           withCredentials: true, | ||||
|  | ||||
|           // 超时时间,默认为 10 秒 | ||||
|           timeout: 5 * 1000 // 5 秒 | ||||
|           timeout: 5 * 1000, // 5 秒 | ||||
|  | ||||
|           // form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image | ||||
|           fieldName: 'file', | ||||
|  | ||||
|           // 上传之前触发 | ||||
|           onBeforeUpload(file: File) { | ||||
|             console.log(file) | ||||
|             return file | ||||
|           }, | ||||
|           // 上传进度的回调函数 | ||||
|           onProgress(progress: number) { | ||||
|             // progress 是 0-100 的数字 | ||||
|             console.log('progress', progress) | ||||
|           }, | ||||
|           onSuccess(file: File, res: any) { | ||||
|             console.log('onSuccess', file, res) | ||||
|           }, | ||||
|           onFailed(file: File, res: any) { | ||||
|             alert(res.message) | ||||
|             console.log('onFailed', file, res) | ||||
|           }, | ||||
|           onError(file: File, err: any, res: any) { | ||||
|             alert(err.message) | ||||
|             console.error('onError', file, err, res) | ||||
|           }, | ||||
|           // 自定义插入图片 | ||||
|           customInsert(res: any, insertFn: InsertFnType) { | ||||
|             insertFn(res.data, 'image', res.data) | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       uploadImgShowBase64: true | ||||
|   | ||||
| @@ -2,18 +2,11 @@ | ||||
| import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus' | ||||
| import { useI18n } from '@/hooks/web/useI18n' | ||||
| import { useCache } from '@/hooks/web/useCache' | ||||
| import { removeToken } from '@/utils/auth' | ||||
| import { resetRouter } from '@/router' | ||||
| import { useRouter } from 'vue-router' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import { useTagsViewStore } from '@/store/modules/tagsView' | ||||
| import avatarImg from '@/assets/imgs/avatar.gif' | ||||
|  | ||||
| const tagsViewStore = useTagsViewStore() | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('user-info') | ||||
| import { useUserStore } from '@/store/modules/user' | ||||
| import { useTagsViewStore } from '@/store/modules/tagsView' | ||||
|  | ||||
| const { t } = useI18n() | ||||
|  | ||||
| @@ -21,6 +14,14 @@ const { wsCache } = useCache() | ||||
|  | ||||
| const { push, replace } = useRouter() | ||||
|  | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const tagsViewStore = useTagsViewStore() | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('user-info') | ||||
|  | ||||
| const user = wsCache.get('user') | ||||
|  | ||||
| const avatar = user.user.avatar ? user.user.avatar : avatarImg | ||||
| @@ -34,10 +35,8 @@ const loginOut = () => { | ||||
|     type: 'warning' | ||||
|   }) | ||||
|     .then(async () => { | ||||
|       resetRouter() // 重置静态路由表 | ||||
|       wsCache.clear() | ||||
|       removeToken() | ||||
|       tagsViewStore.delAllViews() | ||||
|       userStore.loginOut() | ||||
|       tagsViewStore.delAllViews | ||||
|       replace('/login') | ||||
|     }) | ||||
|     .catch(() => {}) | ||||
|   | ||||
							
								
								
									
										3
									
								
								yudao-ui-admin-vue3/src/components/Verifition/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| import Verify from './src/Verify.vue' | ||||
|  | ||||
| export { Verify } | ||||
 芋道源码
					芋道源码