新增:新增修改邮箱功能,并优化部分以往代码(引入 spring-boot-starter-mail 用于发送邮件验证码)
This commit is contained in:
parent
73fadb8315
commit
8b82557883
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.*;
|
||||
|
||||
@ -42,25 +42,35 @@ import cn.hutool.core.util.StrUtil;
|
||||
@ConfigurationProperties(prefix = "captcha")
|
||||
public class CaptchaProperties {
|
||||
|
||||
/**
|
||||
* 图片验证码配置
|
||||
*/
|
||||
private CaptchaImage image;
|
||||
|
||||
/**
|
||||
* 邮箱验证码配置
|
||||
*/
|
||||
private CaptchaMail mail;
|
||||
|
||||
/**
|
||||
* 图片验证码配置
|
||||
*/
|
||||
@Data
|
||||
public static class CaptchaImage {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private CaptchaTypeEnum type;
|
||||
|
||||
/**
|
||||
* 缓存键的前缀
|
||||
*/
|
||||
private String keyPrefix;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
private Long expirationInMinutes = 2L;
|
||||
private CaptchaImageTypeEnum type;
|
||||
|
||||
/**
|
||||
* 内容长度
|
||||
*/
|
||||
private int length = 4;
|
||||
private int length;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
private long expirationInMinutes;
|
||||
|
||||
/**
|
||||
* 宽度
|
||||
@ -83,7 +93,7 @@ public class CaptchaProperties {
|
||||
private int fontSize = 25;
|
||||
|
||||
/**
|
||||
* 获取验证码对象
|
||||
* 获取图片验证码对象
|
||||
*
|
||||
* @return 验证码对象
|
||||
*/
|
||||
@ -95,13 +105,40 @@ public class CaptchaProperties {
|
||||
}
|
||||
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();
|
||||
}
|
||||
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>
|
||||
|
Loading…
Reference in New Issue
Block a user