mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-30 02:57:09 +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: | | ||||
|             cd /docker | ||||
|             docker-compose up --force-recreate --build -d continew-admin-server | ||||
|             docker images  | grep none | awk '{print $3}' | xargs docker rmi | ||||
|  | ||||
|   # 部署前端 | ||||
|   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) | ||||
|  | ||||
| @@ -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)中打开本项目 | ||||
|  | ||||
| # 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)] | ||||
|  | ||||
| # 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 | ||||
|  | ||||
| # 5.部署 | ||||
| @@ -72,7 +72,7 @@ yarn dev | ||||
| | :----------------------------------------------------------- | :----------- | :----------------------------------------------------------- | | ||||
| | [Vue](https://cn.vuejs.org/)                                 | 3.2.45       | 渐进式 JavaScript 框架,易学易用,性能出色,适用场景丰富的 Web 前端框架。 | | ||||
| | [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 应用的初始搭建以及开发过程。                   | | ||||
| | [Undertow](https://undertow.io/)                             | 2.2.22.Final | 采用 Java 开发的灵活的高性能 Web 服务器,提供包括阻塞和基于 NIO 的非堵塞机制。 | | ||||
| | [Sa-Token + JWT](https://sa-token.dev33.cn/)                 | 1.33.0       | 轻量级 Java 权限认证框架,让鉴权变得简单、优雅。             | | ||||
| @@ -110,10 +110,14 @@ continew-admin  # 全局通用项目配置及依赖版本管理 | ||||
|   │      │        ├─ webapi | ||||
|   │      │        │  └─ controller   | ||||
|   │      │        │    ├─ auth    # 认证相关 API | ||||
|   │      │        │    ├─ common  # 公共相关 API(例如:验证码 API 等) | ||||
|   │      │        │    └─ system  # 系统管理相关 API | ||||
|   │      │        └─ ContinewAdminApplication.java  # 启动入口 | ||||
|   │      └─ resources   # 工程配置目录 | ||||
|   │        └─ db.changelog.v0.0.1    # 数据库脚本文件 | ||||
|   │        ├─ db.changelog   # 数据库脚本文件 | ||||
|   │        │  └─ v0.0.1        # v0.0.1 版本数据库脚本文件 | ||||
|   │        └─ templates      # 模板文件 | ||||
|   │          └─ mail           # 邮件模板 | ||||
|   ├─ continew-admin-monitor  # 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等) | ||||
|   │  └─ src | ||||
|   │    └─ main | ||||
| @@ -144,8 +148,7 @@ continew-admin  # 全局通用项目配置及依赖版本管理 | ||||
|   │      │      └─ cnadmin | ||||
|   │      │        ├─ auth     # 系统认证相关业务及配置 | ||||
|   │      │        │  ├─ config    # 系统认证相关配置 | ||||
|   │      │        │  │  ├─ satoken    # Sa-Token 配置 | ||||
|   │      │        │  │  └─ properties # 系统认证相关配置属性 | ||||
|   │      │        │  │  └─ satoken    # Sa-Token 配置 | ||||
|   │      │        │  ├─ model     # 系统认证相关模型 | ||||
|   │      │        │  │  ├─ request    # 系统认证相关请求对象 | ||||
|   │      │        │  │  └─ vo         # 系统认证相关 VO(View Object) | ||||
| @@ -197,6 +200,7 @@ continew-admin | ||||
|     ├─ src | ||||
|     │  ├─ api               # 请求接口 | ||||
|     │  │  ├─ auth             # 认证模块 | ||||
|     │  │  ├─ common           # 公共模块 | ||||
|     │  │  └─ system           # 系统管理模块 | ||||
|     │  ├─ assets            # 静态资源 | ||||
|     │  │  ├─ images           # 图片资源 | ||||
|   | ||||
| @@ -59,6 +59,17 @@ limitations under the License. | ||||
|             </exclusions> | ||||
|         </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 --> | ||||
|         <dependency> | ||||
|             <groupId>org.springframework.boot</groupId> | ||||
| @@ -126,5 +137,11 @@ limitations under the License. | ||||
|             <groupId>org.redisson</groupId> | ||||
|             <artifactId>redisson-spring-boot-starter</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- Easy Captcha(Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目) --> | ||||
|         <dependency> | ||||
|             <groupId>com.github.whvcse</groupId> | ||||
|             <artifactId>easy-captcha</artifactId> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
| </project> | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.charles7c.cnadmin.auth.config.properties; | ||||
| package top.charles7c.cnadmin.common.config.properties; | ||||
| 
 | ||||
| import java.awt.*; | ||||
| 
 | ||||
| @@ -43,65 +43,102 @@ import cn.hutool.core.util.StrUtil; | ||||
| public class CaptchaProperties { | ||||
| 
 | ||||
|     /** | ||||
|      * 类型 | ||||
|      * 图片验证码配置 | ||||
|      */ | ||||
|     private CaptchaTypeEnum type; | ||||
|     private CaptchaImage image; | ||||
| 
 | ||||
|     /** | ||||
|      * 缓存键的前缀 | ||||
|      * 邮箱验证码配置 | ||||
|      */ | ||||
|     private String keyPrefix; | ||||
|     private CaptchaMail mail; | ||||
| 
 | ||||
|     /** | ||||
|      * 过期时间 | ||||
|      * 图片验证码配置 | ||||
|      */ | ||||
|     private Long expirationInMinutes = 2L; | ||||
|     @Data | ||||
|     public static class CaptchaImage { | ||||
|         /** | ||||
|          * 类型 | ||||
|          */ | ||||
|         private CaptchaImageTypeEnum type; | ||||
| 
 | ||||
|     /** | ||||
|      * 内容长度 | ||||
|      */ | ||||
|     private int length = 4; | ||||
|         /** | ||||
|          * 内容长度 | ||||
|          */ | ||||
|         private int length; | ||||
| 
 | ||||
|     /** | ||||
|      * 宽度 | ||||
|      */ | ||||
|     private int width = 111; | ||||
|         /** | ||||
|          * 过期时间 | ||||
|          */ | ||||
|         private long expirationInMinutes; | ||||
| 
 | ||||
|     /** | ||||
|      * 高度 | ||||
|      */ | ||||
|     private int height = 36; | ||||
|         /** | ||||
|          * 宽度 | ||||
|          */ | ||||
|         private int width = 111; | ||||
| 
 | ||||
|     /** | ||||
|      * 字体 | ||||
|      */ | ||||
|     private String fontName; | ||||
|         /** | ||||
|          * 高度 | ||||
|          */ | ||||
|         private int height = 36; | ||||
| 
 | ||||
|     /** | ||||
|      * 字体大小 | ||||
|      */ | ||||
|     private int fontSize = 25; | ||||
|         /** | ||||
|          * 字体 | ||||
|          */ | ||||
|         private String fontName; | ||||
| 
 | ||||
|     /** | ||||
|      * 获取验证码对象 | ||||
|      * | ||||
|      * @return 验证码对象 | ||||
|      */ | ||||
|     public Captcha getCaptcha() { | ||||
|         Captcha captcha = ReflectUtil.newInstance(type.getClazz(), this.width, this.height); | ||||
|         captcha.setLen(length); | ||||
|         if (StrUtil.isNotBlank(this.fontName)) { | ||||
|             captcha.setFont(new Font(this.fontName, Font.PLAIN, this.fontSize)); | ||||
|         /** | ||||
|          * 字体大小 | ||||
|          */ | ||||
|         private int fontSize = 25; | ||||
| 
 | ||||
|         /** | ||||
|          * 获取图片验证码对象 | ||||
|          * | ||||
|          * @return 验证码对象 | ||||
|          */ | ||||
|         public Captcha getCaptcha() { | ||||
|             Captcha captcha = ReflectUtil.newInstance(type.getClazz(), this.width, this.height); | ||||
|             captcha.setLen(length); | ||||
|             if (StrUtil.isNotBlank(this.fontName)) { | ||||
|                 captcha.setFont(new Font(this.fontName, Font.PLAIN, this.fontSize)); | ||||
|             } | ||||
|             return captcha; | ||||
|         } | ||||
|         return captcha; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 验证码类型枚举 | ||||
|      * 邮箱验证码配置 | ||||
|      */ | ||||
|     @Data | ||||
|     public static class CaptchaMail { | ||||
|         /** | ||||
|          * 内容长度 | ||||
|          */ | ||||
|         private int length; | ||||
| 
 | ||||
|         /** | ||||
|          * 过期时间 | ||||
|          */ | ||||
|         private long expirationInMinutes; | ||||
| 
 | ||||
|         /** | ||||
|          * 限制时间 | ||||
|          */ | ||||
|         private long limitInSeconds; | ||||
| 
 | ||||
|         /** | ||||
|          * 模板路径 | ||||
|          */ | ||||
|         private String templatePath; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 图片验证码类型枚举 | ||||
|      */ | ||||
|     @Getter | ||||
|     @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 CAPTCHA_CACHE_KEY = "CAPTCHA"; | ||||
|  | ||||
|     /** | ||||
|      * 限流缓存键 | ||||
|      */ | ||||
|     public static final String LIMIT_CACHE_KEY = "LIMIT"; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -162,8 +162,8 @@ public class GlobalExceptionHandler { | ||||
|     @ResponseStatus(HttpStatus.UNAUTHORIZED) | ||||
|     @ExceptionHandler(NotLoginException.class) | ||||
|     public R handleNotLoginException(NotLoginException e, HttpServletRequest request) { | ||||
|         log.error("请求地址'{}',认证失败'{}',无法访问系统资源", request.getRequestURI(), e.getMessage()); | ||||
|         return R.fail(HttpStatus.UNAUTHORIZED.value(), "认证失败,无法访问系统资源"); | ||||
|         log.error("请求地址'{}',认证失败,无法访问系统资源", request.getRequestURI(), e); | ||||
|         return R.fail(HttpStatus.UNAUTHORIZED.value(), "登录状态已过期,请重新登录"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.charles7c.cnadmin.auth.model.vo; | ||||
| package top.charles7c.cnadmin.common.model.vo; | ||||
| 
 | ||||
| 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; | ||||
|  | ||||
|     /** | ||||
|      * 如果为空,抛出异常 | ||||
|      * | ||||
|      * @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); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 如果不为空,抛出异常 | ||||
|      * | ||||
|      * @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); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 如果相同,抛出异常(不区分大小写) | ||||
|      * | ||||
|      * @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; | ||||
|  | ||||
|     /** | ||||
|      * 如果为空,抛出异常 | ||||
|      * | ||||
|      * @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); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 如果不为空,抛出异常 | ||||
|      * | ||||
|      * @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); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 如果相同,抛出异常(不区分大小写) | ||||
|      * | ||||
|      * @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; | ||||
|  | ||||
| /** | ||||
|  * 校验器 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/1/2 22:12 | ||||
|  */ | ||||
| @@ -32,23 +34,6 @@ import cn.hutool.core.util.StrUtil; | ||||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||||
| 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 | ||||
|      *            异常类型 | ||||
|      */ | ||||
|     public static void exIfBlank(CharSequence str, String message, Class<? extends RuntimeException> exceptionType) { | ||||
|         if (StrUtil.isBlank(str)) { | ||||
|             log.error(message); | ||||
|             throw ReflectUtil.newInstance(exceptionType, message); | ||||
|         } | ||||
|     protected static void exIfBlank(CharSequence str, String message, Class<? extends RuntimeException> exceptionType) { | ||||
|         exIfCondition(() -> StrUtil.isBlank(str), message, exceptionType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 如果不为空,抛出异常 | ||||
|      * | ||||
|      * @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 | ||||
|      *            异常类型 | ||||
|      */ | ||||
|     public static void exIfEqual(Object obj1, Object obj2, String message, | ||||
|     protected static void exIfEqual(Object obj1, Object obj2, String message, | ||||
|         Class<? extends RuntimeException> exceptionType) { | ||||
|         if (ObjectUtil.equals(obj1, obj2)) { | ||||
|             log.error(message); | ||||
|             throw ReflectUtil.newInstance(exceptionType, message); | ||||
|         } | ||||
|         exIfCondition(() -> ObjectUtil.equal(obj1, obj2), message, exceptionType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -98,12 +92,71 @@ public class Validator { | ||||
|      * @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) { | ||||
|         if (ObjectUtil.notEqual(obj1, obj2)) { | ||||
|             log.error(message); | ||||
|             throw ReflectUtil.newInstance(exceptionType, message); | ||||
|         } | ||||
|         exIfCondition(() -> ObjectUtil.notEqual(obj1, obj2), message, exceptionType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 如果相同,抛出异常(不区分大小写) | ||||
|      * | ||||
|      * @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 | ||||
|      *            异常类型 | ||||
|      */ | ||||
|     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) { | ||||
|         if (conditionSupplier != null && conditionSupplier.getAsBoolean()) { | ||||
|             log.error(message); | ||||
|   | ||||
| @@ -28,6 +28,7 @@ import lombok.extern.slf4j.Slf4j; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
|  | ||||
| import org.springframework.core.annotation.AnnotationUtils; | ||||
| import org.springframework.lang.NonNull; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.web.method.HandlerMethod; | ||||
| import org.springframework.web.servlet.HandlerInterceptor; | ||||
| @@ -65,20 +66,21 @@ import top.charles7c.cnadmin.monitor.model.entity.SysLog; | ||||
| public class LogInterceptor implements HandlerInterceptor { | ||||
|  | ||||
|     private final LogProperties operationLogProperties; | ||||
|     private static final String ENCRYPT_SYMBOL = "****************"; | ||||
|  | ||||
|     @Override | ||||
|     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { | ||||
|         if (!checkIsNeedRecord(handler, request)) { | ||||
|             return true; | ||||
|     public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, | ||||
|         @NonNull Object handler) { | ||||
|         if (checkIsNeedRecord(handler, request)) { | ||||
|             // 记录操作时间 | ||||
|             this.logCreateTime(); | ||||
|         } | ||||
|  | ||||
|         // 记录操作时间 | ||||
|         this.logCreateTime(); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @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(); | ||||
|         if (sysLog == null) { | ||||
| @@ -203,6 +205,7 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|      *            待脱敏数据 | ||||
|      * @return 脱敏后的 JSON 字符串数据 | ||||
|      */ | ||||
|     @SuppressWarnings("unchecked") | ||||
|     private String desensitize(Map waitDesensitizeData) { | ||||
|         String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData); | ||||
|         try { | ||||
| @@ -211,9 +214,9 @@ public class LogInterceptor implements HandlerInterceptor { | ||||
|             } | ||||
|  | ||||
|             for (String desensitizeProperty : operationLogProperties.getDesensitize()) { | ||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> "****************"); | ||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> "****************"); | ||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> "****************"); | ||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> ENCRYPT_SYMBOL); | ||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> ENCRYPT_SYMBOL); | ||||
|                 waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> ENCRYPT_SYMBOL); | ||||
|             } | ||||
|             return JSONUtil.toJsonStr(waitDesensitizeData); | ||||
|         } catch (Exception ignored) { | ||||
|   | ||||
| @@ -32,12 +32,6 @@ limitations under the License. | ||||
|     <description>系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等)</description> | ||||
|  | ||||
|     <dependencies> | ||||
|         <!-- Easy Captcha(Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目) --> | ||||
|         <dependency> | ||||
|             <groupId>com.github.whvcse</groupId> | ||||
|             <artifactId>easy-captcha</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- 公共模块(存放公共工具类,公共配置等) --> | ||||
|         <dependency> | ||||
|             <groupId>top.charles7c</groupId> | ||||
|   | ||||
| @@ -52,7 +52,7 @@ public class LoginServiceImpl implements LoginService { | ||||
|         // 校验 | ||||
|         ValidationUtils.exIfNull(sysUser, "用户名或密码错误"); | ||||
|         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(), "此账号已被禁用,如有疑问,请联系管理员"); | ||||
|  | ||||
|   | ||||
| @@ -52,6 +52,6 @@ public class UpdateBasicInfoRequest implements Serializable { | ||||
|      * 性别(0未知 1男 2女) | ||||
|      */ | ||||
|     @Schema(description = "性别(0未知 1男 2女)", type = "Integer", allowableValues = {"0", "1", "2"}) | ||||
|     @NotNull(message = "非法性别") | ||||
|     @NotNull(message = "性别非法") | ||||
|     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 | ||||
|      */ | ||||
|     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) | ||||
|     public void updatePassword(String oldPassword, String newPassword, Long 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); | ||||
|     } | ||||
|  | ||||
|     @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 查询 | ||||
|      * | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "continew-admin-ui", | ||||
|   "description": "ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。", | ||||
|   "description": "ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。", | ||||
|   "version": "0.0.1-SNAPSHOT", | ||||
|   "private": true, | ||||
|   "author": "Charles7c", | ||||
|   | ||||
| @@ -2,14 +2,6 @@ import axios from 'axios'; | ||||
| import type { RouteRecordNormalized } from 'vue-router'; | ||||
| 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 { | ||||
|   username: 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) { | ||||
|   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"> | ||||
|     {{ `Copyright © 2022-${new Date().getFullYear()} Charles7c` }} | ||||
|     <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-layout-footer> | ||||
| </template> | ||||
|   | ||||
| @@ -190,7 +190,7 @@ | ||||
|   import useLocale from '@/hooks/locale'; | ||||
|   import useUser from '@/hooks/user'; | ||||
|   import Menu from '@/components/menu/index.vue'; | ||||
|   import getAvatar from "@/utils/avatar"; | ||||
|   import getAvatar from '@/utils/avatar'; | ||||
|   import MessageBox from '../message-box/index.vue'; | ||||
|  | ||||
|   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" { | ||||
|   interface AxiosResponse<T = any> { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { useRouter } from 'vue-router'; | ||||
| import { useI18n } from "vue-i18n"; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import { Message } from '@arco-design/web-vue'; | ||||
|  | ||||
| import { useLoginStore } from '@/store'; | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import { defineStore } from 'pinia'; | ||||
| import { | ||||
|   getImageCaptcha as getCaptcha, | ||||
|   login as userLogin, | ||||
|   logout as userLogout, | ||||
|   getUserInfo, | ||||
|   LoginReq, | ||||
| } from '@/api/auth/login'; | ||||
| import { getImageCaptcha as getCaptcha } from '@/api/common/captcha'; | ||||
| import { setToken, clearToken } from '@/utils/auth'; | ||||
| import { removeRouteListener } from '@/utils/route-listener'; | ||||
| import { UserState } from './types'; | ||||
|   | ||||
| @@ -29,7 +29,7 @@ | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|   import { useLoginStore } from '@/store'; | ||||
|   import getAvatar from "@/utils/avatar"; | ||||
|   import getAvatar from '@/utils/avatar'; | ||||
|  | ||||
|   const userInfo = useLoginStore(); | ||||
| </script> | ||||
|   | ||||
| @@ -37,7 +37,7 @@ | ||||
|           :placeholder="$t('login.form.placeholder.password')" | ||||
|           size="large" | ||||
|           allow-clear | ||||
|           max-length="50" | ||||
|           max-length="32" | ||||
|         > | ||||
|           <template #prefix> | ||||
|             <icon-lock /> | ||||
| @@ -82,13 +82,13 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|   import { ref, reactive, computed, onMounted } from "vue"; | ||||
|   import { ref, reactive, computed, onMounted } from 'vue'; | ||||
|   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 { useI18n } from 'vue-i18n'; | ||||
|   // import debug from '@/utils/env'; | ||||
|   import { encryptByRsa } from "@/utils/encrypt"; | ||||
|   import { encryptByRsa } from '@/utils/encrypt'; | ||||
|   import { useStorage } from '@vueuse/core'; | ||||
|   import { useLoginStore } from '@/store'; | ||||
|   import useLoading from '@/hooks/loading'; | ||||
|   | ||||
| @@ -53,14 +53,14 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|   import { ref, computed } from "vue"; | ||||
|   import { useI18n } from "vue-i18n"; | ||||
|   import { ref, computed } from 'vue'; | ||||
|   import { useI18n } from 'vue-i18n'; | ||||
|   import { useLoginStore } from '@/store'; | ||||
|   import { updateBasicInfo } from '@/api/system/user-center'; | ||||
|   import useLoading from '@/hooks/loading'; | ||||
|   import { FormInstance } from '@arco-design/web-vue/es/form'; | ||||
|   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 { loading, setLoading } = useLoading(); | ||||
| @@ -84,8 +84,8 @@ | ||||
|  | ||||
|   // 保存 | ||||
|   const save = async () => { | ||||
|     const errors = await formRef.value?.validate(); | ||||
|     if (loading.value) return; | ||||
|     const errors = await formRef.value?.validate(); | ||||
|     if (!errors) { | ||||
|       setLoading(true); | ||||
|       try { | ||||
|   | ||||
| @@ -15,18 +15,206 @@ | ||||
|         </a-typography-paragraph> | ||||
|       </div> | ||||
|       <div class="operation"> | ||||
|         <a-link> | ||||
|         <a-link @click="toUpdate"> | ||||
|           {{ $t('userCenter.securitySettings.button.update') }} | ||||
|         </a-link> | ||||
|       </div> | ||||
|     </template> | ||||
|   </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> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|   import { ref, reactive, computed } from 'vue'; | ||||
|   import { useI18n } from 'vue-i18n'; | ||||
|   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 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> | ||||
|  | ||||
| <style scoped lang="less"></style> | ||||
| <style scoped lang="less"> | ||||
|   .captcha-btn { | ||||
|     margin-left: 5px; | ||||
|   } | ||||
| </style> | ||||
|   | ||||
| @@ -22,12 +22,14 @@ | ||||
|     </template> | ||||
|   </a-list-item-meta> | ||||
|  | ||||
|   <a-modal v-model:visible="visible" :title="$t('userCenter.securitySettings.updatePwd.modal.title')" @cancel="handleCancel" @before-ok="handleUpdate"> | ||||
|     <a-form | ||||
|       ref="formRef" | ||||
|       :model="formData" | ||||
|       :rules="rules" | ||||
|     > | ||||
|   <a-modal | ||||
|     v-model:visible="visible" | ||||
|     :title="$t('userCenter.securitySettings.updatePwd.modal.title')" | ||||
|     :mask-closable="false" | ||||
|     @cancel="handleCancel" | ||||
|     @before-ok="handleUpdate" | ||||
|   > | ||||
|     <a-form ref="formRef" :model="formData" :rules="rules"> | ||||
|       <a-form-item | ||||
|         field="oldPassword" | ||||
|         :validate-trigger="['change', 'blur']" | ||||
| @@ -38,7 +40,7 @@ | ||||
|           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.oldPassword')" | ||||
|           size="large" | ||||
|           allow-clear | ||||
|           max-length="50" | ||||
|           max-length="32" | ||||
|         > | ||||
|         </a-input-password> | ||||
|       </a-form-item> | ||||
| @@ -52,7 +54,7 @@ | ||||
|           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.newPassword')" | ||||
|           size="large" | ||||
|           allow-clear | ||||
|           max-length="50" | ||||
|           max-length="32" | ||||
|         > | ||||
|         </a-input-password> | ||||
|       </a-form-item> | ||||
| @@ -66,7 +68,7 @@ | ||||
|           :placeholder="$t('userCenter.securitySettings.updatePwd.form.placeholder.rePassword')" | ||||
|           size="large" | ||||
|           allow-clear | ||||
|           max-length="50" | ||||
|           max-length="32" | ||||
|         > | ||||
|         </a-input-password> | ||||
|       </a-form-item> | ||||
| @@ -75,14 +77,14 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
|   import { ref, reactive, computed } from "vue"; | ||||
|   import { useI18n } from "vue-i18n"; | ||||
|   import { ref, reactive, computed } from 'vue'; | ||||
|   import { useI18n } from 'vue-i18n'; | ||||
|   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 { updatePassword } from "@/api/system/user-center"; | ||||
|   import { encryptByRsa } from "@/utils/encrypt"; | ||||
|   import { FormInstance } from '@arco-design/web-vue/es/form'; | ||||
|   import useLoading from '@/hooks/loading'; | ||||
|   import { FieldRule, Message } from '@arco-design/web-vue'; | ||||
|   import { updatePassword } from '@/api/system/user-center'; | ||||
|   import { encryptByRsa } from '@/utils/encrypt'; | ||||
|  | ||||
|   const { t } = useI18n(); | ||||
|   const { loading, setLoading } = useLoading(); | ||||
| @@ -129,8 +131,8 @@ | ||||
|  | ||||
|   // 确定修改 | ||||
|   const handleUpdate = async () => { | ||||
|     const errors = await formRef.value?.validate(); | ||||
|     if (loading.value) return false; | ||||
|     const errors = await formRef.value?.validate(); | ||||
|     if (errors) return false; | ||||
|     setLoading(true); | ||||
|     try { | ||||
|   | ||||
| @@ -61,8 +61,8 @@ | ||||
|   } from '@arco-design/web-vue/es/upload/interfaces'; | ||||
|   import { useLoginStore } from '@/store'; | ||||
|   import { uploadAvatar } from '@/api/system/user-center'; | ||||
|   import getAvatar from "@/utils/avatar"; | ||||
|   import { Message } from "@arco-design/web-vue"; | ||||
|   import getAvatar from '@/utils/avatar'; | ||||
|   import { Message } from '@arco-design/web-vue'; | ||||
|  | ||||
|   const loginStore = useLoginStore(); | ||||
|   const avatar = { | ||||
|   | ||||
| @@ -63,5 +63,23 @@ export default { | ||||
|   '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.', | ||||
|  | ||||
|   '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', | ||||
| }; | ||||
|   | ||||
| @@ -63,5 +63,23 @@ export default { | ||||
|   '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': '修改', | ||||
| }; | ||||
|   | ||||
| @@ -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.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.vo.LoginVO; | ||||
| import top.charles7c.cnadmin.auth.model.vo.UserInfoVO; | ||||
| 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.vo.R; | ||||
| import top.charles7c.cnadmin.common.util.ExceptionUtils; | ||||
| @@ -64,11 +65,11 @@ public class LoginController { | ||||
|     @PostMapping("/login") | ||||
|     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); | ||||
|         ValidationUtils.exIfBlank(captcha, "验证码已失效"); | ||||
|         RedisUtils.deleteCacheObject(captchaKey); | ||||
|         ValidationUtils.exIfCondition(() -> !captcha.equalsIgnoreCase(loginRequest.getCaptcha()), "验证码错误"); | ||||
|         ValidationUtils.exIfNotEqualIgnoreCase(loginRequest.getCaptcha(), captcha, "验证码错误"); | ||||
|  | ||||
|         // 用户登录 | ||||
|         String rawPassword = | ||||
| @@ -84,7 +85,6 @@ public class LoginController { | ||||
|         in = ParameterIn.HEADER) | ||||
|     @PostMapping("/logout") | ||||
|     public R logout() { | ||||
|         ValidationUtils.exIfCondition(() -> !StpUtil.isLogin(), "Token 无效"); | ||||
|         StpUtil.logout(); | ||||
|         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 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.RegExpConstants; | ||||
| import top.charles7c.cnadmin.common.model.vo.R; | ||||
| 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.helper.LoginHelper; | ||||
| import top.charles7c.cnadmin.common.util.validate.ValidationUtils; | ||||
| import top.charles7c.cnadmin.system.model.entity.SysUser; | ||||
| 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.vo.AvatarVO; | ||||
| import top.charles7c.cnadmin.system.service.UserService; | ||||
| @@ -111,4 +114,24 @@ public class UserCenterController { | ||||
|         userService.updatePassword(rawOldPassword, rawNewPassword, LoginHelper.getUserId()); | ||||
|         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: 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: | ||||
|   # 排除路径配置 | ||||
| @@ -95,21 +137,6 @@ rsa: | ||||
|   # 私钥 | ||||
|   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: | ||||
|   swagger-ui: | ||||
|   | ||||
| @@ -69,6 +69,48 @@ spring: | ||||
|     # 是否开启 SSL | ||||
|     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: | ||||
|   # 排除路径配置 | ||||
| @@ -88,21 +130,6 @@ rsa: | ||||
|   # 私钥 | ||||
|   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: | ||||
|   swagger-ui: | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| --- ### 项目配置 | ||||
| continew-admin: | ||||
|   # 名称 | ||||
|   name: ContiNew-Admin | ||||
|   name: ContiNew Admin | ||||
|   # 应用名称 | ||||
|   appName: continew-admin | ||||
|   # 版本 | ||||
|   version: 0.0.1-SNAPSHOT | ||||
|   # 描述 | ||||
|   description: ContiNew-Admin (incubating) 中后台管理框架,Continue New Admin,持续以最新流行技术栈构建。 | ||||
|   description: ContiNew Admin 中后台管理框架(孵化中),Continue New Admin,持续以最新流行技术栈构建。 | ||||
|   # URL | ||||
|   url: https://github.com/Charles7c/continew-admin | ||||
|   ## 作者信息配置 | ||||
| @@ -65,7 +65,7 @@ knife4j: | ||||
|     # 是否自定义 footer(默认 false 非自定义) | ||||
|     enable-footer-custom: true | ||||
|     # 自定义 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: | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|     <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> | ||||
|  | ||||
|     <modules> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user