新增润阳项目后端框架
This commit is contained in:
parent
244a14674b
commit
7f8040f33f
@ -4,7 +4,7 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cn.mesmile</groupId>
|
||||
<groupId>com.das</groupId>
|
||||
<artifactId>das</artifactId>
|
||||
<version>1.0.0-release</version>
|
||||
<name>admin</name>
|
||||
@ -32,7 +32,7 @@
|
||||
<jwt.version>0.9.1</jwt.version>
|
||||
<spring.boot.version>2.7.6</spring.boot.version>
|
||||
<mybatis.plus.spring.boot>3.5.2</mybatis.plus.spring.boot>
|
||||
<hutool.version>5.8.4</hutool.version>
|
||||
<hutool.version>5.8.18</hutool.version>
|
||||
<!--注意这里 lombok 1.18.10 和 mapstruct 1.3.0.Final 匹配-->
|
||||
<!--注意这里 lombok 1.18.20 和 mapstruct 1.5.2.Final 匹配-->
|
||||
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
|
||||
@ -83,6 +83,11 @@
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||
<!-- <artifactId>spring-boot-starter-security</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<!--spring 配置支持-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.das.common.config;
|
||||
|
||||
import com.das.common.interceptor.TokenInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
@ -8,12 +9,16 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public TokenInterceptor getTokenInterceptor() {
|
||||
return new TokenInterceptor();
|
||||
}
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 将Token拦截器添加到注册表中
|
||||
registry.addInterceptor(new TokenInterceptor())
|
||||
registry.addInterceptor(getTokenInterceptor())
|
||||
// 可以指定拦截哪些路径,例如"/api/**"表示所有以/api开头的路径
|
||||
// .addPathPatterns("/api/**")
|
||||
// .addPathPatterns("/api/**");
|
||||
// 排除不需要拦截的路径
|
||||
.excludePathPatterns("/api/**");
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
* @author chenhaojie
|
||||
* @Description
|
||||
*/
|
||||
@MapperScan(basePackages = {"cn.mesmile.**.mapper"})
|
||||
@MapperScan(basePackages = {"com.das.**.mapper"})
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
|
@ -9,7 +9,7 @@ public interface AdminConstant {
|
||||
/**
|
||||
* 基础包路径
|
||||
*/
|
||||
String BASE_PACKAGE = "cn.mesmile";
|
||||
String BASE_PACKAGE = "com.das";
|
||||
|
||||
/**
|
||||
* redis分布式所的默认前缀
|
||||
|
@ -12,6 +12,7 @@ import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
@ -23,34 +24,23 @@ public class BaseEntity implements Serializable {
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@ApiModelProperty("创建人")
|
||||
private Long createBy;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@ApiModelProperty("创建部门")
|
||||
private Long createDept;
|
||||
private Long createdBy;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@ApiModelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
private Date createdTime;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@ApiModelProperty("更新人")
|
||||
private Long updateBy;
|
||||
private Long updatedBy;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@ApiModelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updatedTime;
|
||||
|
||||
@ApiModelProperty("删除标志(0代表存在 1代表删除)")
|
||||
@TableLogic
|
||||
private String deleted;
|
||||
|
||||
@ApiModelProperty("业务状态(0正常 1停用)")
|
||||
private String status;
|
||||
}
|
||||
|
@ -1,26 +1,49 @@
|
||||
package com.das.common.interceptor;
|
||||
|
||||
import com.das.common.utils.AESUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
public class TokenInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Resource
|
||||
private AESUtil aesUtil;
|
||||
|
||||
private static String key;
|
||||
@Value("${aesKey}")
|
||||
public void setKey(String key){
|
||||
TokenInterceptor.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
// 从请求头中获取Token
|
||||
String token = request.getHeader("Authorization");
|
||||
|
||||
// 验证Token有效性,这里只是一个示例逻辑,实际应根据你的认证方式实现
|
||||
if (token != null && validateToken(token)) {
|
||||
// Token有效,继续处理请求
|
||||
return true;
|
||||
} else {
|
||||
// Token无效,可以设置响应状态并返回错误信息
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.getWriter().write("Unauthorized");
|
||||
return false; // 阻止请求继续
|
||||
}
|
||||
String param = request.getParameter("param");
|
||||
// 从请求头中获取Token
|
||||
// String token = request.getHeader("token");
|
||||
// 从请求头中获取向量IV
|
||||
String iv = request.getHeader("v");
|
||||
// 先解密token
|
||||
// AESUtil.decrypt(token, iv);
|
||||
// 解密参数
|
||||
aesUtil.decrypt(key, param, iv);
|
||||
System.out.println(aesUtil.decrypt(key ,param, iv));
|
||||
request.setAttribute("param", aesUtil.decrypt(key,param, iv));
|
||||
return true;
|
||||
// if (token != null && validateToken(token)) {
|
||||
// // Token有效,继续处理请求
|
||||
// return true;
|
||||
// } else {
|
||||
// // Token无效,可以设置响应状态并返回错误信息
|
||||
// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
// response.getWriter().write("Unauthorized");
|
||||
// return false; // 阻止请求继续
|
||||
// }
|
||||
}
|
||||
|
||||
// 示例Token验证方法,需要根据实际情况实现
|
||||
|
@ -89,6 +89,10 @@ public class R<T> implements Serializable {
|
||||
return new R(ResultCode.SUCCESS, msg);
|
||||
}
|
||||
|
||||
public static <T> R<T> success(T data) {
|
||||
return new R(ResultCode.SUCCESS, data,"操作成功");
|
||||
}
|
||||
|
||||
public static <T> R<T> success(IResultCode resultCode) {
|
||||
return new R(resultCode);
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
package com.das.common.support;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 返回视图层所需的字段
|
||||
* @author chenhaojie
|
||||
* @Description
|
||||
* @param <E>
|
||||
* @param <V>
|
||||
*/
|
||||
public abstract class BaseEntityWrapper<E, V> {
|
||||
|
||||
public BaseEntityWrapper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体类中部门字段转换为,视图层可看懂的字段,例如 性别 0 -> 女 , 1 -> 男
|
||||
* @param entity
|
||||
* @return
|
||||
*/
|
||||
public abstract V entityVO(E entity);
|
||||
|
||||
public List<V> listVO(List<E> list) {
|
||||
return (List)list.stream().map(this::entityVO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public IPage<V> pageVO(IPage<E> pages) {
|
||||
List<V> records = this.listVO(pages.getRecords());
|
||||
IPage<V> pageVo = new Page(pages.getCurrent(), pages.getSize(), pages.getTotal());
|
||||
pageVo.setRecords(records);
|
||||
return pageVo;
|
||||
}
|
||||
}
|
39
das/src/main/java/com/das/common/utils/AESUtil.java
Normal file
39
das/src/main/java/com/das/common/utils/AESUtil.java
Normal file
@ -0,0 +1,39 @@
|
||||
package com.das.common.utils;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.crypto.Mode;
|
||||
import cn.hutool.crypto.Padding;
|
||||
import cn.hutool.crypto.symmetric.AES;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* @author xxx
|
||||
* @date 2020-09-16 11:17
|
||||
**/
|
||||
@Component
|
||||
public class AESUtil {
|
||||
|
||||
/**
|
||||
* AES加密
|
||||
* @param content 待加密的内容
|
||||
* @return 加密后的Base64字符串
|
||||
*/
|
||||
// public String encrypt(String content, String iv) {
|
||||
// AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, key.getBytes(), iv.getBytes());
|
||||
// return Base64.encode(aes.encrypt(content, Charset.forName("UTF-8")));
|
||||
// }
|
||||
|
||||
/**
|
||||
* AES解密
|
||||
* @param encryptStr 已加密的Base64字符串
|
||||
* @return 解密后的内容
|
||||
*/
|
||||
public static String decrypt(String key, String encryptStr, String iv) {
|
||||
AES aes = new AES(Mode.CBC, Padding.ZeroPadding, key.getBytes(), iv.getBytes());
|
||||
return aes.decryptStr(Base64.decode(encryptStr), Charset.forName("UTF-8"));
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package com.das.common.utils;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
* @Description 自定义工具类
|
||||
*/
|
||||
@Slf4j
|
||||
public class FunctionUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 将英文逗号分割的字符串,转换成 List
|
||||
* @param string 字符串
|
||||
* @return 结果
|
||||
*/
|
||||
public static List<Long> strToLongList(String string) {
|
||||
return strToList(string, StringPool.COMMA);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将某个字符串分割的字符串转换成,List
|
||||
* @param string 字符串
|
||||
* @param split 分隔符
|
||||
* @return 结果
|
||||
*/
|
||||
public static List<Long> strToList(String string,String split){
|
||||
if (string == null || split == null){
|
||||
return new ArrayList<>();
|
||||
}
|
||||
String[] resultArray = string.split(split);
|
||||
List<Long> result = new ArrayList<>(resultArray.length);
|
||||
for (String str : resultArray) {
|
||||
if (NumberUtil.isNumber(str)){
|
||||
try {
|
||||
long number = Long.parseLong(str);
|
||||
result.add(number);
|
||||
}catch (Exception e){
|
||||
log.error("解析数据异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
package com.das.common.utils;
|
||||
|
||||
import cn.hutool.crypto.digest.MD5;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
* @Description
|
||||
*/
|
||||
public class IpUtil {
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return IP地址
|
||||
*/
|
||||
public static String getIpAddr(HttpServletRequest request) {
|
||||
String s = new MD5().digestHex("");
|
||||
|
||||
|
||||
if (request == null) {
|
||||
return "unknown";
|
||||
}
|
||||
String ip = request.getHeader("x-forwarded-for");
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Forwarded-For");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为内部IP地址
|
||||
*
|
||||
* @param ip IP地址
|
||||
* @return 结果
|
||||
*/
|
||||
public static boolean internalIp(String ip) {
|
||||
byte[] addr = textToNumericFormatV4(ip);
|
||||
return internalIp(addr) || "127.0.0.1".equals(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为内部IP地址
|
||||
*
|
||||
* @param addr byte地址
|
||||
* @return 结果
|
||||
*/
|
||||
private static boolean internalIp(byte[] addr) {
|
||||
if (addr == null || addr.length < 2) {
|
||||
return true;
|
||||
}
|
||||
final byte b0 = addr[0];
|
||||
final byte b1 = addr[1];
|
||||
// 10.x.x.x/8
|
||||
final byte SECTION_1 = 0x0A;
|
||||
// 172.16.x.x/12
|
||||
final byte SECTION_2 = (byte) 0xAC;
|
||||
final byte SECTION_3 = (byte) 0x10;
|
||||
final byte SECTION_4 = (byte) 0x1F;
|
||||
// 192.168.x.x/16
|
||||
final byte SECTION_5 = (byte) 0xC0;
|
||||
final byte SECTION_6 = (byte) 0xA8;
|
||||
switch (b0) {
|
||||
case SECTION_1:
|
||||
return true;
|
||||
case SECTION_2:
|
||||
if (b1 >= SECTION_3 && b1 <= SECTION_4) {
|
||||
return true;
|
||||
}
|
||||
case SECTION_5:
|
||||
switch (b1) {
|
||||
case SECTION_6:
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将IPv4地址转换成字节
|
||||
*
|
||||
* @param text IPv4地址
|
||||
* @return byte 字节
|
||||
*/
|
||||
public static byte[] textToNumericFormatV4(String text) {
|
||||
if (text.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[4];
|
||||
String[] elements = text.split("\\.", -1);
|
||||
try {
|
||||
long l;
|
||||
int i;
|
||||
switch (elements.length) {
|
||||
case 1:
|
||||
l = Long.parseLong(elements[0]);
|
||||
if ((l < 0L) || (l > 4294967295L)) {
|
||||
return null;
|
||||
}
|
||||
bytes[0] = (byte) (int) (l >> 24 & 0xFF);
|
||||
bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
|
||||
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
|
||||
bytes[3] = (byte) (int) (l & 0xFF);
|
||||
break;
|
||||
case 2:
|
||||
l = Integer.parseInt(elements[0]);
|
||||
if ((l < 0L) || (l > 255L)) {
|
||||
return null;
|
||||
}
|
||||
bytes[0] = (byte) (int) (l & 0xFF);
|
||||
l = Integer.parseInt(elements[1]);
|
||||
if ((l < 0L) || (l > 16777215L)) {
|
||||
return null;
|
||||
}
|
||||
bytes[1] = (byte) (int) (l >> 16 & 0xFF);
|
||||
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
|
||||
bytes[3] = (byte) (int) (l & 0xFF);
|
||||
break;
|
||||
case 3:
|
||||
for (i = 0; i < 2; ++i) {
|
||||
l = Integer.parseInt(elements[i]);
|
||||
if ((l < 0L) || (l > 255L)) {
|
||||
return null;
|
||||
}
|
||||
bytes[i] = (byte) (int) (l & 0xFF);
|
||||
}
|
||||
l = Integer.parseInt(elements[2]);
|
||||
if ((l < 0L) || (l > 65535L)) {
|
||||
return null;
|
||||
}
|
||||
bytes[2] = (byte) (int) (l >> 8 & 0xFF);
|
||||
bytes[3] = (byte) (int) (l & 0xFF);
|
||||
break;
|
||||
case 4:
|
||||
for (i = 0; i < 4; ++i) {
|
||||
l = Integer.parseInt(elements[i]);
|
||||
if ((l < 0L) || (l > 255L)) {
|
||||
return null;
|
||||
}
|
||||
bytes[i] = (byte) (int) (l & 0xFF);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP地址
|
||||
*
|
||||
* @return 本地IP地址
|
||||
*/
|
||||
public static String getHostIp() {
|
||||
try {
|
||||
return InetAddress.getLocalHost().getHostAddress();
|
||||
} catch (UnknownHostException e) {
|
||||
}
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主机名
|
||||
*
|
||||
* @return 本地主机名
|
||||
*/
|
||||
public static String getHostName() {
|
||||
try {
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
} catch (UnknownHostException e) {
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多级反向代理中获得第一个非unknown IP地址
|
||||
*
|
||||
* @param ip 获得的IP地址
|
||||
* @return 第一个非unknown IP地址
|
||||
*/
|
||||
public static String getMultistageReverseProxyIp(String ip) {
|
||||
// 多级反向代理检测
|
||||
if (ip != null && ip.indexOf(",") > 0) {
|
||||
final String[] ips = ip.trim().split(",");
|
||||
for (String subIp : ips) {
|
||||
if (false == isUnknown(subIp)) {
|
||||
ip = subIp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测给定字符串是否为未知,多用于检测HTTP请求相关
|
||||
*
|
||||
* @param checkString 被检测的字符串
|
||||
* @return 是否未知
|
||||
*/
|
||||
public static boolean isUnknown(String checkString) {
|
||||
return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
|
||||
}
|
||||
|
||||
}
|
109
das/src/main/java/com/das/common/utils/JsonUtils.java
Normal file
109
das/src/main/java/com/das/common/utils/JsonUtils.java
Normal file
@ -0,0 +1,109 @@
|
||||
package com.das.common.utils;
|
||||
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JSON 工具类
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class JsonUtils {
|
||||
|
||||
private static ObjectMapper OBJECT_MAPPER = SpringUtils.getBean(ObjectMapper.class);
|
||||
|
||||
public static ObjectMapper getObjectMapper() {
|
||||
return OBJECT_MAPPER;
|
||||
}
|
||||
|
||||
public static String toJsonString(Object object) {
|
||||
if (ObjectUtil.isNull(object)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(object);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T parseObject(String text, Class<T> clazz) {
|
||||
if (StringUtils.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(text, clazz);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
|
||||
if (ArrayUtil.isEmpty(bytes)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(bytes, clazz);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(text, typeReference);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Dict parseMap(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructType(Dict.class));
|
||||
} catch (MismatchedInputException e) {
|
||||
// 类型不匹配说明不是json
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Dict> parseArrayMap(String text) {
|
||||
if (StringUtils.isBlank(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, Dict.class));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> List<T> parseArray(String text, Class<T> clazz) {
|
||||
if (StringUtils.isEmpty(text)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package com.das.common.utils;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
* @Description
|
||||
*/
|
||||
public class ResourceI18nUtil {
|
||||
|
||||
public static MessageSource getMessageSource() {
|
||||
return SpringUtil.getBean(MessageSource.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过定义key 获取value
|
||||
* @param key key
|
||||
* @return value
|
||||
*/
|
||||
public static String getValueByKey(String key){
|
||||
return getMessageSource().getMessage(key, null, LocaleContextHolder.getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过定义key 获取value
|
||||
* @param key key
|
||||
* @param args 填补参数
|
||||
* @return value
|
||||
*/
|
||||
public static String getValueByKey(String key, Object... args){
|
||||
return getMessageSource().getMessage(key, args, LocaleContextHolder.getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过定义key 获取value
|
||||
* @param key key
|
||||
* @param defaultMessage 默认消息
|
||||
* @param args 填补参数
|
||||
* @return value
|
||||
*/
|
||||
public static String getValueDefaultByKey(String key,String defaultMessage,Object... args){
|
||||
return getMessageSource().getMessage(key, args, defaultMessage, LocaleContextHolder.getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过定义key 获取value
|
||||
* @param key key
|
||||
* @param defaultMessage 默认消息
|
||||
* @return value
|
||||
*/
|
||||
public static String getValueDefaultByKey(String key,String defaultMessage){
|
||||
return getMessageSource().getMessage(key, null, defaultMessage,LocaleContextHolder.getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过定义key 获取value
|
||||
* @param resolvable resolvable
|
||||
* @return value
|
||||
*/
|
||||
public static String getValueByResolvable(MessageSourceResolvable resolvable){
|
||||
return getMessageSource().getMessage(resolvable, LocaleContextHolder.getLocale());
|
||||
}
|
||||
|
||||
}
|
69
das/src/main/java/com/das/common/utils/SpringUtils.java
Normal file
69
das/src/main/java/com/das/common/utils/SpringUtils.java
Normal file
@ -0,0 +1,69 @@
|
||||
package com.das.common.utils;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import org.springframework.aop.framework.AopContext;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
/**
|
||||
* spring工具类
|
||||
*/
|
||||
public final class SpringUtils extends SpringUtil {
|
||||
|
||||
/**
|
||||
* 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
|
||||
*
|
||||
* @param name
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean containsBean(String name) {
|
||||
return getBeanFactory().containsBean(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
|
||||
* 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
|
||||
*
|
||||
* @param name
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
|
||||
return getBeanFactory().isSingleton(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name
|
||||
* @return Class 注册对象的类型
|
||||
*/
|
||||
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
|
||||
return getBeanFactory().getType(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果给定的bean名字在bean定义中有别名,则返回这些别名
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
|
||||
return getBeanFactory().getAliases(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取aop代理对象
|
||||
*
|
||||
* @param invoker
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getAopProxy(T invoker) {
|
||||
return (T) AopContext.currentProxy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取spring上下文
|
||||
*/
|
||||
public static ApplicationContext context() {
|
||||
return getApplicationContext();
|
||||
}
|
||||
|
||||
}
|
346
das/src/main/java/com/das/common/utils/StringUtils.java
Normal file
346
das/src/main/java/com/das/common/utils/StringUtils.java
Normal file
@ -0,0 +1,346 @@
|
||||
package com.das.common.utils;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.Validator;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 字符串工具类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class StringUtils extends org.apache.commons.lang3.StringUtils {
|
||||
|
||||
public static final String SEPARATOR = ",";
|
||||
|
||||
/**
|
||||
* 获取参数不为空值
|
||||
*
|
||||
* @param str defaultValue 要判断的value
|
||||
* @return value 返回值
|
||||
*/
|
||||
public static String blankToDefault(String str, String defaultValue) {
|
||||
return StrUtil.blankToDefault(str, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* * 判断一个字符串是否为空串
|
||||
*
|
||||
* @param str String
|
||||
* @return true:为空 false:非空
|
||||
*/
|
||||
public static boolean isEmpty(String str) {
|
||||
return StrUtil.isEmpty(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* * 判断一个字符串是否为非空串
|
||||
*
|
||||
* @param str String
|
||||
* @return true:非空串 false:空串
|
||||
*/
|
||||
public static boolean isNotEmpty(String str) {
|
||||
return !isEmpty(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 去空格
|
||||
*/
|
||||
public static String trim(String str) {
|
||||
return StrUtil.trim(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取字符串
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param start 开始
|
||||
* @return 结果
|
||||
*/
|
||||
public static String substring(final String str, int start) {
|
||||
return substring(str, start, str.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取字符串
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param start 开始
|
||||
* @param end 结束
|
||||
* @return 结果
|
||||
*/
|
||||
public static String substring(final String str, int start, int end) {
|
||||
return StrUtil.sub(str, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文本, {} 表示占位符<br>
|
||||
* 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
|
||||
* 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
|
||||
* 例:<br>
|
||||
* 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
|
||||
* 转义{}: format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
|
||||
* 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
|
||||
*
|
||||
* @param template 文本模板,被替换的部分用 {} 表示
|
||||
* @param params 参数值
|
||||
* @return 格式化后的文本
|
||||
*/
|
||||
public static String format(String template, Object... params) {
|
||||
return StrUtil.format(template, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为http(s)://开头
|
||||
*
|
||||
* @param link 链接
|
||||
* @return 结果
|
||||
*/
|
||||
public static boolean ishttp(String link) {
|
||||
return Validator.isUrl(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串转set
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param sep 分隔符
|
||||
* @return set集合
|
||||
*/
|
||||
public static Set<String> str2Set(String str, String sep) {
|
||||
return new HashSet<>(str2List(str, sep, true, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串转list
|
||||
*
|
||||
* @param str 字符串
|
||||
* @param sep 分隔符
|
||||
* @param filterBlank 过滤纯空白
|
||||
* @param trim 去掉首尾空白
|
||||
* @return list集合
|
||||
*/
|
||||
public static List<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
|
||||
List<String> list = new ArrayList<>();
|
||||
if (isEmpty(str)) {
|
||||
return list;
|
||||
}
|
||||
|
||||
// 过滤空白字符串
|
||||
if (filterBlank && isBlank(str)) {
|
||||
return list;
|
||||
}
|
||||
String[] split = str.split(sep);
|
||||
for (String string : split) {
|
||||
if (filterBlank && isBlank(string)) {
|
||||
continue;
|
||||
}
|
||||
if (trim) {
|
||||
string = trim(string);
|
||||
}
|
||||
list.add(string);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
|
||||
*
|
||||
* @param cs 指定字符串
|
||||
* @param searchCharSequences 需要检查的字符串数组
|
||||
* @return 是否包含任意一个字符串
|
||||
*/
|
||||
public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
|
||||
return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转下划线命名
|
||||
*/
|
||||
public static String toUnderScoreCase(String str) {
|
||||
return StrUtil.toUnderlineCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否包含字符串
|
||||
*
|
||||
* @param str 验证字符串
|
||||
* @param strs 字符串组
|
||||
* @return 包含返回true
|
||||
*/
|
||||
public static boolean inStringIgnoreCase(String str, String... strs) {
|
||||
return StrUtil.equalsAnyIgnoreCase(str, strs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
|
||||
*
|
||||
* @param name 转换前的下划线大写方式命名的字符串
|
||||
* @return 转换后的驼峰式命名的字符串
|
||||
*/
|
||||
public static String convertToCamelCase(String name) {
|
||||
return StrUtil.upperFirst(StrUtil.toCamelCase(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰式命名法 例如:user_name->userName
|
||||
*/
|
||||
public static String toCamelCase(String s) {
|
||||
return StrUtil.toCamelCase(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
|
||||
*
|
||||
* @param str 指定字符串
|
||||
* @param strs 需要检查的字符串数组
|
||||
* @return 是否匹配
|
||||
*/
|
||||
public static boolean matches(String str, List<String> strs) {
|
||||
if (isEmpty(str) || CollUtil.isEmpty(strs)) {
|
||||
return false;
|
||||
}
|
||||
for (String pattern : strs) {
|
||||
if (isMatch(pattern, str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断url是否与规则配置:
|
||||
* ? 表示单个字符;
|
||||
* * 表示一层路径内的任意字符串,不可跨层级;
|
||||
* ** 表示任意层路径;
|
||||
*
|
||||
* @param pattern 匹配规则
|
||||
* @param url 需要匹配的url
|
||||
*/
|
||||
public static boolean isMatch(String pattern, String url) {
|
||||
AntPathMatcher matcher = new AntPathMatcher();
|
||||
return matcher.match(pattern, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
|
||||
*
|
||||
* @param num 数字对象
|
||||
* @param size 字符串指定长度
|
||||
* @return 返回数字的字符串格式,该字符串为指定长度。
|
||||
*/
|
||||
public static String padl(final Number num, final int size) {
|
||||
return padl(num.toString(), size, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
|
||||
*
|
||||
* @param s 原始字符串
|
||||
* @param size 字符串指定长度
|
||||
* @param c 用于补齐的字符
|
||||
* @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
|
||||
*/
|
||||
public static String padl(final String s, final int size, final char c) {
|
||||
final StringBuilder sb = new StringBuilder(size);
|
||||
if (s != null) {
|
||||
final int len = s.length();
|
||||
if (s.length() <= size) {
|
||||
for (int i = size - len; i > 0; i--) {
|
||||
sb.append(c);
|
||||
}
|
||||
sb.append(s);
|
||||
} else {
|
||||
return s.substring(len - size, len);
|
||||
}
|
||||
} else {
|
||||
for (int i = size; i > 0; i--) {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切分字符串(分隔符默认逗号)
|
||||
*
|
||||
* @param str 被切分的字符串
|
||||
* @return 分割后的数据列表
|
||||
*/
|
||||
public static List<String> splitList(String str) {
|
||||
return splitTo(str, Convert::toStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切分字符串
|
||||
*
|
||||
* @param str 被切分的字符串
|
||||
* @param separator 分隔符
|
||||
* @return 分割后的数据列表
|
||||
*/
|
||||
public static List<String> splitList(String str, String separator) {
|
||||
return splitTo(str, separator, Convert::toStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切分字符串自定义转换(分隔符默认逗号)
|
||||
*
|
||||
* @param str 被切分的字符串
|
||||
* @param mapper 自定义转换
|
||||
* @return 分割后的数据列表
|
||||
*/
|
||||
public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
|
||||
return splitTo(str, SEPARATOR, mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切分字符串自定义转换
|
||||
*
|
||||
* @param str 被切分的字符串
|
||||
* @param separator 分隔符
|
||||
* @param mapper 自定义转换
|
||||
* @return 分割后的数据列表
|
||||
*/
|
||||
public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) {
|
||||
if (isBlank(str)) {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
return StrUtil.split(str, separator)
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(mapper)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/*
|
||||
* 首字母变大写
|
||||
*
|
||||
* */
|
||||
public static String capitalizeFirstLetter(String str) {
|
||||
char[] chars = str.toCharArray();
|
||||
chars[0] = toUpperCase(chars[0]);
|
||||
return String.valueOf(chars);
|
||||
}
|
||||
|
||||
/*
|
||||
* 字母变大写
|
||||
*
|
||||
* */
|
||||
public static char toUpperCase(char c) {
|
||||
if (97 <= c && c <= 122) {
|
||||
c ^= 32;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.das.modules.auth.controller;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.das.common.captcha.CaptchaProperties;
|
||||
import com.das.common.captcha.CaptchaUtil;
|
||||
import com.das.common.captcha.CaptchaVO;
|
||||
import com.das.common.result.R;
|
||||
import com.das.common.utils.AdminRedisTemplate;
|
||||
import com.das.modules.auth.domain.LoginUserDetails;
|
||||
import com.das.modules.auth.domain.vo.LoginVO;
|
||||
import com.das.modules.auth.service.ILoginService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.code.kaptcha.Producer;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
* @Description 获取验证码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequestMapping("/api")
|
||||
@RestController
|
||||
public class CaptchaImageController {
|
||||
@Resource
|
||||
private Producer producer;
|
||||
@Resource
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
@Value("${aesKey}")
|
||||
String key;
|
||||
@Resource
|
||||
private AdminRedisTemplate adminRedisTemplate;
|
||||
|
||||
/**
|
||||
* 限制频率在 5 秒钟 3次
|
||||
*/
|
||||
@ApiOperation("获取验证码")
|
||||
@PostMapping("/captchaImage")
|
||||
public R<CaptchaVO> getCaptcha() {
|
||||
String uuid = IdUtil.fastSimpleUUID();
|
||||
String imageBase64Str = CaptchaUtil.getImageBase64Str(producer, adminRedisTemplate, captchaProperties, uuid);
|
||||
CaptchaVO captchaVO = new CaptchaVO(uuid, imageBase64Str);
|
||||
return R.success(captchaVO);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,56 +1,69 @@
|
||||
package com.das.modules.auth.controller;
|
||||
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.das.common.captcha.CaptchaProperties;
|
||||
import com.das.common.captcha.CaptchaUtil;
|
||||
import com.das.common.captcha.CaptchaVO;
|
||||
import com.das.common.result.R;
|
||||
import com.das.common.utils.AESUtil;
|
||||
import com.das.common.utils.AdminRedisTemplate;
|
||||
import com.das.modules.auth.domain.request.LoginRequest;
|
||||
import com.das.modules.auth.domain.LoginUserDetails;
|
||||
import com.das.modules.auth.domain.vo.LoginVO;
|
||||
import com.das.modules.auth.domain.vo.TokenVo;
|
||||
import com.das.modules.auth.service.ILoginService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.code.kaptcha.Producer;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
* @Description
|
||||
* @Description 登录控制层
|
||||
*/
|
||||
@Slf4j
|
||||
@RequestMapping("/api")
|
||||
@RequestMapping("/api/auth")
|
||||
@RestController
|
||||
public class LoginController {
|
||||
@Resource
|
||||
private Producer producer;
|
||||
@Resource
|
||||
private CaptchaProperties captchaProperties;
|
||||
@Resource
|
||||
private ILoginService loginService;
|
||||
@Value("${aesKey}")
|
||||
String key;
|
||||
|
||||
@Resource
|
||||
private AdminRedisTemplate adminRedisTemplate;
|
||||
|
||||
/**
|
||||
* 限制频率在 5 秒钟 3次
|
||||
*/
|
||||
@ApiOperation("获取验证码")
|
||||
@PostMapping("/captchaImage")
|
||||
public R<CaptchaVO> getCaptcha() {
|
||||
String uuid = IdUtil.fastSimpleUUID();
|
||||
String imageBase64Str = CaptchaUtil.getImageBase64Str(producer, adminRedisTemplate, captchaProperties, uuid);
|
||||
CaptchaVO captchaVO = new CaptchaVO(uuid, imageBase64Str);
|
||||
return R.data(captchaVO);
|
||||
@PostMapping("/login")
|
||||
@ApiOperation("登录接口")
|
||||
public R<LoginUserDetails> login(@RequestParam String param, HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException {
|
||||
LoginUserDetails loginInfo = loginService.login(param,request, response);
|
||||
return R.success(loginInfo);
|
||||
}
|
||||
|
||||
@PostMapping("/auth/login")
|
||||
@ApiOperation("登录接口")
|
||||
public R<?> login(@RequestBody @Validated LoginRequest loginRequest) {
|
||||
String a = loginService.login(loginRequest);
|
||||
return R.data(a);
|
||||
/**
|
||||
* 退出登录接口
|
||||
* @return 退出结果提示信息
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public R<?> logout(HttpServletRequest request) {
|
||||
if (loginService.logout(request)) {
|
||||
return R.success("退出成功");
|
||||
} else {
|
||||
return R.fail("退出失败");
|
||||
}
|
||||
|
||||
}
|
||||
@PostMapping("/refreshToken")
|
||||
@ApiOperation("刷新token接口")
|
||||
public R<LoginVO> refreshToken(@NotEmpty(message = "刷新token不允许为空") @RequestParam("refreshToken")String refreshToken){
|
||||
LoginVO loginVO = loginService.refreshToken(refreshToken);
|
||||
return R.data(loginVO);
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
package com.das.modules.auth.domain;
|
||||
|
||||
import com.das.modules.auth.domain.vo.SysUserVo;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author zb
|
||||
* @Description
|
||||
*/
|
||||
@Data
|
||||
public class LoginUserDetails implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private SysUserVo sysUser;
|
||||
|
||||
|
||||
private String token;
|
||||
|
||||
|
||||
public LoginUserDetails() {
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package com.das.modules.auth.domain.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
* @Description
|
||||
*/
|
||||
@Data
|
||||
public class SysUserDTO {
|
||||
|
||||
@ApiModelProperty("用户账号")
|
||||
private String username;
|
||||
|
||||
@ApiModelProperty("用户昵称")
|
||||
private String nickName;
|
||||
|
||||
@ApiModelProperty("真实姓名")
|
||||
private String realName;
|
||||
|
||||
@ApiModelProperty("用户类型(00系统用户)")
|
||||
private String userType;
|
||||
|
||||
@ApiModelProperty("用户邮箱")
|
||||
private String email;
|
||||
|
||||
@ApiModelProperty("手机号码")
|
||||
private String mobile;
|
||||
|
||||
}
|
@ -20,11 +20,17 @@ public class LoginRequest {
|
||||
@ApiModelProperty("密码")
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "是否记住")
|
||||
@ApiModelProperty("是否记住")
|
||||
private boolean keep;
|
||||
|
||||
@NotBlank(message = "验证码不允许为空")
|
||||
@ApiModelProperty("验证码")
|
||||
private String captcha;
|
||||
private String code;
|
||||
|
||||
@NotBlank(message = "验证码唯一标识不允许为空")
|
||||
@ApiModelProperty("验证码唯一标识")
|
||||
private String uuid;
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
package com.das.modules.auth.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 账号信息
|
||||
*
|
||||
* @author guchengwei
|
||||
*/
|
||||
@Data
|
||||
public class SysUserVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 账号ID
|
||||
*/
|
||||
@ApiModelProperty("账号ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 账号名
|
||||
*/
|
||||
@ApiModelProperty("账号名")
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 账号本地名
|
||||
*/
|
||||
@ApiModelProperty("账号本地名")
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@ApiModelProperty("手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* E-Mail地址
|
||||
*/
|
||||
@ApiModelProperty("E-Mail地址")
|
||||
private String email;
|
||||
|
||||
|
||||
/**
|
||||
* 所属机构
|
||||
*/
|
||||
@ApiModelProperty("组织机构")
|
||||
private Long orgId;
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.das.modules.auth.domain.vo;
|
||||
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 令牌实体
|
||||
* @author Administrator
|
||||
*/
|
||||
@Api(description = "令牌实体")
|
||||
@Data
|
||||
public class TokenVo {
|
||||
/**
|
||||
* 令牌
|
||||
*/
|
||||
@ApiModelProperty("令牌值")
|
||||
private String token;
|
||||
/**
|
||||
* 令牌超时时间,单位(秒)
|
||||
*/
|
||||
@ApiModelProperty("令牌超时时间,单位(秒)")
|
||||
public long tokenTimeout;
|
||||
/**
|
||||
* 令牌活动超时间隔,单位(秒)
|
||||
*/
|
||||
@ApiModelProperty("令牌活动超时间隔,单位(秒)")
|
||||
public long tokenActivityTimeout;
|
||||
/**
|
||||
* 账号ID
|
||||
*/
|
||||
@ApiModelProperty("账号ID")
|
||||
public long accountId;
|
||||
}
|
@ -14,6 +14,7 @@ import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@ -35,43 +36,29 @@ public class SysUser extends BaseEntity {
|
||||
@TableId(value = "id", type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
@ApiModelProperty("用户账号")
|
||||
private String username;
|
||||
@ApiModelProperty("登录账号")
|
||||
private String account;
|
||||
|
||||
@ApiModelProperty("用户昵称")
|
||||
private String nickName;
|
||||
|
||||
@ApiModelProperty("真实姓名")
|
||||
private String realName;
|
||||
|
||||
@ApiModelProperty("用户类型(00系统用户)")
|
||||
private String userType;
|
||||
|
||||
@ApiModelProperty("用户邮箱")
|
||||
private String email;
|
||||
|
||||
@ApiModelProperty("手机号码")
|
||||
private String mobile;
|
||||
|
||||
@ApiModelProperty("用户性别(1男 2女 3未知)")
|
||||
private String sex;
|
||||
|
||||
@ApiModelProperty("头像地址")
|
||||
private String avatar;
|
||||
|
||||
// @JsonDeserialize(using = LocalDateDeserializer.class)
|
||||
// @JsonSerialize(using = LocalDateSerializer.class)
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
@ApiModelProperty("出生年月")
|
||||
private LocalDate birthday;
|
||||
|
||||
@JSONField(serialize = false)
|
||||
@ApiModelProperty("密码")
|
||||
@ApiModelProperty("加密后的密码")
|
||||
private String password;
|
||||
|
||||
@ApiModelProperty("备注")
|
||||
private String remark;
|
||||
@ApiModelProperty("职员名称")
|
||||
private String userName;
|
||||
|
||||
@ApiModelProperty("职员邮箱")
|
||||
private String email;
|
||||
|
||||
@ApiModelProperty("职员联系电话")
|
||||
private String phone;
|
||||
|
||||
@ApiModelProperty("所属机构id")
|
||||
private Long orgId;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@ApiModelProperty("最近一次登录时间")
|
||||
private Date lastLogin;
|
||||
|
||||
@ApiModelProperty("乐观锁")
|
||||
private int revision;
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,13 @@
|
||||
package com.das.modules.auth.service;
|
||||
|
||||
|
||||
import com.das.modules.auth.domain.request.LoginRequest;
|
||||
import com.das.common.result.R;
|
||||
import com.das.modules.auth.domain.LoginUserDetails;
|
||||
import com.das.modules.auth.domain.vo.LoginVO;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
@ -12,16 +17,17 @@ public interface ILoginService {
|
||||
|
||||
/**
|
||||
* 登录接口
|
||||
* @param loginRequest 登录请求
|
||||
*
|
||||
* @param data 登录请求
|
||||
* @return 登录成功
|
||||
*/
|
||||
String login(LoginRequest loginRequest);
|
||||
LoginUserDetails login(String data, HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException;
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @return 是否退出成功
|
||||
*/
|
||||
boolean logout();
|
||||
boolean logout(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
|
@ -1,68 +1,116 @@
|
||||
package com.das.modules.auth.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.das.common.captcha.CaptchaUtil;
|
||||
import com.das.common.exceptions.ServiceException;
|
||||
import com.das.common.result.R;
|
||||
import com.das.common.result.ResultCode;
|
||||
import com.das.common.utils.AESUtil;
|
||||
import com.das.common.utils.AdminRedisTemplate;
|
||||
import com.das.modules.auth.domain.LoginUserDetails;
|
||||
import com.das.modules.auth.domain.request.LoginRequest;
|
||||
import com.das.modules.auth.domain.vo.SysUserVo;
|
||||
import com.das.modules.auth.entity.SysUser;
|
||||
import com.das.modules.auth.mapper.SysUserMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
//import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import com.das.modules.auth.domain.vo.LoginVO;
|
||||
import com.das.modules.auth.service.ILoginService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author chenhaojie
|
||||
* @Description
|
||||
*/
|
||||
@Service
|
||||
public class LoginServiceImpl implements ILoginService {
|
||||
@Resource
|
||||
private SysUserMapper sysUserMapper;
|
||||
|
||||
@Autowired
|
||||
private AdminRedisTemplate adminRedisTemplate;
|
||||
|
||||
@Resource
|
||||
private AESUtil aesUtil;
|
||||
|
||||
@Value("${aesKey}")
|
||||
private String key;
|
||||
|
||||
// @Override
|
||||
// public String login(LoginRequest loginRequest) {
|
||||
// String captcha = loginRequest.getUsername();
|
||||
// String account = loginRequest.getUsername();
|
||||
// String password = loginRequest.getPassword();
|
||||
// String uuid = loginRequest.getPassword();
|
||||
//
|
||||
// // 验证码验证逻辑(这里省略,需根据实际情况实现)
|
||||
// if (CaptchaUtil.checkVerificationCode(uuid, captcha, adminRedisTemplate)) {
|
||||
// return "验证码错误";
|
||||
// }
|
||||
// // 用户名密码验证
|
||||
//// if (isValidUser(account, password)) {
|
||||
// // 登录成功,使用Sa-Token生成Token
|
||||
// StpUtil.login(account);
|
||||
// // 返回Token给客户端
|
||||
// return StpUtil.getTokenValue();
|
||||
//// } else {
|
||||
//// return "用户名或密码错误";
|
||||
//
|
||||
// }
|
||||
|
||||
@Override
|
||||
public String login(LoginRequest loginRequest) {
|
||||
String captcha = loginRequest.getUsername();
|
||||
String account = loginRequest.getUsername();
|
||||
public LoginUserDetails login(String param, HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException {
|
||||
String iv = request.getHeader("v");
|
||||
System.out.println("iv:" + iv);
|
||||
param = aesUtil.decrypt(key, param, iv);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
LoginRequest loginRequest = objectMapper.readValue(param, LoginRequest.class);
|
||||
String name = loginRequest.getUsername();
|
||||
String password = loginRequest.getPassword();
|
||||
String uuid = loginRequest.getPassword();
|
||||
String code = loginRequest.getCode();
|
||||
String uuid = loginRequest.getUuid();
|
||||
|
||||
// 验证码验证逻辑(这里省略,需根据实际情况实现)
|
||||
if (CaptchaUtil.checkVerificationCode(uuid, captcha, adminRedisTemplate)) {
|
||||
return "验证码错误";
|
||||
|
||||
LambdaQueryWrapper<SysUser> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(SysUser::getAccount, loginRequest.getUsername());
|
||||
SysUser sysUser = sysUserMapper.selectOne(wrapper);
|
||||
if (sysUser == null) {
|
||||
throw new ServiceException("无账号信息");
|
||||
}
|
||||
// 用户名密码验证
|
||||
// if (isValidUser(account, password)) {
|
||||
// 登录成功,使用Sa-Token生成Token
|
||||
StpUtil.login(account);
|
||||
// 返回Token给客户端
|
||||
return StpUtil.getTokenValue();
|
||||
// } else {
|
||||
// return "用户名或密码错误";
|
||||
|
||||
if (!CaptchaUtil.checkVerificationCode(uuid, code, adminRedisTemplate)) {
|
||||
throw new ServiceException("验证码不正确");
|
||||
}
|
||||
StpUtil.login(sysUser.getId()); // 执行登录,这里username为用户唯一标识
|
||||
SysUserVo sysUserVo = new SysUserVo();
|
||||
sysUserVo.setId(sysUser.getId());
|
||||
sysUserVo.setAccount(sysUser.getAccount());
|
||||
sysUserVo.setUserName(sysUser.getUserName());
|
||||
sysUserVo.setPhone(sysUser.getPhone());
|
||||
sysUserVo.setEmail(sysUser.getEmail());
|
||||
sysUserVo.setOrgId(sysUser.getOrgId());
|
||||
LoginUserDetails loginInfo = new LoginUserDetails();
|
||||
loginInfo.setSysUser(sysUserVo); // 存储用户信息到会话
|
||||
loginInfo.setToken(StpUtil.getTokenValue());
|
||||
return loginInfo;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean logout() {
|
||||
// // 调用注销接口的时候需要携带token
|
||||
// // 从 SecurityContextHolder 请求中获取认证信息,然后再获取username
|
||||
// SecurityContext context = SecurityContextHolder.getContext();
|
||||
// Authentication authentication = context.getAuthentication();
|
||||
// if (authentication == null){
|
||||
// return false;
|
||||
// }
|
||||
// // 认证之后 principal 里面是 UserDetails 的子类
|
||||
// // 未认证的时候 principal 里面是 username (登录账号)
|
||||
// Object principal = authentication.getPrincipal();
|
||||
// // UserLogin 实现了 UserDetails 接口
|
||||
// LoginUserDetails userLogin = (LoginUserDetails) principal;
|
||||
// SysUser user = userLogin.getSysUser();
|
||||
// String username = user.getUsername();
|
||||
// String uuid = userLogin.getUuid();
|
||||
// // 认证设置为空
|
||||
// context.setAuthentication(null);
|
||||
// // 删除redis中的token
|
||||
// return adminRedisTemplate.del(username+":"+uuid);
|
||||
public boolean logout(HttpServletRequest request) {
|
||||
String iv = request.getHeader("v");
|
||||
String token = request.getHeader("token");
|
||||
System.out.println("iv:" + iv);
|
||||
token = aesUtil.decrypt(key, token, iv);
|
||||
StpUtil.logoutByTokenValue(token);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -72,11 +120,11 @@ public class LoginServiceImpl implements ILoginService {
|
||||
// String uuid = (String) claim.get("uuid");
|
||||
// String username = (String) claim.get("username");
|
||||
// String refresh = (String) claim.get("refresh");
|
||||
// if (StrUtil.isEmpty(refresh)){
|
||||
// if (StrUtil.isEmpty(refresh)) {
|
||||
// throw new BusinessException("非法token");
|
||||
// }
|
||||
// LoginUserDetails loginUserDetails = adminRedisTemplate.get(username + ":refresh:" + uuid);
|
||||
// if (loginUserDetails == null){
|
||||
// if (loginUserDetails == null) {
|
||||
// throw new BusinessException("token过期,请重新登录");
|
||||
// }
|
||||
// String token = tokenService.createToken(loginUserDetails);
|
||||
@ -84,7 +132,7 @@ public class LoginServiceImpl implements ILoginService {
|
||||
// String tokenPrefix = jwtProperties.getTokenPrefix();
|
||||
// // 删除原有刷新token
|
||||
// adminRedisTemplate.del(username + ":refresh:" + uuid);
|
||||
// return new LoginVO(tokenPrefix + " " +token,newRefreshToken ,jwtProperties.getExpireTime());
|
||||
// return new LoginVO(tokenPrefix + " " + token, newRefreshToken, jwtProperties.getExpireTime());
|
||||
return new LoginVO();
|
||||
}
|
||||
|
||||
|
@ -34,3 +34,5 @@ spring:
|
||||
# 日志配置文件位置
|
||||
logging:
|
||||
config: classpath:log/logback-spring-dev.xml
|
||||
|
||||
aesKey: b6967ee87b86d85a
|
||||
|
@ -23,7 +23,7 @@ sa-token:
|
||||
# token前缀
|
||||
token-prefix: Bearer
|
||||
# token有效期,单位秒
|
||||
timeout: 3600
|
||||
timeout: 7200
|
||||
# 是否允许同一账号多终端登录,默认为true
|
||||
is-concurrent: true
|
||||
|
||||
@ -76,7 +76,7 @@ spring:
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
# 搜索指定包别名
|
||||
typeAliasesPackage: cn.mesmile.**.entity
|
||||
typeAliasesPackage: com.das.**.entity
|
||||
global-config:
|
||||
# 关闭MP3.0自带的banner
|
||||
banner: false
|
||||
|
@ -5,34 +5,27 @@
|
||||
<!-- 通用查询映射结果 -->
|
||||
<resultMap id="BaseResultMap" type="com.das.modules.auth.entity.SysUser">
|
||||
<id column="id" property="id"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="deleted" property="deleted"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="create_by" property="createBy"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
<result column="update_by" property="updateBy"/>
|
||||
<result column="created_time" property="createdTime"/>
|
||||
<result column="created_by" property="createdBy"/>
|
||||
<result column="updated_time" property="updatedTime"/>
|
||||
<result column="updated_by" property="updatedBy"/>
|
||||
<result column="account" property="account"/>
|
||||
<result column="nick_name" property="nickName"/>
|
||||
<result column="real_name" property="realName"/>
|
||||
<result column="user_type" property="userType"/>
|
||||
<result column="email" property="email"/>
|
||||
<result column="mobile" property="mobile"/>
|
||||
<result column="sex" property="sex"/>
|
||||
<result column="avatar" property="avatar"/>
|
||||
<result column="birthday" property="birthday"/>
|
||||
<result column="password" property="password"/>
|
||||
<result column="remark" property="remark"/>
|
||||
<result column="user_name" property="userName"/>
|
||||
<result column="email" property="email"/>
|
||||
<result column="phone" property="phone"/>
|
||||
<result column="org_id" property="orgId"/>
|
||||
<result column="last_login" property="lastLogin"/>
|
||||
<result column="revision" property="revision"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 通用查询结果列 -->
|
||||
<sql id="Base_Column_List">
|
||||
status,
|
||||
deleted,
|
||||
create_time,
|
||||
create_by,
|
||||
update_time,
|
||||
update_by,
|
||||
id, account, nick_name, real_name, user_type, email, mobile, sex, avatar, birthday, password, remark
|
||||
created_time,
|
||||
created_by,
|
||||
updated_time,
|
||||
updated_by,
|
||||
id, account, user_name, org_id, last_login, email, phone, revision, password
|
||||
</sql>
|
||||
|
||||
</mapper>
|
||||
|
Loading…
Reference in New Issue
Block a user