mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 22:57:17 +08:00 
			
		
		
		
	新增:新增修改邮箱功能,并优化部分以往代码(引入 spring-boot-starter-mail 用于发送邮件验证码)
This commit is contained in:
		
							
								
								
									
										1
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/deploy.yml
									
									
									
									
										vendored
									
									
								
							| @@ -49,6 +49,7 @@ jobs: | |||||||
|           script: | |           script: | | ||||||
|             cd /docker |             cd /docker | ||||||
|             docker-compose up --force-recreate --build -d continew-admin-server |             docker-compose up --force-recreate --build -d continew-admin-server | ||||||
|  |             docker images  | grep none | awk '{print $3}' | xargs docker rmi | ||||||
|  |  | ||||||
|   # 部署前端 |   # 部署前端 | ||||||
|   deploy-web: |   deploy-web: | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| # ContiNew-Admin 中后台管理框架 | # ContiNew Admin 中后台管理框架 | ||||||
|  |  | ||||||
| [](https://github.com/Charles7c/continew-admin/blob/dev/LICENSE) | [](https://github.com/Charles7c/continew-admin/blob/dev/LICENSE) | ||||||
|  |  | ||||||
| @@ -7,7 +7,7 @@ | |||||||
|  |  | ||||||
| ## 简介 | ## 简介 | ||||||
|  |  | ||||||
| ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。当前阶段采用的技术栈:Vue3、TypeScript、Arco Design Pro Vue、Spring Boot、Undertow、Sa-Token、JWT、MariaDB、MyBatis Plus、Redis、Redisson、Hutool 等。 | ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。当前阶段采用的技术栈:Vue3、TypeScript、Arco Design Pro Vue、Spring Boot、Undertow、Sa-Token、JWT、MariaDB、MyBatis Plus、Redis、Redisson、Hutool 等。 | ||||||
|  |  | ||||||
| ## 开始 | ## 开始 | ||||||
|  |  | ||||||
| @@ -21,11 +21,11 @@ git clone https://github.com/Charles7c/continew-admin.git | |||||||
|  |  | ||||||
| # 2.在 IDE(IntelliJ IDEA/Eclipse)中打开本项目 | # 2.在 IDE(IntelliJ IDEA/Eclipse)中打开本项目 | ||||||
|  |  | ||||||
| # 3.修改配置文件中的 Redis 配置信息 | # 3.修改配置文件中的数据源配置信息、Redis 配置信息、邮件配置信息等 | ||||||
| # [3.也可以在 IntelliJ IDEA 中直接配置程序启动环境变量(DB_HOST、DB_PORT、DB_USER、DB_PWD、DB_NAME;REDIS_HOST、REDIS_PORT、REDIS_PWD、REDIS_DB)] | # [3.也可以在 IntelliJ IDEA 中直接配置程序启动环境变量(DB_HOST、DB_PORT、DB_USER、DB_PWD、DB_NAME;REDIS_HOST、REDIS_PORT、REDIS_PWD、REDIS_DB)] | ||||||
|  |  | ||||||
| # 4.启动程序 | # 4.启动程序 | ||||||
| # 4.1 启动成功:访问 http://localhost:8000/,页面输出:ContiNew-Admin backend service started successfully. | # 4.1 启动成功:访问 http://localhost:8000/,页面输出:ContiNew Admin backend service started successfully. | ||||||
| # 4.2 接口文档:http://localhost:8000/doc.html | # 4.2 接口文档:http://localhost:8000/doc.html | ||||||
|  |  | ||||||
| # 5.部署 | # 5.部署 | ||||||
| @@ -72,7 +72,7 @@ yarn dev | |||||||
| | :----------------------------------------------------------- | :----------- | :----------------------------------------------------------- | | | :----------------------------------------------------------- | :----------- | :----------------------------------------------------------- | | ||||||
| | [Vue](https://cn.vuejs.org/)                                 | 3.2.45       | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 | | | [Vue](https://cn.vuejs.org/)                                 | 3.2.45       | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 | | ||||||
| | [TypeScript](https://www.typescriptlang.org/zh/)             | 4.9.4        | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 | | | [TypeScript](https://www.typescriptlang.org/zh/)             | 4.9.4        | TypeScript 是微软开发的一个开源的编程语言,通过在 JavaScript 的基础上添加静态类型定义构建而成。 | | ||||||
| | [Arco Design Pro Vue](http://pro.arco.design/)               | 2.5.15       | 基于 Arco Design Vue 组件库的开箱即用的中后台前端解决方案。  | | | [Arco Design Pro Vue](http://pro.arco.design/)               | 2.6.0        | 基于 Arco Design Vue 组件库的开箱即用的中后台前端解决方案。  | | ||||||
| | [Spring Boot](https://spring.io/projects/spring-boot)        | 2.7.7        | 简化新 Spring 应用的初始搭建以及开发过程。                   | | | [Spring Boot](https://spring.io/projects/spring-boot)        | 2.7.7        | 简化新 Spring 应用的初始搭建以及开发过程。                   | | ||||||
| | [Undertow](https://undertow.io/)                             | 2.2.22.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 | | | [Undertow](https://undertow.io/)                             | 2.2.22.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 | | ||||||
| | [Sa-Token + JWT](https://sa-token.dev33.cn/)                 | 1.33.0       | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。             | | | [Sa-Token + JWT](https://sa-token.dev33.cn/)                 | 1.33.0       | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。             | | ||||||
| @@ -110,10 +110,14 @@ continew-admin  # 全局通用项目配置及依赖版本管理 | |||||||
|   │      │        ├─ webapi |   │      │        ├─ webapi | ||||||
|   │      │        │  └─ controller   |   │      │        │  └─ controller   | ||||||
|   │      │        │    ├─ auth    # 认证相关 API |   │      │        │    ├─ auth    # 认证相关 API | ||||||
|  |   │      │        │    ├─ common  # 公共相关 API(例如:验证码 API 等) | ||||||
|   │      │        │    └─ system  # 系统管理相关 API |   │      │        │    └─ system  # 系统管理相关 API | ||||||
|   │      │        └─ ContinewAdminApplication.java  # 启动入口 |   │      │        └─ ContinewAdminApplication.java  # 启动入口 | ||||||
|   │      └─ resources   # 工程配置目录 |   │      └─ resources   # 工程配置目录 | ||||||
|   │        └─ db.changelog.v0.0.1    # 数据库脚本文件 |   │        ├─ db.changelog   # 数据库脚本文件 | ||||||
|  |   │        │  └─ v0.0.1        # v0.0.1 版本数据库脚本文件 | ||||||
|  |   │        └─ templates      # 模板文件 | ||||||
|  |   │          └─ mail           # 邮件模板 | ||||||
|   ├─ continew-admin-monitor  # 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等) |   ├─ continew-admin-monitor  # 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等) | ||||||
|   │  └─ src |   │  └─ src | ||||||
|   │    └─ main |   │    └─ main | ||||||
| @@ -144,8 +148,7 @@ continew-admin  # 全局通用项目配置及依赖版本管理 | |||||||
|   │      │      └─ cnadmin |   │      │      └─ cnadmin | ||||||
|   │      │        ├─ auth     # 系统认证相关业务及配置 |   │      │        ├─ auth     # 系统认证相关业务及配置 | ||||||
|   │      │        │  ├─ config    # 系统认证相关配置 |   │      │        │  ├─ config    # 系统认证相关配置 | ||||||
|   │      │        │  │  ├─ satoken    # Sa-Token 配置 |   │      │        │  │  └─ satoken    # Sa-Token 配置 | ||||||
|   │      │        │  │  └─ properties # 系统认证相关配置属性 |  | ||||||
|   │      │        │  ├─ model     # 系统认证相关模型 |   │      │        │  ├─ model     # 系统认证相关模型 | ||||||
|   │      │        │  │  ├─ request    # 系统认证相关请求对象 |   │      │        │  │  ├─ request    # 系统认证相关请求对象 | ||||||
|   │      │        │  │  └─ vo         # 系统认证相关 VO(View Object) |   │      │        │  │  └─ vo         # 系统认证相关 VO(View Object) | ||||||
| @@ -197,6 +200,7 @@ continew-admin | |||||||
|     ├─ src |     ├─ src | ||||||
|     │  ├─ api               # 请求接口 |     │  ├─ api               # 请求接口 | ||||||
|     │  │  ├─ auth             # 认证模块 |     │  │  ├─ auth             # 认证模块 | ||||||
|  |     │  │  ├─ common           # 公共模块 | ||||||
|     │  │  └─ system           # 系统管理模块 |     │  │  └─ system           # 系统管理模块 | ||||||
|     │  ├─ assets            # 静态资源 |     │  ├─ assets            # 静态资源 | ||||||
|     │  │  ├─ images           # 图片资源 |     │  │  ├─ images           # 图片资源 | ||||||
|   | |||||||
| @@ -59,6 +59,17 @@ limitations under the License. | |||||||
|             </exclusions> |             </exclusions> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |         <!-- Java 邮件支持 --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.springframework.boot</groupId> | ||||||
|  |             <artifactId>spring-boot-starter-mail</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |         <!-- FreeMarker(模板引擎) --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.freemarker</groupId> | ||||||
|  |             <artifactId>freemarker</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|         <!-- Hibernate Validator --> |         <!-- Hibernate Validator --> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>org.springframework.boot</groupId> |             <groupId>org.springframework.boot</groupId> | ||||||
| @@ -126,5 +137,11 @@ limitations under the License. | |||||||
|             <groupId>org.redisson</groupId> |             <groupId>org.redisson</groupId> | ||||||
|             <artifactId>redisson-spring-boot-starter</artifactId> |             <artifactId>redisson-spring-boot-starter</artifactId> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |         <!-- Easy Captcha(Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目) --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>com.github.whvcse</groupId> | ||||||
|  |             <artifactId>easy-captcha</artifactId> | ||||||
|  |         </dependency> | ||||||
|     </dependencies> |     </dependencies> | ||||||
| </project> | </project> | ||||||
| @@ -14,7 +14,7 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package top.charles7c.cnadmin.auth.config.properties; | package top.charles7c.cnadmin.common.config.properties; | ||||||
| 
 | 
 | ||||||
| import java.awt.*; | import java.awt.*; | ||||||
| 
 | 
 | ||||||
| @@ -42,25 +42,35 @@ import cn.hutool.core.util.StrUtil; | |||||||
| @ConfigurationProperties(prefix = "captcha") | @ConfigurationProperties(prefix = "captcha") | ||||||
| public class CaptchaProperties { | public class CaptchaProperties { | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * 图片验证码配置 | ||||||
|  |      */ | ||||||
|  |     private CaptchaImage image; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 邮箱验证码配置 | ||||||
|  |      */ | ||||||
|  |     private CaptchaMail mail; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 图片验证码配置 | ||||||
|  |      */ | ||||||
|  |     @Data | ||||||
|  |     public static class CaptchaImage { | ||||||
|         /** |         /** | ||||||
|          * 类型 |          * 类型 | ||||||
|          */ |          */ | ||||||
|     private CaptchaTypeEnum type; |         private CaptchaImageTypeEnum type; | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * 缓存键的前缀 |  | ||||||
|      */ |  | ||||||
|     private String keyPrefix; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * 过期时间 |  | ||||||
|      */ |  | ||||||
|     private Long expirationInMinutes = 2L; |  | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * 内容长度 |          * 内容长度 | ||||||
|          */ |          */ | ||||||
|     private int length = 4; |         private int length; | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * 过期时间 | ||||||
|  |          */ | ||||||
|  |         private long expirationInMinutes; | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * 宽度 |          * 宽度 | ||||||
| @@ -83,7 +93,7 @@ public class CaptchaProperties { | |||||||
|         private int fontSize = 25; |         private int fontSize = 25; | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|      * 获取验证码对象 |          * 获取图片验证码对象 | ||||||
|          * |          * | ||||||
|          * @return 验证码对象 |          * @return 验证码对象 | ||||||
|          */ |          */ | ||||||
| @@ -95,13 +105,40 @@ public class CaptchaProperties { | |||||||
|             } |             } | ||||||
|             return captcha; |             return captcha; | ||||||
|         } |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * 验证码类型枚举 |      * 邮箱验证码配置 | ||||||
|  |      */ | ||||||
|  |     @Data | ||||||
|  |     public static class CaptchaMail { | ||||||
|  |         /** | ||||||
|  |          * 内容长度 | ||||||
|  |          */ | ||||||
|  |         private int length; | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * 过期时间 | ||||||
|  |          */ | ||||||
|  |         private long expirationInMinutes; | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * 限制时间 | ||||||
|  |          */ | ||||||
|  |         private long limitInSeconds; | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * 模板路径 | ||||||
|  |          */ | ||||||
|  |         private String templatePath; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 图片验证码类型枚举 | ||||||
|      */ |      */ | ||||||
|     @Getter |     @Getter | ||||||
|     @RequiredArgsConstructor |     @RequiredArgsConstructor | ||||||
|     public enum CaptchaTypeEnum { |     private enum CaptchaImageTypeEnum { | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * 算术 |          * 算术 | ||||||
| @@ -33,4 +33,14 @@ public class CacheConstants { | |||||||
|      */ |      */ | ||||||
|     public static final String LOGIN_USER_CACHE_KEY = "LOGIN_USER"; |     public static final String LOGIN_USER_CACHE_KEY = "LOGIN_USER"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 验证码缓存键 | ||||||
|  |      */ | ||||||
|  |     public static final String CAPTCHA_CACHE_KEY = "CAPTCHA"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 限流缓存键 | ||||||
|  |      */ | ||||||
|  |     public static final String LIMIT_CACHE_KEY = "LIMIT"; | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -162,8 +162,8 @@ public class GlobalExceptionHandler { | |||||||
|     @ResponseStatus(HttpStatus.UNAUTHORIZED) |     @ResponseStatus(HttpStatus.UNAUTHORIZED) | ||||||
|     @ExceptionHandler(NotLoginException.class) |     @ExceptionHandler(NotLoginException.class) | ||||||
|     public R handleNotLoginException(NotLoginException e, HttpServletRequest request) { |     public R handleNotLoginException(NotLoginException e, HttpServletRequest request) { | ||||||
|         log.error("请求地址'{}',认证失败'{}',无法访问系统资源", request.getRequestURI(), e.getMessage()); |         log.error("请求地址'{}',认证失败,无法访问系统资源", request.getRequestURI(), e); | ||||||
|         return R.fail(HttpStatus.UNAUTHORIZED.value(), "认证失败,无法访问系统资源"); |         return R.fail(HttpStatus.UNAUTHORIZED.value(), "登录状态已过期,请重新登录"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ | |||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package top.charles7c.cnadmin.auth.model.vo; | package top.charles7c.cnadmin.common.model.vo; | ||||||
| 
 | 
 | ||||||
| import java.io.Serializable; | import java.io.Serializable; | ||||||
| 
 | 
 | ||||||
| @@ -0,0 +1,244 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package top.charles7c.cnadmin.common.util; | ||||||
|  |  | ||||||
|  | import java.io.File; | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import javax.mail.MessagingException; | ||||||
|  | import javax.mail.internet.MimeMessage; | ||||||
|  |  | ||||||
|  | import lombok.AccessLevel; | ||||||
|  | import lombok.Data; | ||||||
|  | import lombok.NoArgsConstructor; | ||||||
|  |  | ||||||
|  | import org.springframework.mail.javamail.JavaMailSender; | ||||||
|  | import org.springframework.mail.javamail.MimeMessageHelper; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.collection.CollUtil; | ||||||
|  | import cn.hutool.core.util.ArrayUtil; | ||||||
|  | import cn.hutool.core.util.CharUtil; | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
|  | import cn.hutool.extra.spring.SpringUtil; | ||||||
|  |  | ||||||
|  | import top.charles7c.cnadmin.common.util.validate.CheckUtils; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 邮件工具类 | ||||||
|  |  * | ||||||
|  |  * @author Charles7c | ||||||
|  |  * @since 2023/1/12 23:25 | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||||
|  | public class MailUtils { | ||||||
|  |  | ||||||
|  |     private static final JavaMailSender MAIL_SENDER = SpringUtil.getBean(JavaMailSender.class); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 发送文本邮件给单个人 | ||||||
|  |      * | ||||||
|  |      * @param subject | ||||||
|  |      *            主题 | ||||||
|  |      * @param content | ||||||
|  |      *            内容 | ||||||
|  |      * @param to | ||||||
|  |      *            收件人 | ||||||
|  |      * @throws MessagingException | ||||||
|  |      *             / | ||||||
|  |      */ | ||||||
|  |     public static void sendText(String to, String subject, String content) throws MessagingException { | ||||||
|  |         send(splitAddress(to), null, null, subject, content, false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 发送 HTML 邮件给单个人 | ||||||
|  |      * | ||||||
|  |      * @param subject | ||||||
|  |      *            主题 | ||||||
|  |      * @param content | ||||||
|  |      *            内容 | ||||||
|  |      * @param to | ||||||
|  |      *            收件人 | ||||||
|  |      * @throws MessagingException | ||||||
|  |      *             / | ||||||
|  |      */ | ||||||
|  |     public static void sendHtml(String to, String subject, String content) throws MessagingException { | ||||||
|  |         send(splitAddress(to), null, null, subject, content, true); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 发送 HTML 邮件给单个人 | ||||||
|  |      * | ||||||
|  |      * @param subject | ||||||
|  |      *            主题 | ||||||
|  |      * @param content | ||||||
|  |      *            内容 | ||||||
|  |      * @param to | ||||||
|  |      *            收件人 | ||||||
|  |      * @param files | ||||||
|  |      *            附件列表 | ||||||
|  |      * @throws MessagingException | ||||||
|  |      *             / | ||||||
|  |      */ | ||||||
|  |     public static void sendHtml(String to, String subject, String content, File... files) throws MessagingException { | ||||||
|  |         send(splitAddress(to), null, null, subject, content, true, files); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 发送 HTML 邮件给多个人 | ||||||
|  |      * | ||||||
|  |      * @param subject | ||||||
|  |      *            主题 | ||||||
|  |      * @param content | ||||||
|  |      *            内容 | ||||||
|  |      * @param tos | ||||||
|  |      *            收件人列表 | ||||||
|  |      * @param files | ||||||
|  |      *            附件列表 | ||||||
|  |      * @throws MessagingException | ||||||
|  |      *             / | ||||||
|  |      */ | ||||||
|  |     public static void sendHtml(Collection<String> tos, String subject, String content, File... files) | ||||||
|  |         throws MessagingException { | ||||||
|  |         send(tos, null, null, subject, content, true, files); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 发送 HTML 邮件给多个人 | ||||||
|  |      * | ||||||
|  |      * @param subject | ||||||
|  |      *            主题 | ||||||
|  |      * @param content | ||||||
|  |      *            内容 | ||||||
|  |      * @param tos | ||||||
|  |      *            收件人列表 | ||||||
|  |      * @param ccs | ||||||
|  |      *            抄送人列表 | ||||||
|  |      * @param files | ||||||
|  |      *            附件列表 | ||||||
|  |      * @throws MessagingException | ||||||
|  |      *             / | ||||||
|  |      */ | ||||||
|  |     public static void sendHtml(Collection<String> tos, Collection<String> ccs, String subject, String content, | ||||||
|  |         File... files) throws MessagingException { | ||||||
|  |         send(tos, ccs, null, subject, content, true, files); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 发送 HTML 邮件给多个人 | ||||||
|  |      * | ||||||
|  |      * @param subject | ||||||
|  |      *            主题 | ||||||
|  |      * @param content | ||||||
|  |      *            内容 | ||||||
|  |      * @param tos | ||||||
|  |      *            收件人列表 | ||||||
|  |      * @param ccs | ||||||
|  |      *            抄送人列表 | ||||||
|  |      * @param bccs | ||||||
|  |      *            密送人列表 | ||||||
|  |      * @param files | ||||||
|  |      *            附件列表 | ||||||
|  |      * @throws MessagingException | ||||||
|  |      *             / | ||||||
|  |      */ | ||||||
|  |     public static void sendHtml(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, | ||||||
|  |         String content, File... files) throws MessagingException { | ||||||
|  |         send(tos, ccs, bccs, subject, content, true, files); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 发送邮件给多个人 | ||||||
|  |      * | ||||||
|  |      * @param tos | ||||||
|  |      *            收件人列表 | ||||||
|  |      * @param ccs | ||||||
|  |      *            抄送人列表 | ||||||
|  |      * @param bccs | ||||||
|  |      *            密送人列表 | ||||||
|  |      * @param subject | ||||||
|  |      *            主题 | ||||||
|  |      * @param content | ||||||
|  |      *            内容 | ||||||
|  |      * @param isHtml | ||||||
|  |      *            是否是 HTML | ||||||
|  |      * @param files | ||||||
|  |      *            附件列表 | ||||||
|  |      * @throws MessagingException | ||||||
|  |      *             / | ||||||
|  |      */ | ||||||
|  |     public static void send(Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, | ||||||
|  |         String content, boolean isHtml, File... files) throws MessagingException { | ||||||
|  |         CheckUtils.exIfCondition(() -> CollUtil.isEmpty(tos), "请至少指定一名收件人"); | ||||||
|  |         MimeMessage mimeMessage = MAIL_SENDER.createMimeMessage(); | ||||||
|  |         MimeMessageHelper messageHelper = | ||||||
|  |             new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.displayName()); | ||||||
|  |  | ||||||
|  |         // 设置基本信息 | ||||||
|  |         messageHelper.setFrom(SpringUtil.getProperty("spring.mail.username")); | ||||||
|  |         messageHelper.setSubject(subject); | ||||||
|  |         messageHelper.setText(content, isHtml); | ||||||
|  |  | ||||||
|  |         // 设置收信人 | ||||||
|  |         // 抄送人 | ||||||
|  |         if (CollUtil.isNotEmpty(ccs)) { | ||||||
|  |             messageHelper.setCc(ccs.toArray(new String[0])); | ||||||
|  |         } | ||||||
|  |         // 密送人 | ||||||
|  |         if (CollUtil.isNotEmpty(bccs)) { | ||||||
|  |             messageHelper.setBcc(bccs.toArray(new String[0])); | ||||||
|  |         } | ||||||
|  |         // 收件人 | ||||||
|  |         messageHelper.setTo(tos.toArray(new String[0])); | ||||||
|  |  | ||||||
|  |         // 设置附件 | ||||||
|  |         if (ArrayUtil.isNotEmpty(files)) { | ||||||
|  |             for (File file : files) { | ||||||
|  |                 messageHelper.addAttachment(file.getName(), file); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 发送邮件 | ||||||
|  |         MAIL_SENDER.send(mimeMessage); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 将多个联系人转为列表,分隔符为逗号或者分号 | ||||||
|  |      * | ||||||
|  |      * @param addresses | ||||||
|  |      *            多个联系人,如果为空返回null | ||||||
|  |      * @return 联系人列表 | ||||||
|  |      */ | ||||||
|  |     private static List<String> splitAddress(String addresses) { | ||||||
|  |         if (StrUtil.isBlank(addresses)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         List<String> result; | ||||||
|  |         if (StrUtil.contains(addresses, CharUtil.COMMA)) { | ||||||
|  |             result = StrUtil.splitTrim(addresses, CharUtil.COMMA); | ||||||
|  |         } else if (StrUtil.contains(addresses, ';')) { | ||||||
|  |             result = StrUtil.splitTrim(addresses, ';'); | ||||||
|  |         } else { | ||||||
|  |             result = CollUtil.newArrayList(addresses); | ||||||
|  |         } | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package top.charles7c.cnadmin.common.util; | ||||||
|  |  | ||||||
|  | import java.util.Map; | ||||||
|  |  | ||||||
|  | import lombok.AccessLevel; | ||||||
|  | import lombok.NoArgsConstructor; | ||||||
|  |  | ||||||
|  | import cn.hutool.extra.template.Template; | ||||||
|  | import cn.hutool.extra.template.TemplateConfig; | ||||||
|  | import cn.hutool.extra.template.TemplateEngine; | ||||||
|  | import cn.hutool.extra.template.TemplateUtil; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 模板工具类 | ||||||
|  |  * | ||||||
|  |  * @author Charles7c | ||||||
|  |  * @since 2023/1/13 20:37 | ||||||
|  |  */ | ||||||
|  | @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||||
|  | public class TemplateUtils { | ||||||
|  |  | ||||||
|  |     private static final String TEMPLATE_PARENT_PATH = "templates"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 将模板与绑定参数融合后返回为字符串 | ||||||
|  |      * | ||||||
|  |      * @param bindingMap | ||||||
|  |      *            绑定的参数,此Map中的参数会替换模板中的变量 | ||||||
|  |      * @return 融合后的内容 | ||||||
|  |      */ | ||||||
|  |     public static String render(String templatePath, Map<?, ?> bindingMap) { | ||||||
|  |         TemplateEngine engine = | ||||||
|  |             TemplateUtil.createEngine(new TemplateConfig(TEMPLATE_PARENT_PATH, TemplateConfig.ResourceMode.CLASSPATH)); | ||||||
|  |         Template template = engine.getTemplate(templatePath); | ||||||
|  |         return template.render(bindingMap); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -35,18 +35,6 @@ public class CheckUtils extends Validator { | |||||||
|  |  | ||||||
|     private static final Class<ServiceException> EXCEPTION_TYPE = ServiceException.class; |     private static final Class<ServiceException> EXCEPTION_TYPE = ServiceException.class; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 如果为空,抛出异常 |  | ||||||
|      * |  | ||||||
|      * @param obj |  | ||||||
|      *            被检测的对象 |  | ||||||
|      * @param message |  | ||||||
|      *            错误信息 |  | ||||||
|      */ |  | ||||||
|     public static void exIfNull(Object obj, String message) { |  | ||||||
|         exIfNull(obj, message, EXCEPTION_TYPE); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 如果为空,抛出异常 |      * 如果为空,抛出异常 | ||||||
|      * |      * | ||||||
| @@ -59,6 +47,18 @@ public class CheckUtils extends Validator { | |||||||
|         exIfBlank(str, message, EXCEPTION_TYPE); |         exIfBlank(str, message, EXCEPTION_TYPE); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param str | ||||||
|  |      *            被检测的字符串 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfNotBlank(CharSequence str, String message) { | ||||||
|  |         exIfNotBlank(str, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 如果相同,抛出异常 |      * 如果相同,抛出异常 | ||||||
|      * |      * | ||||||
| @@ -87,6 +87,58 @@ public class CheckUtils extends Validator { | |||||||
|         exIfNotEqual(obj1, obj2, message, EXCEPTION_TYPE); |         exIfNotEqual(obj1, obj2, message, EXCEPTION_TYPE); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果相同,抛出异常(不区分大小写) | ||||||
|  |      * | ||||||
|  |      * @param str1 | ||||||
|  |      *            要比较的字符串1 | ||||||
|  |      * @param str2 | ||||||
|  |      *            要比较的字符串2 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String message) { | ||||||
|  |         exIfEqualIgnoreCase(str1, str2, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不相同,抛出异常(不区分大小写) | ||||||
|  |      * | ||||||
|  |      * @param str1 | ||||||
|  |      *            要比较的字符串1 | ||||||
|  |      * @param str2 | ||||||
|  |      *            要比较的字符串2 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfNotEqualIgnoreCase(CharSequence str1, CharSequence str2, String message) { | ||||||
|  |         exIfNotEqualIgnoreCase(str1, str2, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param obj | ||||||
|  |      *            被检测的对象 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfNull(Object obj, String message) { | ||||||
|  |         exIfNull(obj, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param obj | ||||||
|  |      *            被检测的对象 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfNotNull(Object obj, String message) { | ||||||
|  |         exIfNotNull(obj, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 如果条件成立,抛出异常 |      * 如果条件成立,抛出异常 | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -35,18 +35,6 @@ public class ValidationUtils extends Validator { | |||||||
|  |  | ||||||
|     private static final Class<BadRequestException> EXCEPTION_TYPE = BadRequestException.class; |     private static final Class<BadRequestException> EXCEPTION_TYPE = BadRequestException.class; | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 如果为空,抛出异常 |  | ||||||
|      * |  | ||||||
|      * @param obj |  | ||||||
|      *            被检测的对象 |  | ||||||
|      * @param message |  | ||||||
|      *            错误信息 |  | ||||||
|      */ |  | ||||||
|     public static void exIfNull(Object obj, String message) { |  | ||||||
|         exIfNull(obj, message, EXCEPTION_TYPE); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 如果为空,抛出异常 |      * 如果为空,抛出异常 | ||||||
|      * |      * | ||||||
| @@ -59,6 +47,18 @@ public class ValidationUtils extends Validator { | |||||||
|         exIfBlank(str, message, EXCEPTION_TYPE); |         exIfBlank(str, message, EXCEPTION_TYPE); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param str | ||||||
|  |      *            被检测的字符串 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfNotBlank(CharSequence str, String message) { | ||||||
|  |         exIfNotBlank(str, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 如果相同,抛出异常 |      * 如果相同,抛出异常 | ||||||
|      * |      * | ||||||
| @@ -87,6 +87,58 @@ public class ValidationUtils extends Validator { | |||||||
|         exIfNotEqual(obj1, obj2, message, EXCEPTION_TYPE); |         exIfNotEqual(obj1, obj2, message, EXCEPTION_TYPE); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果相同,抛出异常(不区分大小写) | ||||||
|  |      * | ||||||
|  |      * @param str1 | ||||||
|  |      *            要比较的字符串1 | ||||||
|  |      * @param str2 | ||||||
|  |      *            要比较的字符串2 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String message) { | ||||||
|  |         exIfEqualIgnoreCase(str1, str2, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不相同,抛出异常(不区分大小写) | ||||||
|  |      * | ||||||
|  |      * @param str1 | ||||||
|  |      *            要比较的字符串1 | ||||||
|  |      * @param str2 | ||||||
|  |      *            要比较的字符串2 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfNotEqualIgnoreCase(CharSequence str1, CharSequence str2, String message) { | ||||||
|  |         exIfNotEqualIgnoreCase(str1, str2, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param obj | ||||||
|  |      *            被检测的对象 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfNull(Object obj, String message) { | ||||||
|  |         exIfNull(obj, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param obj | ||||||
|  |      *            被检测的对象 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      */ | ||||||
|  |     public static void exIfNotNull(Object obj, String message) { | ||||||
|  |         exIfNotNull(obj, message, EXCEPTION_TYPE); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 如果条件成立,抛出异常 |      * 如果条件成立,抛出异常 | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -25,6 +25,8 @@ import cn.hutool.core.util.ReflectUtil; | |||||||
| import cn.hutool.core.util.StrUtil; | import cn.hutool.core.util.StrUtil; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  |  * 校验器 | ||||||
|  |  * | ||||||
|  * @author Charles7c |  * @author Charles7c | ||||||
|  * @since 2023/1/2 22:12 |  * @since 2023/1/2 22:12 | ||||||
|  */ |  */ | ||||||
| @@ -32,23 +34,6 @@ import cn.hutool.core.util.StrUtil; | |||||||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||||||
| public class Validator { | public class Validator { | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * 如果为空,抛出异常 |  | ||||||
|      * |  | ||||||
|      * @param obj |  | ||||||
|      *            被检测的对象 |  | ||||||
|      * @param message |  | ||||||
|      *            错误信息 |  | ||||||
|      * @param exceptionType |  | ||||||
|      *            异常类型 |  | ||||||
|      */ |  | ||||||
|     protected static void exIfNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) { |  | ||||||
|         if (obj == null) { |  | ||||||
|             log.error(message); |  | ||||||
|             throw ReflectUtil.newInstance(exceptionType, message); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 如果为空,抛出异常 |      * 如果为空,抛出异常 | ||||||
|      * |      * | ||||||
| @@ -59,11 +44,23 @@ public class Validator { | |||||||
|      * @param exceptionType |      * @param exceptionType | ||||||
|      *            异常类型 |      *            异常类型 | ||||||
|      */ |      */ | ||||||
|     public static void exIfBlank(CharSequence str, String message, Class<? extends RuntimeException> exceptionType) { |     protected static void exIfBlank(CharSequence str, String message, Class<? extends RuntimeException> exceptionType) { | ||||||
|         if (StrUtil.isBlank(str)) { |         exIfCondition(() -> StrUtil.isBlank(str), message, exceptionType); | ||||||
|             log.error(message); |  | ||||||
|             throw ReflectUtil.newInstance(exceptionType, message); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param str | ||||||
|  |      *            被检测的字符串 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      * @param exceptionType | ||||||
|  |      *            异常类型 | ||||||
|  |      */ | ||||||
|  |     protected static void exIfNotBlank(CharSequence str, String message, | ||||||
|  |         Class<? extends RuntimeException> exceptionType) { | ||||||
|  |         exIfCondition(() -> StrUtil.isNotBlank(str), message, exceptionType); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -78,12 +75,9 @@ public class Validator { | |||||||
|      * @param exceptionType |      * @param exceptionType | ||||||
|      *            异常类型 |      *            异常类型 | ||||||
|      */ |      */ | ||||||
|     public static void exIfEqual(Object obj1, Object obj2, String message, |     protected static void exIfEqual(Object obj1, Object obj2, String message, | ||||||
|         Class<? extends RuntimeException> exceptionType) { |         Class<? extends RuntimeException> exceptionType) { | ||||||
|         if (ObjectUtil.equals(obj1, obj2)) { |         exIfCondition(() -> ObjectUtil.equal(obj1, obj2), message, exceptionType); | ||||||
|             log.error(message); |  | ||||||
|             throw ReflectUtil.newInstance(exceptionType, message); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -98,12 +92,71 @@ public class Validator { | |||||||
|      * @param exceptionType |      * @param exceptionType | ||||||
|      *            异常类型 |      *            异常类型 | ||||||
|      */ |      */ | ||||||
|     public static void exIfNotEqual(Object obj1, Object obj2, String message, |     protected static void exIfNotEqual(Object obj1, Object obj2, String message, | ||||||
|         Class<? extends RuntimeException> exceptionType) { |         Class<? extends RuntimeException> exceptionType) { | ||||||
|         if (ObjectUtil.notEqual(obj1, obj2)) { |         exIfCondition(() -> ObjectUtil.notEqual(obj1, obj2), message, exceptionType); | ||||||
|             log.error(message); |  | ||||||
|             throw ReflectUtil.newInstance(exceptionType, message); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果相同,抛出异常(不区分大小写) | ||||||
|  |      * | ||||||
|  |      * @param str1 | ||||||
|  |      *            要比较的字符串1 | ||||||
|  |      * @param str2 | ||||||
|  |      *            要比较的字符串2 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      * @param exceptionType | ||||||
|  |      *            异常类型 | ||||||
|  |      */ | ||||||
|  |     protected static void exIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String message, | ||||||
|  |         Class<? extends RuntimeException> exceptionType) { | ||||||
|  |         exIfCondition(() -> StrUtil.equalsIgnoreCase(str1, str2), message, exceptionType); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不相同,抛出异常(不区分大小写) | ||||||
|  |      * | ||||||
|  |      * @param str1 | ||||||
|  |      *            要比较的字符串1 | ||||||
|  |      * @param str2 | ||||||
|  |      *            要比较的字符串2 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      * @param exceptionType | ||||||
|  |      *            异常类型 | ||||||
|  |      */ | ||||||
|  |     protected static void exIfNotEqualIgnoreCase(CharSequence str1, CharSequence str2, String message, | ||||||
|  |         Class<? extends RuntimeException> exceptionType) { | ||||||
|  |         exIfCondition(() -> !StrUtil.equalsIgnoreCase(str1, str2), message, exceptionType); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param obj | ||||||
|  |      *            被检测的对象 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      * @param exceptionType | ||||||
|  |      *            异常类型 | ||||||
|  |      */ | ||||||
|  |     protected static void exIfNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) { | ||||||
|  |         exIfCondition(() -> obj == null, message, exceptionType); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 如果不为空,抛出异常 | ||||||
|  |      * | ||||||
|  |      * @param obj | ||||||
|  |      *            被检测的对象 | ||||||
|  |      * @param message | ||||||
|  |      *            错误信息 | ||||||
|  |      * @param exceptionType | ||||||
|  |      *            异常类型 | ||||||
|  |      */ | ||||||
|  |     protected static void exIfNotNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) { | ||||||
|  |         exIfCondition(() -> obj != null, message, exceptionType); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -116,7 +169,7 @@ public class Validator { | |||||||
|      * @param exceptionType |      * @param exceptionType | ||||||
|      *            异常类型 |      *            异常类型 | ||||||
|      */ |      */ | ||||||
|     public static void exIfCondition(java.util.function.BooleanSupplier conditionSupplier, String message, |     protected static void exIfCondition(java.util.function.BooleanSupplier conditionSupplier, String message, | ||||||
|         Class<? extends RuntimeException> exceptionType) { |         Class<? extends RuntimeException> exceptionType) { | ||||||
|         if (conditionSupplier != null && conditionSupplier.getAsBoolean()) { |         if (conditionSupplier != null && conditionSupplier.getAsBoolean()) { | ||||||
|             log.error(message); |             log.error(message); | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ import lombok.extern.slf4j.Slf4j; | |||||||
| import io.swagger.v3.oas.annotations.Operation; | import io.swagger.v3.oas.annotations.Operation; | ||||||
|  |  | ||||||
| import org.springframework.core.annotation.AnnotationUtils; | import org.springframework.core.annotation.AnnotationUtils; | ||||||
|  | import org.springframework.lang.NonNull; | ||||||
| import org.springframework.stereotype.Component; | import org.springframework.stereotype.Component; | ||||||
| import org.springframework.web.method.HandlerMethod; | import org.springframework.web.method.HandlerMethod; | ||||||
| import org.springframework.web.servlet.HandlerInterceptor; | import org.springframework.web.servlet.HandlerInterceptor; | ||||||
| @@ -65,20 +66,21 @@ import top.charles7c.cnadmin.monitor.model.entity.SysLog; | |||||||
| public class LogInterceptor implements HandlerInterceptor { | public class LogInterceptor implements HandlerInterceptor { | ||||||
|  |  | ||||||
|     private final LogProperties operationLogProperties; |     private final LogProperties operationLogProperties; | ||||||
|  |     private static final String ENCRYPT_SYMBOL = "****************"; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { |     public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, | ||||||
|         if (!checkIsNeedRecord(handler, request)) { |         @NonNull Object handler) { | ||||||
|             return true; |         if (checkIsNeedRecord(handler, request)) { | ||||||
|         } |  | ||||||
|  |  | ||||||
|             // 记录操作时间 |             // 记录操作时间 | ||||||
|             this.logCreateTime(); |             this.logCreateTime(); | ||||||
|  |         } | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) { |     public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, | ||||||
|  |         @NonNull Object handler, Exception e) { | ||||||
|         // 记录请求耗时及异常信息 |         // 记录请求耗时及异常信息 | ||||||
|         SysLog sysLog = this.logElapsedTimeAndException(); |         SysLog sysLog = this.logElapsedTimeAndException(); | ||||||
|         if (sysLog == null) { |         if (sysLog == null) { | ||||||
| @@ -203,6 +205,7 @@ public class LogInterceptor implements HandlerInterceptor { | |||||||
|      *            待脱敏数据 |      *            待脱敏数据 | ||||||
|      * @return 脱敏后的 JSON 字符串数据 |      * @return 脱敏后的 JSON 字符串数据 | ||||||
|      */ |      */ | ||||||
|  |     @SuppressWarnings("unchecked") | ||||||
|     private String desensitize(Map waitDesensitizeData) { |     private String desensitize(Map waitDesensitizeData) { | ||||||
|         String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData); |         String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData); | ||||||
|         try { |         try { | ||||||
| @@ -211,9 +214,9 @@ public class LogInterceptor implements HandlerInterceptor { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             for (String desensitizeProperty : operationLogProperties.getDesensitize()) { |             for (String desensitizeProperty : operationLogProperties.getDesensitize()) { | ||||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> "****************"); |                 waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> ENCRYPT_SYMBOL); | ||||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> "****************"); |                 waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> ENCRYPT_SYMBOL); | ||||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> "****************"); |                 waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> ENCRYPT_SYMBOL); | ||||||
|             } |             } | ||||||
|             return JSONUtil.toJsonStr(waitDesensitizeData); |             return JSONUtil.toJsonStr(waitDesensitizeData); | ||||||
|         } catch (Exception ignored) { |         } catch (Exception ignored) { | ||||||
|   | |||||||
| @@ -32,12 +32,6 @@ limitations under the License. | |||||||
|     <description>系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等)</description> |     <description>系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等)</description> | ||||||
|  |  | ||||||
|     <dependencies> |     <dependencies> | ||||||
|         <!-- Easy Captcha(Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目) --> |  | ||||||
|         <dependency> |  | ||||||
|             <groupId>com.github.whvcse</groupId> |  | ||||||
|             <artifactId>easy-captcha</artifactId> |  | ||||||
|         </dependency> |  | ||||||
|  |  | ||||||
|         <!-- 公共模块(存放公共工具类,公共配置等) --> |         <!-- 公共模块(存放公共工具类,公共配置等) --> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>top.charles7c</groupId> |             <groupId>top.charles7c</groupId> | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ public class LoginServiceImpl implements LoginService { | |||||||
|         // 校验 |         // 校验 | ||||||
|         ValidationUtils.exIfNull(sysUser, "用户名或密码错误"); |         ValidationUtils.exIfNull(sysUser, "用户名或密码错误"); | ||||||
|         Long userId = sysUser.getUserId(); |         Long userId = sysUser.getUserId(); | ||||||
|         ValidationUtils.exIfNotEqual(sysUser.getPassword(), SecureUtils.md5Salt(password, userId.toString()), |         ValidationUtils.exIfNotEqual(SecureUtils.md5Salt(password, userId.toString()), sysUser.getPassword(), | ||||||
|             "用户名或密码错误"); |             "用户名或密码错误"); | ||||||
|         ValidationUtils.exIfEqual(DisEnableStatusEnum.DISABLE, sysUser.getStatus(), "此账号已被禁用,如有疑问,请联系管理员"); |         ValidationUtils.exIfEqual(DisEnableStatusEnum.DISABLE, sysUser.getStatus(), "此账号已被禁用,如有疑问,请联系管理员"); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -52,6 +52,6 @@ public class UpdateBasicInfoRequest implements Serializable { | |||||||
|      * 性别(0未知 1男 2女) |      * 性别(0未知 1男 2女) | ||||||
|      */ |      */ | ||||||
|     @Schema(description = "性别(0未知 1男 2女)", type = "Integer", allowableValues = {"0", "1", "2"}) |     @Schema(description = "性别(0未知 1男 2女)", type = "Integer", allowableValues = {"0", "1", "2"}) | ||||||
|     @NotNull(message = "非法性别") |     @NotNull(message = "性别非法") | ||||||
|     private GenderEnum gender; |     private GenderEnum gender; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,66 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package top.charles7c.cnadmin.system.model.request; | ||||||
|  |  | ||||||
|  | import java.io.Serializable; | ||||||
|  |  | ||||||
|  | import javax.validation.constraints.NotBlank; | ||||||
|  | import javax.validation.constraints.Pattern; | ||||||
|  |  | ||||||
|  | import lombok.Data; | ||||||
|  |  | ||||||
|  | import io.swagger.v3.oas.annotations.media.Schema; | ||||||
|  |  | ||||||
|  | import org.hibernate.validator.constraints.Length; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.lang.RegexPool; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 修改邮箱信息 | ||||||
|  |  * | ||||||
|  |  * @author Charles7c | ||||||
|  |  * @since 2023/1/12 20:18 | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | @Schema(description = "修改邮箱信息") | ||||||
|  | public class UpdateEmailRequest implements Serializable { | ||||||
|  |  | ||||||
|  |     private static final long serialVersionUID = 1L; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 新邮箱 | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "新邮箱") | ||||||
|  |     @NotBlank(message = "新邮箱不能为空") | ||||||
|  |     @Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误") | ||||||
|  |     private String newEmail; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 验证码 | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "验证码") | ||||||
|  |     @NotBlank(message = "验证码不能为空") | ||||||
|  |     @Length(max = 6, message = "验证码非法") | ||||||
|  |     private String captcha; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 当前密码(加密后) | ||||||
|  |      */ | ||||||
|  |     @Schema(description = "当前密码(加密后)") | ||||||
|  |     @NotBlank(message = "当前密码不能为空") | ||||||
|  |     private String currentPassword; | ||||||
|  | } | ||||||
| @@ -67,4 +67,16 @@ public interface UserService { | |||||||
|      *            用户 ID |      *            用户 ID | ||||||
|      */ |      */ | ||||||
|     void updatePassword(String oldPassword, String newPassword, Long userId); |     void updatePassword(String oldPassword, String newPassword, Long userId); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 修改邮箱 | ||||||
|  |      * | ||||||
|  |      * @param newEmail | ||||||
|  |      *            新邮箱 | ||||||
|  |      * @param currentPassword | ||||||
|  |      *            当前密码 | ||||||
|  |      * @param userId | ||||||
|  |      *            用户ID | ||||||
|  |      */ | ||||||
|  |     void updateEmail(String newEmail, String currentPassword, Long userId); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -104,7 +104,7 @@ public class UserServiceImpl implements UserService { | |||||||
|     @Transactional(rollbackFor = Exception.class) |     @Transactional(rollbackFor = Exception.class) | ||||||
|     public void updatePassword(String oldPassword, String newPassword, Long userId) { |     public void updatePassword(String oldPassword, String newPassword, Long userId) { | ||||||
|         SysUser sysUser = this.getById(userId); |         SysUser sysUser = this.getById(userId); | ||||||
|         ValidationUtils.exIfNotEqual(sysUser.getPassword(), SecureUtils.md5Salt(oldPassword, userId.toString()), |         ValidationUtils.exIfNotEqual(SecureUtils.md5Salt(oldPassword, userId.toString()), sysUser.getPassword(), | ||||||
|             "当前密码错误"); |             "当前密码错误"); | ||||||
|  |  | ||||||
|         // 更新密码和密码重置时间 |         // 更新密码和密码重置时间 | ||||||
| @@ -120,6 +120,27 @@ public class UserServiceImpl implements UserService { | |||||||
|         LoginHelper.updateLoginUser(loginUser); |         LoginHelper.updateLoginUser(loginUser); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     @Transactional(rollbackFor = Exception.class) | ||||||
|  |     public void updateEmail(String newEmail, String currentPassword, Long userId) { | ||||||
|  |         // 校验 | ||||||
|  |         SysUser sysUser = this.getById(userId); | ||||||
|  |         ValidationUtils.exIfNotEqual(SecureUtils.md5Salt(currentPassword, userId.toString()), sysUser.getPassword(), | ||||||
|  |             "当前密码错误"); | ||||||
|  |         Long count = userMapper.selectCount(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getEmail, newEmail)); | ||||||
|  |         ValidationUtils.exIfCondition(() -> count > 0, "邮箱已绑定其他账号,请更换其他邮箱"); | ||||||
|  |         ValidationUtils.exIfEqual(newEmail, sysUser.getEmail(), "新邮箱不能与当前邮箱相同"); | ||||||
|  |  | ||||||
|  |         // 更新邮箱 | ||||||
|  |         userMapper.update(null, | ||||||
|  |             new LambdaUpdateWrapper<SysUser>().set(SysUser::getEmail, newEmail).eq(SysUser::getUserId, userId)); | ||||||
|  |  | ||||||
|  |         // 更新登录用户信息 | ||||||
|  |         LoginUser loginUser = LoginHelper.getLoginUser(); | ||||||
|  |         loginUser.setEmail(newEmail); | ||||||
|  |         LoginHelper.updateLoginUser(loginUser); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 根据 ID 查询 |      * 根据 ID 查询 | ||||||
|      * |      * | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "continew-admin-ui", |   "name": "continew-admin-ui", | ||||||
|   "description": "ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。", |   "description": "ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。", | ||||||
|   "version": "0.0.1-SNAPSHOT", |   "version": "0.0.1-SNAPSHOT", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "author": "Charles7c", |   "author": "Charles7c", | ||||||
|   | |||||||
| @@ -2,14 +2,6 @@ import axios from 'axios'; | |||||||
| import type { RouteRecordNormalized } from 'vue-router'; | import type { RouteRecordNormalized } from 'vue-router'; | ||||||
| import { UserState } from '@/store/modules/login/types'; | import { UserState } from '@/store/modules/login/types'; | ||||||
|  |  | ||||||
| export interface ImageCaptchaRes { |  | ||||||
|   uuid: string; |  | ||||||
|   img: string; |  | ||||||
| } |  | ||||||
| export function getImageCaptcha() { |  | ||||||
|   return axios.get<ImageCaptchaRes>('/captcha/img'); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface LoginReq { | export interface LoginReq { | ||||||
|   username: string; |   username: string; | ||||||
|   password: string; |   password: string; | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								continew-admin-ui/src/api/common/captcha.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								continew-admin-ui/src/api/common/captcha.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import axios from 'axios'; | ||||||
|  | import qs from 'query-string'; | ||||||
|  |  | ||||||
|  | export interface ImageCaptchaRes { | ||||||
|  |   uuid: string; | ||||||
|  |   img: string; | ||||||
|  | } | ||||||
|  | export function getImageCaptcha() { | ||||||
|  |   return axios.get<ImageCaptchaRes>('/common/captcha/img'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface MailCaptchaReq { | ||||||
|  |   email: string; | ||||||
|  | } | ||||||
|  | export function getMailCaptcha(params: MailCaptchaReq) { | ||||||
|  |   return axios.get('/common/captcha/mail', { | ||||||
|  |     params, | ||||||
|  |     paramsSerializer: (obj) => { | ||||||
|  |       return qs.stringify(obj); | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -28,3 +28,12 @@ export interface UpdatePasswordReq { | |||||||
| export function updatePassword(req: UpdatePasswordReq) { | export function updatePassword(req: UpdatePasswordReq) { | ||||||
|   return axios.patch('/system/user/center/password', req); |   return axios.patch('/system/user/center/password', req); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface UpdateEmailReq { | ||||||
|  |   newEmail: string; | ||||||
|  |   captcha: string; | ||||||
|  |   currentPassword: string; | ||||||
|  | } | ||||||
|  | export function updateEmail(req: UpdateEmailReq) { | ||||||
|  |   return axios.patch('/system/user/center/email', req); | ||||||
|  | } | ||||||
| @@ -2,6 +2,8 @@ | |||||||
|   <a-layout-footer class="footer"> |   <a-layout-footer class="footer"> | ||||||
|     {{ `Copyright © 2022-${new Date().getFullYear()} Charles7c` }} |     {{ `Copyright © 2022-${new Date().getFullYear()} Charles7c` }} | ||||||
|     <span> ⋅ </span> |     <span> ⋅ </span> | ||||||
|  |     <a href="https://github.com/Charles7c/continew-admin" target="_blank">{{ $t('title') }}</a> | ||||||
|  |     <span> ⋅ </span> | ||||||
|     <a href="https://beian.miit.gov.cn" target="_blank">津ICP备2022005864号-2</a> |     <a href="https://beian.miit.gov.cn" target="_blank">津ICP备2022005864号-2</a> | ||||||
|   </a-layout-footer> |   </a-layout-footer> | ||||||
| </template> | </template> | ||||||
|   | |||||||
| @@ -190,7 +190,7 @@ | |||||||
|   import useLocale from '@/hooks/locale'; |   import useLocale from '@/hooks/locale'; | ||||||
|   import useUser from '@/hooks/user'; |   import useUser from '@/hooks/user'; | ||||||
|   import Menu from '@/components/menu/index.vue'; |   import Menu from '@/components/menu/index.vue'; | ||||||
|   import getAvatar from "@/utils/avatar"; |   import getAvatar from '@/utils/avatar'; | ||||||
|   import MessageBox from '../message-box/index.vue'; |   import MessageBox from '../message-box/index.vue'; | ||||||
|  |  | ||||||
|   const appStore = useAppStore(); |   const appStore = useAppStore(); | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								continew-admin-ui/src/hooks/axios.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								continew-admin-ui/src/hooks/axios.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| import axios, { Axios, AxiosResponse, AxiosRequestConfig } from "axios"; | import axios, { Axios, AxiosResponse, AxiosRequestConfig } from 'axios'; | ||||||
|  |  | ||||||
| declare module "axios" { | declare module "axios" { | ||||||
|   interface AxiosResponse<T = any> { |   interface AxiosResponse<T = any> { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { useRouter } from 'vue-router'; | import { useRouter } from 'vue-router'; | ||||||
| import { useI18n } from "vue-i18n"; | import { useI18n } from 'vue-i18n'; | ||||||
| import { Message } from '@arco-design/web-vue'; | import { Message } from '@arco-design/web-vue'; | ||||||
|  |  | ||||||
| import { useLoginStore } from '@/store'; | import { useLoginStore } from '@/store'; | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import { defineStore } from 'pinia'; | import { defineStore } from 'pinia'; | ||||||
| import { | import { | ||||||
|   getImageCaptcha as getCaptcha, |  | ||||||
|   login as userLogin, |   login as userLogin, | ||||||
|   logout as userLogout, |   logout as userLogout, | ||||||
|   getUserInfo, |   getUserInfo, | ||||||
|   LoginReq, |   LoginReq, | ||||||
| } from '@/api/auth/login'; | } from '@/api/auth/login'; | ||||||
|  | import { getImageCaptcha as getCaptcha } from '@/api/common/captcha'; | ||||||
| import { setToken, clearToken } from '@/utils/auth'; | import { setToken, clearToken } from '@/utils/auth'; | ||||||
| import { removeRouteListener } from '@/utils/route-listener'; | import { removeRouteListener } from '@/utils/route-listener'; | ||||||
| import { UserState } from './types'; | import { UserState } from './types'; | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ | |||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|   import { useLoginStore } from '@/store'; |   import { useLoginStore } from '@/store'; | ||||||
|   import getAvatar from "@/utils/avatar"; |   import getAvatar from '@/utils/avatar'; | ||||||
|  |  | ||||||
|   const userInfo = useLoginStore(); |   const userInfo = useLoginStore(); | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ | |||||||
|           :placeholder="$t('login.form.placeholder.password')" |           :placeholder="$t('login.form.placeholder.password')" | ||||||
|           size="large" |           size="large" | ||||||
|           allow-clear |           allow-clear | ||||||
|           max-length="50" |           max-length="32" | ||||||
|         > |         > | ||||||
|           <template #prefix> |           <template #prefix> | ||||||
|             <icon-lock /> |             <icon-lock /> | ||||||
| @@ -82,13 +82,13 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|   import { ref, reactive, computed, onMounted } from "vue"; |   import { ref, reactive, computed, onMounted } from 'vue'; | ||||||
|   import { useRouter } from 'vue-router'; |   import { useRouter } from 'vue-router'; | ||||||
|   import { FieldRule, Message } from "@arco-design/web-vue"; |   import { FieldRule, Message } from '@arco-design/web-vue'; | ||||||
|   import { ValidatedError } from '@arco-design/web-vue/es/form/interface'; |   import { ValidatedError } from '@arco-design/web-vue/es/form/interface'; | ||||||
|   import { useI18n } from 'vue-i18n'; |   import { useI18n } from 'vue-i18n'; | ||||||
|   // import debug from '@/utils/env'; |   // import debug from '@/utils/env'; | ||||||
|   import { encryptByRsa } from "@/utils/encrypt"; |   import { encryptByRsa } from '@/utils/encrypt'; | ||||||
|   import { useStorage } from '@vueuse/core'; |   import { useStorage } from '@vueuse/core'; | ||||||
|   import { useLoginStore } from '@/store'; |   import { useLoginStore } from '@/store'; | ||||||
|   import useLoading from '@/hooks/loading'; |   import useLoading from '@/hooks/loading'; | ||||||
|   | |||||||
| @@ -53,14 +53,14 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|   import { ref, computed } from "vue"; |   import { ref, computed } from 'vue'; | ||||||
|   import { useI18n } from "vue-i18n"; |   import { useI18n } from 'vue-i18n'; | ||||||
|   import { useLoginStore } from '@/store'; |   import { useLoginStore } from '@/store'; | ||||||
|   import { updateBasicInfo } from '@/api/system/user-center'; |   import { updateBasicInfo } from '@/api/system/user-center'; | ||||||
|   import useLoading from '@/hooks/loading'; |   import useLoading from '@/hooks/loading'; | ||||||
|   import { FormInstance } from '@arco-design/web-vue/es/form'; |   import { FormInstance } from '@arco-design/web-vue/es/form'; | ||||||
|   import { BasicInfoModel } from '@/api/system/user-center'; |   import { BasicInfoModel } from '@/api/system/user-center'; | ||||||
|   import { FieldRule, Message } from "@arco-design/web-vue"; |   import { FieldRule, Message } from '@arco-design/web-vue'; | ||||||
|  |  | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
|   const { loading, setLoading } = useLoading(); |   const { loading, setLoading } = useLoading(); | ||||||
| @@ -84,8 +84,8 @@ | |||||||
|  |  | ||||||
|   // 保存 |   // 保存 | ||||||
|   const save = async () => { |   const save = async () => { | ||||||
|     const errors = await formRef.value?.validate(); |  | ||||||
|     if (loading.value) return; |     if (loading.value) return; | ||||||
|  |     const errors = await formRef.value?.validate(); | ||||||
|     if (!errors) { |     if (!errors) { | ||||||
|       setLoading(true); |       setLoading(true); | ||||||
|       try { |       try { | ||||||
|   | |||||||
| @@ -15,18 +15,206 @@ | |||||||
|         </a-typography-paragraph> |         </a-typography-paragraph> | ||||||
|       </div> |       </div> | ||||||
|       <div class="operation"> |       <div class="operation"> | ||||||
|         <a-link> |         <a-link @click="toUpdate"> | ||||||
|           {{ $t('userCenter.securitySettings.button.update') }} |           {{ $t('userCenter.securitySettings.button.update') }} | ||||||
|         </a-link> |         </a-link> | ||||||
|       </div> |       </div> | ||||||
|     </template> |     </template> | ||||||
|   </a-list-item-meta> |   </a-list-item-meta> | ||||||
|  |  | ||||||
|  |   <a-modal | ||||||
|  |     v-model:visible="visible" | ||||||
|  |     :title="$t('userCenter.securitySettings.updateEmail.modal.title')" | ||||||
|  |     :mask-closable="false" | ||||||
|  |     @cancel="handleCancel" | ||||||
|  |     @before-ok="handleUpdate" | ||||||
|  |   > | ||||||
|  |     <a-form ref="formRef" :model="formData" :rules="rules"> | ||||||
|  |       <a-form-item | ||||||
|  |         field="newEmail" | ||||||
|  |         :validate-trigger="['change', 'blur']" | ||||||
|  |         :label="$t('userCenter.securitySettings.updateEmail.form.label.newEmail')" | ||||||
|  |       > | ||||||
|  |         <a-input | ||||||
|  |           v-model="formData.newEmail" | ||||||
|  |           :placeholder="$t('userCenter.securitySettings.updateEmail.form.placeholder.newEmail')" | ||||||
|  |           size="large" | ||||||
|  |           allow-clear | ||||||
|  |         > | ||||||
|  |         </a-input> | ||||||
|  |       </a-form-item> | ||||||
|  |       <a-form-item | ||||||
|  |         field="captcha" | ||||||
|  |         :validate-trigger="['change', 'blur']" | ||||||
|  |         :label="$t('userCenter.securitySettings.updateEmail.form.label.captcha')" | ||||||
|  |       > | ||||||
|  |         <a-input | ||||||
|  |           v-model="formData.captcha" | ||||||
|  |           :placeholder="$t('userCenter.securitySettings.updateEmail.form.placeholder.captcha')" | ||||||
|  |           size="large" | ||||||
|  |           style="width: 80%" | ||||||
|  |           allow-clear | ||||||
|  |           max-length="6" | ||||||
|  |         > | ||||||
|  |         </a-input> | ||||||
|  |         <a-button | ||||||
|  |           class="captcha-btn" | ||||||
|  |           type="primary" | ||||||
|  |           size="large" | ||||||
|  |           :loading="captchaLoading" | ||||||
|  |           :disabled="captchaDisable" | ||||||
|  |           @click="sendCaptcha" | ||||||
|  |         > | ||||||
|  |           {{ captchaBtnName }} | ||||||
|  |         </a-button> | ||||||
|  |       </a-form-item> | ||||||
|  |       <a-form-item | ||||||
|  |         field="currentPassword" | ||||||
|  |         :validate-trigger="['change', 'blur']" | ||||||
|  |         :label="$t('userCenter.securitySettings.updateEmail.form.label.currentPassword')" | ||||||
|  |       > | ||||||
|  |         <a-input-password | ||||||
|  |           v-model="formData.currentPassword" | ||||||
|  |           :placeholder="$t('userCenter.securitySettings.updateEmail.form.placeholder.currentPassword')" | ||||||
|  |           size="large" | ||||||
|  |           allow-clear | ||||||
|  |           max-length="32" | ||||||
|  |         > | ||||||
|  |         </a-input-password> | ||||||
|  |       </a-form-item> | ||||||
|  |     </a-form> | ||||||
|  |   </a-modal> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  |   import { ref, reactive, computed } from 'vue'; | ||||||
|  |   import { useI18n } from 'vue-i18n'; | ||||||
|   import { useLoginStore } from '@/store'; |   import { useLoginStore } from '@/store'; | ||||||
|  |   import { FormInstance } from '@arco-design/web-vue/es/form'; | ||||||
|  |   import useLoading from '@/hooks/loading'; | ||||||
|  |   import { FieldRule, Message } from '@arco-design/web-vue'; | ||||||
|  |   import { getMailCaptcha } from '@/api/common/captcha'; | ||||||
|  |   import { updateEmail } from '@/api/system/user-center'; | ||||||
|  |   import { encryptByRsa } from '@/utils/encrypt'; | ||||||
|  |  | ||||||
|  |   const { t } = useI18n(); | ||||||
|  |   const { loading, setLoading } = useLoading(); | ||||||
|   const loginStore = useLoginStore(); |   const loginStore = useLoginStore(); | ||||||
|  |   const visible = ref(false); | ||||||
|  |   const captchaBtnNameKey = ref('userCenter.securitySettings.updateEmail.form.sendCaptcha'); | ||||||
|  |   const captchaBtnName = computed(() => t(captchaBtnNameKey.value)); | ||||||
|  |   const captchaLoading = ref(false); | ||||||
|  |   const captchaDisable = ref(false); | ||||||
|  |   const captchaTime = ref(60); | ||||||
|  |   const captchaTimer = ref(); | ||||||
|  |   const formRef = ref<FormInstance>(); | ||||||
|  |   const formData = reactive({ | ||||||
|  |     newEmail: '', | ||||||
|  |     captcha: '', | ||||||
|  |     currentPassword: '', | ||||||
|  |   }); | ||||||
|  |   const rules = computed((): Record<string, FieldRule[]> => { | ||||||
|  |     return { | ||||||
|  |       newEmail: [ | ||||||
|  |         { required: true, message: t('userCenter.securitySettings.updateEmail.form.error.required.newEmail') }, | ||||||
|  |         { type: 'email', message: t('userCenter.securitySettings.updateEmail.form.error.match.newEmail') }, | ||||||
|  |         { | ||||||
|  |           validator: (value, callback) => { | ||||||
|  |             if (value === loginStore.email) { | ||||||
|  |               callback(t('userCenter.securitySettings.updateEmail.form.error.validator.newEmail')) | ||||||
|  |             } else { | ||||||
|  |               callback() | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       captcha: [ | ||||||
|  |         { required: true, message: t('userCenter.securitySettings.updateEmail.form.error.required.captcha') } | ||||||
|  |       ], | ||||||
|  |       currentPassword: [ | ||||||
|  |         { required: true, message: t('userCenter.securitySettings.updateEmail.form.error.required.currentPassword') } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // 重置验证码相关 | ||||||
|  |   const resetCaptcha = () => { | ||||||
|  |     window.clearInterval(captchaTimer.value); | ||||||
|  |     captchaTime.value = 60; | ||||||
|  |     captchaBtnNameKey.value = 'userCenter.securitySettings.updateEmail.form.sendCaptcha'; | ||||||
|  |     captchaDisable.value = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // 发送验证码 | ||||||
|  |   const sendCaptcha = async () => { | ||||||
|  |     if (captchaLoading.value) return; | ||||||
|  |     const errors = await formRef.value?.validateField('newEmail'); | ||||||
|  |     if (errors) return; | ||||||
|  |     captchaLoading.value = true; | ||||||
|  |     captchaBtnNameKey.value = 'userCenter.securitySettings.updateEmail.form.loading.sendCaptcha'; | ||||||
|  |     try { | ||||||
|  |       const res = await getMailCaptcha({ | ||||||
|  |         email: formData.newEmail | ||||||
|  |       }); | ||||||
|  |       if (res.success) { | ||||||
|  |         captchaLoading.value = false; | ||||||
|  |         captchaDisable.value = true; | ||||||
|  |         captchaBtnNameKey.value = `${t('userCenter.securitySettings.updateEmail.form.reSendCaptcha')}(${captchaTime.value -= 1}s)`; | ||||||
|  |         Message.success(res.msg); | ||||||
|  |  | ||||||
|  |         captchaTimer.value = window.setInterval(function() { | ||||||
|  |           captchaTime.value -= 1; | ||||||
|  |           captchaBtnNameKey.value = `${t('userCenter.securitySettings.updateEmail.form.reSendCaptcha')}(${captchaTime.value}s)`; | ||||||
|  |           if (captchaTime.value < 0) { | ||||||
|  |             window.clearInterval(captchaTimer.value); | ||||||
|  |             captchaTime.value = 60; | ||||||
|  |             captchaBtnNameKey.value = t('userCenter.securitySettings.updateEmail.form.reSendCaptcha'); | ||||||
|  |             captchaDisable.value = false; | ||||||
|  |           } | ||||||
|  |         }, 1000) | ||||||
|  |       } | ||||||
|  |     } catch (err) { | ||||||
|  |       resetCaptcha(); | ||||||
|  |       captchaLoading.value = false; | ||||||
|  |       console.log((err as Error)); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // 确定修改 | ||||||
|  |   const handleUpdate = async () => { | ||||||
|  |     if (loading.value) return false; | ||||||
|  |     const errors = await formRef.value?.validate(); | ||||||
|  |     if (errors) return false; | ||||||
|  |     setLoading(true); | ||||||
|  |     try { | ||||||
|  |       const res = await updateEmail({ | ||||||
|  |         newEmail: formData.newEmail, | ||||||
|  |         captcha: formData.captcha, | ||||||
|  |         currentPassword: encryptByRsa(formData.currentPassword) || '', | ||||||
|  |       }); | ||||||
|  |       await loginStore.getInfo(); | ||||||
|  |       if (res.success) Message.success(res.msg); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // 取消修改 | ||||||
|  |   const handleCancel = () => { | ||||||
|  |     visible.value = false; | ||||||
|  |     formRef.value?.resetFields(); | ||||||
|  |     resetCaptcha(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // 打开修改窗口 | ||||||
|  |   const toUpdate = () => { | ||||||
|  |     visible.value = true; | ||||||
|  |   }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style scoped lang="less"></style> | <style scoped lang="less"> | ||||||
|  |   .captcha-btn { | ||||||
|  |     margin-left: 5px; | ||||||
|  |   } | ||||||
|  | </style> | ||||||
|   | |||||||
| @@ -22,12 +22,14 @@ | |||||||
|     </template> |     </template> | ||||||
|   </a-list-item-meta> |   </a-list-item-meta> | ||||||
|  |  | ||||||
|   <a-modal v-model:visible="visible" :title="$t('userCenter.securitySettings.updatePwd.modal.title')" @cancel="handleCancel" @before-ok="handleUpdate"> |   <a-modal | ||||||
|     <a-form |     v-model:visible="visible" | ||||||
|       ref="formRef" |     :title="$t('userCenter.securitySettings.updatePwd.modal.title')" | ||||||
|       :model="formData" |     :mask-closable="false" | ||||||
|       :rules="rules" |     @cancel="handleCancel" | ||||||
|  |     @before-ok="handleUpdate" | ||||||
|   > |   > | ||||||
|  |     <a-form ref="formRef" :model="formData" :rules="rules"> | ||||||
|       <a-form-item |       <a-form-item | ||||||
|         field="oldPassword" |         field="oldPassword" | ||||||
|         :validate-trigger="['change', 'blur']" |         :validate-trigger="['change', 'blur']" | ||||||
| @@ -38,7 +40,7 @@ | |||||||
|           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.oldPassword')" |           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.oldPassword')" | ||||||
|           size="large" |           size="large" | ||||||
|           allow-clear |           allow-clear | ||||||
|           max-length="50" |           max-length="32" | ||||||
|         > |         > | ||||||
|         </a-input-password> |         </a-input-password> | ||||||
|       </a-form-item> |       </a-form-item> | ||||||
| @@ -52,7 +54,7 @@ | |||||||
|           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.newPassword')" |           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.newPassword')" | ||||||
|           size="large" |           size="large" | ||||||
|           allow-clear |           allow-clear | ||||||
|           max-length="50" |           max-length="32" | ||||||
|         > |         > | ||||||
|         </a-input-password> |         </a-input-password> | ||||||
|       </a-form-item> |       </a-form-item> | ||||||
| @@ -66,7 +68,7 @@ | |||||||
|           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.rePassword')" |           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.rePassword')" | ||||||
|           size="large" |           size="large" | ||||||
|           allow-clear |           allow-clear | ||||||
|           max-length="50" |           max-length="32" | ||||||
|         > |         > | ||||||
|         </a-input-password> |         </a-input-password> | ||||||
|       </a-form-item> |       </a-form-item> | ||||||
| @@ -75,14 +77,14 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|   import { ref, reactive, computed } from "vue"; |   import { ref, reactive, computed } from 'vue'; | ||||||
|   import { useI18n } from "vue-i18n"; |   import { useI18n } from 'vue-i18n'; | ||||||
|   import { useLoginStore } from '@/store'; |   import { useLoginStore } from '@/store'; | ||||||
|   import { FormInstance } from "@arco-design/web-vue/es/form"; |   import { FormInstance } from '@arco-design/web-vue/es/form'; | ||||||
|   import useLoading from "@/hooks/loading"; |   import useLoading from '@/hooks/loading'; | ||||||
|   import { FieldRule, Message } from "@arco-design/web-vue"; |   import { FieldRule, Message } from '@arco-design/web-vue'; | ||||||
|   import { updatePassword } from "@/api/system/user-center"; |   import { updatePassword } from '@/api/system/user-center'; | ||||||
|   import { encryptByRsa } from "@/utils/encrypt"; |   import { encryptByRsa } from '@/utils/encrypt'; | ||||||
|  |  | ||||||
|   const { t } = useI18n(); |   const { t } = useI18n(); | ||||||
|   const { loading, setLoading } = useLoading(); |   const { loading, setLoading } = useLoading(); | ||||||
| @@ -129,8 +131,8 @@ | |||||||
|  |  | ||||||
|   // 确定修改 |   // 确定修改 | ||||||
|   const handleUpdate = async () => { |   const handleUpdate = async () => { | ||||||
|     const errors = await formRef.value?.validate(); |  | ||||||
|     if (loading.value) return false; |     if (loading.value) return false; | ||||||
|  |     const errors = await formRef.value?.validate(); | ||||||
|     if (errors) return false; |     if (errors) return false; | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     try { |     try { | ||||||
|   | |||||||
| @@ -61,8 +61,8 @@ | |||||||
|   } from '@arco-design/web-vue/es/upload/interfaces'; |   } from '@arco-design/web-vue/es/upload/interfaces'; | ||||||
|   import { useLoginStore } from '@/store'; |   import { useLoginStore } from '@/store'; | ||||||
|   import { uploadAvatar } from '@/api/system/user-center'; |   import { uploadAvatar } from '@/api/system/user-center'; | ||||||
|   import getAvatar from "@/utils/avatar"; |   import getAvatar from '@/utils/avatar'; | ||||||
|   import { Message } from "@arco-design/web-vue"; |   import { Message } from '@arco-design/web-vue'; | ||||||
|  |  | ||||||
|   const loginStore = useLoginStore(); |   const loginStore = useLoginStore(); | ||||||
|   const avatar = { |   const avatar = { | ||||||
|   | |||||||
| @@ -63,5 +63,23 @@ export default { | |||||||
|   'userCenter.securitySettings.updateEmail.placeholder.error.email': |   'userCenter.securitySettings.updateEmail.placeholder.error.email': | ||||||
|     'You have not set a mailbox yet. The mailbox binding can be used to retrieve passwords and receive notifications.', |     'You have not set a mailbox yet. The mailbox binding can be used to retrieve passwords and receive notifications.', | ||||||
|  |  | ||||||
|  |   'userCenter.securitySettings.updateEmail.modal.title': 'Update email', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.label.newEmail': 'New email', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.label.captcha': 'Captcha', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.label.currentPassword': 'Current password', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.sendCaptcha': 'Send captcha', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.reSendCaptcha': 'Resend captcha', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.loading.sendCaptcha': 'Sending...', | ||||||
|  |  | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.placeholder.newEmail': 'Please enter new email', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.placeholder.captcha': 'Please enter email captcha', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.placeholder.currentPassword': 'Please enter current password', | ||||||
|  |  | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.required.newEmail': 'Please enter new email', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.match.newEmail': 'Please enter the correct email', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.validator.newEmail': 'New email cannot be the same as the old email', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.required.captcha': 'Please enter email captcha', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.required.currentPassword': 'Please enter current password', | ||||||
|  |  | ||||||
|   'userCenter.securitySettings.button.update': 'Update', |   'userCenter.securitySettings.button.update': 'Update', | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -63,5 +63,23 @@ export default { | |||||||
|   'userCenter.securitySettings.updateEmail.placeholder.error.email': |   'userCenter.securitySettings.updateEmail.placeholder.error.email': | ||||||
|     '您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。', |     '您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。', | ||||||
|  |  | ||||||
|  |   'userCenter.securitySettings.updateEmail.modal.title': '修改邮箱', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.label.newEmail': '新邮箱', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.label.captcha': '验证码', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.label.currentPassword': '当前密码', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.sendCaptcha': '发送验证码', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.reSendCaptcha': '重新发送', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.loading.sendCaptcha': '发送中...', | ||||||
|  |  | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.placeholder.newEmail': '请输入新邮箱', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.placeholder.captcha': '请输入邮箱验证码', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.placeholder.currentPassword': '请输入当前密码', | ||||||
|  |  | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.required.newEmail': '请输入新邮箱', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.match.newEmail': '请输入正确的邮箱', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.validator.newEmail': '新邮箱不能与当前邮箱相同', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.required.captcha': '请输入邮箱验证码', | ||||||
|  |   'userCenter.securitySettings.updateEmail.form.error.required.currentPassword': '请输入当前密码', | ||||||
|  |  | ||||||
|   'userCenter.securitySettings.button.update': '修改', |   'userCenter.securitySettings.button.update': '修改', | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,72 +0,0 @@ | |||||||
| /* |  | ||||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. |  | ||||||
|  * |  | ||||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
|  * you may not use this file except in compliance with the License. |  | ||||||
|  * You may obtain a copy of the License at |  | ||||||
|  * |  | ||||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
|  * |  | ||||||
|  * Unless required by applicable law or agreed to in writing, software |  | ||||||
|  * distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
|  * See the License for the specific language governing permissions and |  | ||||||
|  * limitations under the License. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| package top.charles7c.cnadmin.webapi.controller.auth; |  | ||||||
|  |  | ||||||
| import java.time.Duration; |  | ||||||
|  |  | ||||||
| import lombok.RequiredArgsConstructor; |  | ||||||
|  |  | ||||||
| import io.swagger.v3.oas.annotations.Operation; |  | ||||||
| import io.swagger.v3.oas.annotations.tags.Tag; |  | ||||||
|  |  | ||||||
| import org.springframework.http.MediaType; |  | ||||||
| import org.springframework.web.bind.annotation.GetMapping; |  | ||||||
| import org.springframework.web.bind.annotation.RequestMapping; |  | ||||||
| import org.springframework.web.bind.annotation.RestController; |  | ||||||
|  |  | ||||||
| import com.wf.captcha.base.Captcha; |  | ||||||
|  |  | ||||||
| import cn.dev33.satoken.annotation.SaIgnore; |  | ||||||
| import cn.hutool.core.util.IdUtil; |  | ||||||
|  |  | ||||||
| import top.charles7c.cnadmin.auth.config.properties.CaptchaProperties; |  | ||||||
| import top.charles7c.cnadmin.auth.model.vo.CaptchaVO; |  | ||||||
| import top.charles7c.cnadmin.common.model.vo.R; |  | ||||||
| import top.charles7c.cnadmin.common.util.RedisUtils; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 验证码 API |  | ||||||
|  * |  | ||||||
|  * @author Charles7c |  | ||||||
|  * @since 2022/12/11 14:00 |  | ||||||
|  */ |  | ||||||
| @Tag(name = "验证码 API") |  | ||||||
| @SaIgnore |  | ||||||
| @RestController |  | ||||||
| @RequiredArgsConstructor |  | ||||||
| @RequestMapping(value = "/captcha", produces = MediaType.APPLICATION_JSON_VALUE) |  | ||||||
| public class CaptchaController { |  | ||||||
|  |  | ||||||
|     private final CaptchaProperties captchaProperties; |  | ||||||
|  |  | ||||||
|     @Operation(summary = "获取图片验证码", description = "获取图片验证码(Base64编码,带图片格式:data:image/gif;base64)") |  | ||||||
|     @GetMapping("/img") |  | ||||||
|     public R<CaptchaVO> getImageCaptcha() { |  | ||||||
|         // 生成验证码 |  | ||||||
|         Captcha captcha = captchaProperties.getCaptcha(); |  | ||||||
|  |  | ||||||
|         // 保存验证码 |  | ||||||
|         String uuid = IdUtil.fastSimpleUUID(); |  | ||||||
|         String captchaKey = RedisUtils.formatKey(captchaProperties.getKeyPrefix(), uuid); |  | ||||||
|         RedisUtils.setCacheObject(captchaKey, captcha.text(), |  | ||||||
|             Duration.ofMinutes(captchaProperties.getExpirationInMinutes())); |  | ||||||
|  |  | ||||||
|         // 返回验证码 |  | ||||||
|         CaptchaVO captchaVo = new CaptchaVO().setUuid(uuid).setImg(captcha.toBase64()); |  | ||||||
|         return R.ok(captchaVo); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -31,11 +31,12 @@ import cn.dev33.satoken.annotation.SaIgnore; | |||||||
| import cn.dev33.satoken.stp.StpUtil; | import cn.dev33.satoken.stp.StpUtil; | ||||||
| import cn.hutool.core.bean.BeanUtil; | import cn.hutool.core.bean.BeanUtil; | ||||||
|  |  | ||||||
| import top.charles7c.cnadmin.auth.config.properties.CaptchaProperties; |  | ||||||
| import top.charles7c.cnadmin.auth.model.request.LoginRequest; | import top.charles7c.cnadmin.auth.model.request.LoginRequest; | ||||||
| import top.charles7c.cnadmin.auth.model.vo.LoginVO; | import top.charles7c.cnadmin.auth.model.vo.LoginVO; | ||||||
| import top.charles7c.cnadmin.auth.model.vo.UserInfoVO; | import top.charles7c.cnadmin.auth.model.vo.UserInfoVO; | ||||||
| import top.charles7c.cnadmin.auth.service.LoginService; | import top.charles7c.cnadmin.auth.service.LoginService; | ||||||
|  | import top.charles7c.cnadmin.common.config.properties.CaptchaProperties; | ||||||
|  | import top.charles7c.cnadmin.common.consts.CacheConstants; | ||||||
| import top.charles7c.cnadmin.common.model.dto.LoginUser; | import top.charles7c.cnadmin.common.model.dto.LoginUser; | ||||||
| import top.charles7c.cnadmin.common.model.vo.R; | import top.charles7c.cnadmin.common.model.vo.R; | ||||||
| import top.charles7c.cnadmin.common.util.ExceptionUtils; | import top.charles7c.cnadmin.common.util.ExceptionUtils; | ||||||
| @@ -64,11 +65,11 @@ public class LoginController { | |||||||
|     @PostMapping("/login") |     @PostMapping("/login") | ||||||
|     public R<LoginVO> login(@Validated @RequestBody LoginRequest loginRequest) { |     public R<LoginVO> login(@Validated @RequestBody LoginRequest loginRequest) { | ||||||
|         // 校验验证码 |         // 校验验证码 | ||||||
|         String captchaKey = RedisUtils.formatKey(captchaProperties.getKeyPrefix(), loginRequest.getUuid()); |         String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, loginRequest.getUuid()); | ||||||
|         String captcha = RedisUtils.getCacheObject(captchaKey); |         String captcha = RedisUtils.getCacheObject(captchaKey); | ||||||
|         ValidationUtils.exIfBlank(captcha, "验证码已失效"); |         ValidationUtils.exIfBlank(captcha, "验证码已失效"); | ||||||
|         RedisUtils.deleteCacheObject(captchaKey); |         RedisUtils.deleteCacheObject(captchaKey); | ||||||
|         ValidationUtils.exIfCondition(() -> !captcha.equalsIgnoreCase(loginRequest.getCaptcha()), "验证码错误"); |         ValidationUtils.exIfNotEqualIgnoreCase(loginRequest.getCaptcha(), captcha, "验证码错误"); | ||||||
|  |  | ||||||
|         // 用户登录 |         // 用户登录 | ||||||
|         String rawPassword = |         String rawPassword = | ||||||
| @@ -84,7 +85,6 @@ public class LoginController { | |||||||
|         in = ParameterIn.HEADER) |         in = ParameterIn.HEADER) | ||||||
|     @PostMapping("/logout") |     @PostMapping("/logout") | ||||||
|     public R logout() { |     public R logout() { | ||||||
|         ValidationUtils.exIfCondition(() -> !StpUtil.isLogin(), "Token 无效"); |  | ||||||
|         StpUtil.logout(); |         StpUtil.logout(); | ||||||
|         return R.ok(); |         return R.ok(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,116 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package top.charles7c.cnadmin.webapi.controller.common; | ||||||
|  |  | ||||||
|  | import java.time.Duration; | ||||||
|  |  | ||||||
|  | import javax.mail.MessagingException; | ||||||
|  | import javax.validation.constraints.NotBlank; | ||||||
|  | import javax.validation.constraints.Pattern; | ||||||
|  |  | ||||||
|  | import lombok.RequiredArgsConstructor; | ||||||
|  |  | ||||||
|  | import io.swagger.v3.oas.annotations.Operation; | ||||||
|  | import io.swagger.v3.oas.annotations.tags.Tag; | ||||||
|  |  | ||||||
|  | import org.springframework.http.MediaType; | ||||||
|  | import org.springframework.validation.annotation.Validated; | ||||||
|  | import org.springframework.web.bind.annotation.GetMapping; | ||||||
|  | import org.springframework.web.bind.annotation.RequestMapping; | ||||||
|  | import org.springframework.web.bind.annotation.RestController; | ||||||
|  |  | ||||||
|  | import com.wf.captcha.base.Captcha; | ||||||
|  |  | ||||||
|  | import cn.dev33.satoken.annotation.SaIgnore; | ||||||
|  | import cn.hutool.core.lang.Dict; | ||||||
|  | import cn.hutool.core.lang.RegexPool; | ||||||
|  | import cn.hutool.core.util.IdUtil; | ||||||
|  | import cn.hutool.core.util.RandomUtil; | ||||||
|  |  | ||||||
|  | import top.charles7c.cnadmin.common.config.properties.CaptchaProperties; | ||||||
|  | import top.charles7c.cnadmin.common.config.properties.ContinewAdminProperties; | ||||||
|  | import top.charles7c.cnadmin.common.consts.CacheConstants; | ||||||
|  | import top.charles7c.cnadmin.common.model.vo.CaptchaVO; | ||||||
|  | import top.charles7c.cnadmin.common.model.vo.R; | ||||||
|  | import top.charles7c.cnadmin.common.util.*; | ||||||
|  | import top.charles7c.cnadmin.common.util.validate.ValidationUtils; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 验证码 API | ||||||
|  |  * | ||||||
|  |  * @author Charles7c | ||||||
|  |  * @since 2022/12/11 14:00 | ||||||
|  |  */ | ||||||
|  | @Tag(name = "验证码 API") | ||||||
|  | @SaIgnore | ||||||
|  | @Validated | ||||||
|  | @RestController | ||||||
|  | @RequiredArgsConstructor | ||||||
|  | @RequestMapping(value = "/common/captcha", produces = MediaType.APPLICATION_JSON_VALUE) | ||||||
|  | public class CaptchaController { | ||||||
|  |  | ||||||
|  |     private final CaptchaProperties captchaProperties; | ||||||
|  |     private final ContinewAdminProperties properties; | ||||||
|  |  | ||||||
|  |     @Operation(summary = "获取图片验证码", description = "获取图片验证码(Base64编码,带图片格式:data:image/gif;base64)") | ||||||
|  |     @GetMapping("/img") | ||||||
|  |     public R<CaptchaVO> getImageCaptcha() { | ||||||
|  |         // 生成验证码 | ||||||
|  |         CaptchaProperties.CaptchaImage captchaImage = captchaProperties.getImage(); | ||||||
|  |         Captcha captcha = captchaImage.getCaptcha(); | ||||||
|  |  | ||||||
|  |         // 保存验证码 | ||||||
|  |         String uuid = IdUtil.fastSimpleUUID(); | ||||||
|  |         String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, uuid); | ||||||
|  |         RedisUtils.setCacheObject(captchaKey, captcha.text(), | ||||||
|  |             Duration.ofMinutes(captchaImage.getExpirationInMinutes())); | ||||||
|  |  | ||||||
|  |         // 返回验证码 | ||||||
|  |         CaptchaVO captchaVo = new CaptchaVO().setUuid(uuid).setImg(captcha.toBase64()); | ||||||
|  |         return R.ok(captchaVo); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Operation(summary = "获取邮箱验证码", description = "发送验证码到指定邮箱") | ||||||
|  |     @GetMapping("/mail") | ||||||
|  |     public R getMailCaptcha( | ||||||
|  |         @NotBlank(message = "邮箱不能为空") @Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误") String email) | ||||||
|  |         throws MessagingException { | ||||||
|  |         // 校验 | ||||||
|  |         String limitCacheKey = CacheConstants.LIMIT_CACHE_KEY; | ||||||
|  |         String captchaCacheKey = CacheConstants.CAPTCHA_CACHE_KEY; | ||||||
|  |         String limitCaptchaKey = RedisUtils.formatKey(limitCacheKey, captchaCacheKey, email); | ||||||
|  |         long limitTimeInMillisecond = RedisUtils.getTimeToLive(limitCaptchaKey); | ||||||
|  |         ValidationUtils.exIfCondition(() -> limitTimeInMillisecond > 0, | ||||||
|  |             String.format("发送邮箱验证码过于频繁,请您 %ds 后再试", limitTimeInMillisecond / 1000)); | ||||||
|  |  | ||||||
|  |         // 生成验证码 | ||||||
|  |         CaptchaProperties.CaptchaMail captchaMail = captchaProperties.getMail(); | ||||||
|  |         String captcha = RandomUtil.randomNumbers(captchaMail.getLength()); | ||||||
|  |  | ||||||
|  |         // 发送验证码 | ||||||
|  |         Long expirationInMinutes = captchaMail.getExpirationInMinutes(); | ||||||
|  |         String content = TemplateUtils.render(captchaMail.getTemplatePath(), | ||||||
|  |             Dict.create().set("captcha", captcha).set("expiration", expirationInMinutes)); | ||||||
|  |         MailUtils.sendHtml(email, String.format("【%s】邮箱验证码", properties.getName()), content); | ||||||
|  |  | ||||||
|  |         // 保存验证码 | ||||||
|  |         String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, email); | ||||||
|  |         RedisUtils.setCacheObject(captchaKey, captcha, Duration.ofMinutes(expirationInMinutes)); | ||||||
|  |         RedisUtils.setCacheObject(limitCaptchaKey, captcha, Duration.ofSeconds(captchaMail.getLimitInSeconds())); | ||||||
|  |         return R.ok(String.format("发送成功,验证码有效期 %s 分钟", expirationInMinutes)); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -34,15 +34,18 @@ import cn.hutool.core.util.ReUtil; | |||||||
| import cn.hutool.core.util.StrUtil; | import cn.hutool.core.util.StrUtil; | ||||||
|  |  | ||||||
| import top.charles7c.cnadmin.common.config.properties.LocalStorageProperties; | import top.charles7c.cnadmin.common.config.properties.LocalStorageProperties; | ||||||
|  | import top.charles7c.cnadmin.common.consts.CacheConstants; | ||||||
| import top.charles7c.cnadmin.common.consts.FileConstants; | import top.charles7c.cnadmin.common.consts.FileConstants; | ||||||
| import top.charles7c.cnadmin.common.consts.RegExpConstants; | import top.charles7c.cnadmin.common.consts.RegExpConstants; | ||||||
| import top.charles7c.cnadmin.common.model.vo.R; | import top.charles7c.cnadmin.common.model.vo.R; | ||||||
| import top.charles7c.cnadmin.common.util.ExceptionUtils; | import top.charles7c.cnadmin.common.util.ExceptionUtils; | ||||||
|  | import top.charles7c.cnadmin.common.util.RedisUtils; | ||||||
| import top.charles7c.cnadmin.common.util.SecureUtils; | import top.charles7c.cnadmin.common.util.SecureUtils; | ||||||
| import top.charles7c.cnadmin.common.util.helper.LoginHelper; | import top.charles7c.cnadmin.common.util.helper.LoginHelper; | ||||||
| import top.charles7c.cnadmin.common.util.validate.ValidationUtils; | import top.charles7c.cnadmin.common.util.validate.ValidationUtils; | ||||||
| import top.charles7c.cnadmin.system.model.entity.SysUser; | import top.charles7c.cnadmin.system.model.entity.SysUser; | ||||||
| import top.charles7c.cnadmin.system.model.request.UpdateBasicInfoRequest; | import top.charles7c.cnadmin.system.model.request.UpdateBasicInfoRequest; | ||||||
|  | import top.charles7c.cnadmin.system.model.request.UpdateEmailRequest; | ||||||
| import top.charles7c.cnadmin.system.model.request.UpdatePasswordRequest; | import top.charles7c.cnadmin.system.model.request.UpdatePasswordRequest; | ||||||
| import top.charles7c.cnadmin.system.model.vo.AvatarVO; | import top.charles7c.cnadmin.system.model.vo.AvatarVO; | ||||||
| import top.charles7c.cnadmin.system.service.UserService; | import top.charles7c.cnadmin.system.service.UserService; | ||||||
| @@ -111,4 +114,24 @@ public class UserCenterController { | |||||||
|         userService.updatePassword(rawOldPassword, rawNewPassword, LoginHelper.getUserId()); |         userService.updatePassword(rawOldPassword, rawNewPassword, LoginHelper.getUserId()); | ||||||
|         return R.ok("修改成功"); |         return R.ok("修改成功"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Operation(summary = "修改邮箱", description = "修改用户邮箱") | ||||||
|  |     @PatchMapping("/email") | ||||||
|  |     public R updateEmail(@Validated @RequestBody UpdateEmailRequest updateEmailRequest) { | ||||||
|  |         // 解密 | ||||||
|  |         String rawCurrentPassword = | ||||||
|  |             ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateEmailRequest.getCurrentPassword())); | ||||||
|  |         ValidationUtils.exIfBlank(rawCurrentPassword, "当前密码解密失败"); | ||||||
|  |  | ||||||
|  |         // 校验 | ||||||
|  |         String captchaKey = RedisUtils.formatKey(CacheConstants.CAPTCHA_CACHE_KEY, updateEmailRequest.getNewEmail()); | ||||||
|  |         String captcha = RedisUtils.getCacheObject(captchaKey); | ||||||
|  |         ValidationUtils.exIfBlank(captcha, "验证码已失效"); | ||||||
|  |         ValidationUtils.exIfNotEqualIgnoreCase(updateEmailRequest.getCaptcha(), captcha, "验证码错误"); | ||||||
|  |         RedisUtils.deleteCacheObject(captchaKey); | ||||||
|  |  | ||||||
|  |         // 修改邮箱 | ||||||
|  |         userService.updateEmail(updateEmailRequest.getNewEmail(), rawCurrentPassword, LoginHelper.getUserId()); | ||||||
|  |         return R.ok("修改成功"); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -69,6 +69,48 @@ spring: | |||||||
|     # 是否开启 SSL |     # 是否开启 SSL | ||||||
|     ssl: false |     ssl: false | ||||||
|  |  | ||||||
|  | --- ### 邮件配置 | ||||||
|  | spring: | ||||||
|  |   mail: | ||||||
|  |     # 根据需要更换 | ||||||
|  |     host: smtp.126.com | ||||||
|  |     port: 465 | ||||||
|  |     username: 你的邮箱 | ||||||
|  |     password: 你的邮箱授权码 | ||||||
|  |     default-encoding: utf-8 | ||||||
|  |     properties: | ||||||
|  |       mail: | ||||||
|  |         smtp: | ||||||
|  |           auth: true | ||||||
|  |           socketFactory: | ||||||
|  |             class: javax.net.ssl.SSLSocketFactory | ||||||
|  |             port: 465 | ||||||
|  |  | ||||||
|  | --- ### 验证码配置 | ||||||
|  | captcha: | ||||||
|  |   ## 图片验证码配置 | ||||||
|  |   image: | ||||||
|  |     # 类型 | ||||||
|  |     type: SPEC | ||||||
|  |     # 内容长度 | ||||||
|  |     length: 4 | ||||||
|  |     # 过期时间 | ||||||
|  |     expirationInMinutes: 2 | ||||||
|  |     # 宽度 | ||||||
|  |     width: 111 | ||||||
|  |     # 高度 | ||||||
|  |     height: 36 | ||||||
|  |   ## 邮箱验证码配置 | ||||||
|  |   mail: | ||||||
|  |     # 内容长度 | ||||||
|  |     length: 6 | ||||||
|  |     # 过期时间 | ||||||
|  |     expirationInMinutes: 5 | ||||||
|  |     # 限制时间 | ||||||
|  |     limitInSeconds: 60 | ||||||
|  |     # 模板路径 | ||||||
|  |     templatePath: mail/captcha.ftl | ||||||
|  |  | ||||||
| --- ### 安全配置 | --- ### 安全配置 | ||||||
| security: | security: | ||||||
|   # 排除路径配置 |   # 排除路径配置 | ||||||
| @@ -95,21 +137,6 @@ rsa: | |||||||
|   # 私钥 |   # 私钥 | ||||||
|   privateKey: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV |   privateKey: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV | ||||||
|  |  | ||||||
| --- ### 验证码配置 |  | ||||||
| captcha: |  | ||||||
|   # 类型 |  | ||||||
|   type: SPEC |  | ||||||
|   # 缓存键的前缀 |  | ||||||
|   keyPrefix: CAPTCHA |  | ||||||
|   # 过期时间 |  | ||||||
|   expirationInMinutes: 2 |  | ||||||
|   # 内容长度 |  | ||||||
|   length: 4 |  | ||||||
|   # 宽度 |  | ||||||
|   width: 111 |  | ||||||
|   # 高度 |  | ||||||
|   height: 36 |  | ||||||
|  |  | ||||||
| --- ### 接口文档配置 | --- ### 接口文档配置 | ||||||
| springdoc: | springdoc: | ||||||
|   swagger-ui: |   swagger-ui: | ||||||
|   | |||||||
| @@ -69,6 +69,48 @@ spring: | |||||||
|     # 是否开启 SSL |     # 是否开启 SSL | ||||||
|     ssl: false |     ssl: false | ||||||
|  |  | ||||||
|  | --- ### 邮件配置 | ||||||
|  | spring: | ||||||
|  |   mail: | ||||||
|  |     # 根据需要更换 | ||||||
|  |     host: smtp.126.com | ||||||
|  |     port: 465 | ||||||
|  |     username: 你的邮箱 | ||||||
|  |     password: 你的邮箱授权码 | ||||||
|  |     default-encoding: utf-8 | ||||||
|  |     properties: | ||||||
|  |       mail: | ||||||
|  |         smtp: | ||||||
|  |           auth: true | ||||||
|  |           socketFactory: | ||||||
|  |             class: javax.net.ssl.SSLSocketFactory | ||||||
|  |             port: 465 | ||||||
|  |  | ||||||
|  | --- ### 验证码配置 | ||||||
|  | captcha: | ||||||
|  |   ## 图片验证码配置 | ||||||
|  |   image: | ||||||
|  |     # 类型 | ||||||
|  |     type: SPEC | ||||||
|  |     # 内容长度 | ||||||
|  |     length: 4 | ||||||
|  |     # 过期时间 | ||||||
|  |     expirationInMinutes: 2 | ||||||
|  |     # 宽度 | ||||||
|  |     width: 111 | ||||||
|  |     # 高度 | ||||||
|  |     height: 36 | ||||||
|  |   ## 邮箱验证码配置 | ||||||
|  |   mail: | ||||||
|  |     # 内容长度 | ||||||
|  |     length: 6 | ||||||
|  |     # 过期时间 | ||||||
|  |     expirationInMinutes: 5 | ||||||
|  |     # 限制时间 | ||||||
|  |     limitInSeconds: 60 | ||||||
|  |     # 模板路径 | ||||||
|  |     templatePath: mail/captcha.ftl | ||||||
|  |  | ||||||
| --- ### 安全配置 | --- ### 安全配置 | ||||||
| security: | security: | ||||||
|   # 排除路径配置 |   # 排除路径配置 | ||||||
| @@ -88,21 +130,6 @@ rsa: | |||||||
|   # 私钥 |   # 私钥 | ||||||
|   privateKey: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV |   privateKey: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV | ||||||
|  |  | ||||||
| --- ### 验证码配置 |  | ||||||
| captcha: |  | ||||||
|   # 类型 |  | ||||||
|   type: SPEC |  | ||||||
|   # 缓存键的前缀 |  | ||||||
|   keyPrefix: CAPTCHA |  | ||||||
|   # 过期时间 |  | ||||||
|   expirationInMinutes: 2 |  | ||||||
|   # 内容长度 |  | ||||||
|   length: 4 |  | ||||||
|   # 宽度 |  | ||||||
|   width: 111 |  | ||||||
|   # 高度 |  | ||||||
|   height: 36 |  | ||||||
|  |  | ||||||
| --- ### 接口文档配置 | --- ### 接口文档配置 | ||||||
| springdoc: | springdoc: | ||||||
|   swagger-ui: |   swagger-ui: | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| --- ### 项目配置 | --- ### 项目配置 | ||||||
| continew-admin: | continew-admin: | ||||||
|   # 名称 |   # 名称 | ||||||
|   name: ContiNew-Admin |   name: ContiNew Admin | ||||||
|   # 应用名称 |   # 应用名称 | ||||||
|   appName: continew-admin |   appName: continew-admin | ||||||
|   # 版本 |   # 版本 | ||||||
|   version: 0.0.1-SNAPSHOT |   version: 0.0.1-SNAPSHOT | ||||||
|   # 描述 |   # 描述 | ||||||
|   description: ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。 |   description: ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。 | ||||||
|   # URL |   # URL | ||||||
|   url: https://github.com/Charles7c/continew-admin |   url: https://github.com/Charles7c/continew-admin | ||||||
|   ## 作者信息配置 |   ## 作者信息配置 | ||||||
| @@ -65,7 +65,7 @@ knife4j: | |||||||
|     # 是否自定义 footer(默认 false 非自定义) |     # 是否自定义 footer(默认 false 非自定义) | ||||||
|     enable-footer-custom: true |     enable-footer-custom: true | ||||||
|     # 自定义 footer 内容,支持 Markdown 语法 |     # 自定义 footer 内容,支持 Markdown 语法 | ||||||
|     footer-custom-content: '[Apache-2.0](https://github.com/Charles7c/continew-admin/blob/dev/LICENSE) | Copyright © 2022-present [ContiNew-Admin](https://github.com/Charles7c/continew-admin)' |     footer-custom-content: 'Copyright © 2022-present Charles7c ⋅ [ContiNew Admin](https://github.com/Charles7c/continew-admin)' | ||||||
|  |  | ||||||
| --- ### Sa-Token 配置 | --- ### Sa-Token 配置 | ||||||
| sa-token: | sa-token: | ||||||
|   | |||||||
| @@ -0,0 +1,47 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |     <meta charset="UTF-8" /> | ||||||
|  |     <meta name="description" content="邮箱验证码"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | ||||||
|  |     <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||||
|  |     <base target="_blank"> | ||||||
|  |     <style>::-webkit-scrollbar{ display: none; }</style> | ||||||
|  | </head> | ||||||
|  | <body tabindex="0"> | ||||||
|  | <div style="background-color: #ECECEC; padding: 25px;"> | ||||||
|  |     <div style="margin: 0 auto; text-align: left; position: relative; border-radius: 5px; border-collapse: collapse; box-shadow: rgb(153, 153, 153) 0px 0px 5px; background: #fff; font-family: 微软雅黑, 黑体, sans-serif; font-size: 14px; line-height: 1.5;"> | ||||||
|  |         <div style="height: 29px; line-height: 25px; padding: 15px 30px; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #307AF2; background: #00308f; border-radius: 5px 5px 0 0;"> | ||||||
|  |             <div style="font-size: 24px; font-weight: bolder; color: #fff; display: inline-flex; align-items: center;"> | ||||||
|  |                 <a href="https://cnadmin.charles7c.top/"> | ||||||
|  |                     <img src="https://cnadmin.charles7c.top/logo.svg" alt="ContiNew Admin" style="vertical-align: middle;"> | ||||||
|  |                 </a> | ||||||
|  |                 <a href="https://cnadmin.charles7c.top/" style="margin-left: 4px; text-decoration: none; color: #fff;">ContiNew Admin</a> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |         <div style="word-break: break-word;"> | ||||||
|  |             <div style="border-radius: 5px; padding: 25px 30px 11px; background-color: #fff; opacity: 0.8;"> | ||||||
|  |                 <h2 style="margin: 5px 0; font-size: 18px; line-height: 22px; color: #333;">亲爱的用户:</h2> | ||||||
|  |                 <p> | ||||||
|  |                     您好!感谢您使用 <a href="https://github.com/Charles7c/continew-admin" style="color: #333;">ContiNew Admin</a>,本次请求的验证码为:<span style="font-size: 16px; color: #ff8c00;">${captcha}</span>,请在 ${expiration} 分钟内使用此验证码完成验证。 | ||||||
|  |                 </p> | ||||||
|  |                 <br> | ||||||
|  |                 <h2 style="margin: 5px 0; font-size: 18px; line-height: 22px; color: #333;">Dear user:</h2> | ||||||
|  |                 <p> | ||||||
|  |                     Hello! Thanks for using ContiNew Admin, The verification code for this request is: <span style="font-size: 16px; color: #ff8c00;">${captcha}</span>, please use this verification code to complete the verification within ${expiration} minutes. | ||||||
|  |                 </p> | ||||||
|  |                 <div style="width: 100%; margin: 0 auto;"> | ||||||
|  |                     <div style="padding: 10px 10px 0; border-top: 1px solid #ccc; color: #747474; margin-bottom: 20px; line-height: 1.3em; font-size: 12px;"> | ||||||
|  |                         <p> | ||||||
|  |                             若非本人操作,请忽略此邮件。此邮件由系统自动发送,请勿直接回复该邮件。<br> | ||||||
|  |                             Please ignore this email if not by yourself. This email is sent automatically by the system, please do not reply to this email directly. | ||||||
|  |                         </p> | ||||||
|  |                         <p>Copyright © 2022-present Charles7c</p> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -25,7 +25,7 @@ limitations under the License. | |||||||
|     <packaging>pom</packaging> |     <packaging>pom</packaging> | ||||||
|  |  | ||||||
|     <name>${project.artifactId}</name> |     <name>${project.artifactId}</name> | ||||||
|     <description>ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。</description> |     <description>ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。</description> | ||||||
|     <url>https://github.com/Charles7c/continew-admin</url> |     <url>https://github.com/Charles7c/continew-admin</url> | ||||||
|  |  | ||||||
|     <modules> |     <modules> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user