feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.destiny</groupId>
<artifactId>everything-is-suitable-api</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>everything-is-suitable-common</artifactId>
<packaging>jar</packaging>
<name>Everything Is Suitable Common</name>
<description>Common utilities and shared components for Everything Is Suitable API</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,15 @@
package io.destiny.common.annotation;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SensitiveData {
SensitiveType type() default SensitiveType.PHONE;
boolean encrypt() default true;
boolean mask() default true;
}
@@ -0,0 +1,40 @@
package io.destiny.common.annotation;
public enum SensitiveType {
PHONE(3, 4, "手机号"),
EMAIL(2, 3, "邮箱"),
ID_CARD(6, 4, "身份证号"),
BANK_CARD(6, 4, "银行卡号"),
PASSWORD(0, 0, "密码"),
CUSTOM(0, 0, "自定义");
private final int prefixLen;
private final int suffixLen;
private final String description;
SensitiveType(int prefixLen, int suffixLen, String description) {
this.prefixLen = prefixLen;
this.suffixLen = suffixLen;
this.description = description;
}
public int getPrefixLen() {
return prefixLen;
}
public int getSuffixLen() {
return suffixLen;
}
public String getDescription() {
return description;
}
}
@@ -0,0 +1,51 @@
package io.destiny.common.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>();
caches.add(buildCache("userCache", 1000, 30, TimeUnit.MINUTES));
caches.add(buildCache("roleCache", 500, 30, TimeUnit.MINUTES));
caches.add(buildCache("menuCache", 500, 30, TimeUnit.MINUTES));
caches.add(buildCache("permissionCache", 1000, 30, TimeUnit.MINUTES));
caches.add(buildCache("almanacCache", 10000, 24, TimeUnit.HOURS));
caches.add(buildCache("dailyFortuneCache", 10000, 24, TimeUnit.HOURS));
caches.add(buildCache("monthlyFortuneCache", 5000, 7, TimeUnit.DAYS));
caches.add(buildCache("yearlyFortuneCache", 5000, 30, TimeUnit.DAYS));
caches.add(buildCache("ziweiChartCache", 5000, 24, TimeUnit.HOURS));
caches.add(buildCache("statisticsCache", 1000, 5, TimeUnit.MINUTES));
caches.add(buildCache("searchCache", 5000, 10, TimeUnit.MINUTES));
caches.add(buildCache("loginCache", 1000, 15, TimeUnit.MINUTES));
caches.add(buildCache("smsCodeCache", 10000, 5, TimeUnit.MINUTES));
caches.add(buildCache("exportTaskCache", 500, 1, TimeUnit.HOURS));
caches.add(buildCache("subscriptionCache", 5000, 1, TimeUnit.HOURS));
cacheManager.setCaches(caches);
return cacheManager;
}
private CaffeineCache buildCache(String name, long maxSize, long expireTime, TimeUnit timeUnit) {
return new CaffeineCache(name, Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireTime, timeUnit)
.recordStats()
.build());
}
}
@@ -0,0 +1,95 @@
package io.destiny.common.config;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Schema(description = "错误响应")
public class ErrorResponse {
@Schema(description = "错误码")
private String code;
@Schema(description = "错误消息")
private String message;
@Schema(description = "错误详情")
private String details;
@Schema(description = "请求路径")
private String path;
@Schema(description = "时间戳")
private LocalDateTime timestamp;
@Schema(description = "附加信息")
private Map<String, Object> extra;
public ErrorResponse() {
this.timestamp = LocalDateTime.now();
}
public ErrorResponse(String code, String message, String path) {
this();
this.code = code;
this.message = message;
this.path = path;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public Map<String, Object> getExtra() {
return extra;
}
public void setExtra(Map<String, Object> extra) {
this.extra = extra;
}
public void addExtra(String key, Object value) {
if (this.extra == null) {
this.extra = new HashMap<>();
}
this.extra.put(key, value);
}
}
@@ -0,0 +1,20 @@
package io.destiny.common.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper mapper = builder.createXmlMapper(false).build();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
@@ -0,0 +1,139 @@
package io.destiny.common.exception;
/**
* @author zhangxiang
* @version 1.0
* @description 应用异常类
* @date 2023/11/3 16:42
**/
public class ApplicationException extends RuntimeException {
private static final long serialVersionUID = 100L;
private ExceptionCode exceptionCode;
private String exceptionMessage;
private transient Object[] arguments;
public ApplicationException(ExceptionCode exceptionCode) {
super();
this.exceptionCode = exceptionCode;
}
public ApplicationException(String message) {
super();
this.exceptionMessage = message;
}
public ApplicationException(ExceptionCode exceptionCode, String exceptionMessage) {
super();
this.exceptionCode = exceptionCode;
this.exceptionMessage = exceptionMessage;
}
public ApplicationException(ExceptionCode exceptionCode, Object... arguments) {
super();
this.exceptionCode = exceptionCode;
this.arguments = arguments;
}
public ApplicationException(Throwable cause, ExceptionCode exceptionCode) {
super(cause);
this.exceptionCode = exceptionCode;
}
public ApplicationException(Throwable cause, ExceptionCode exceptionCode, Object... arguments) {
super(cause);
this.exceptionCode = exceptionCode;
this.arguments = arguments;
}
public ApplicationException(ExceptionCode exceptionCode, String exceptionMessage, Object... arguments) {
this.exceptionCode = exceptionCode;
this.exceptionMessage = exceptionMessage;
this.arguments = arguments;
}
public ApplicationException(String message, ExceptionCode exceptionCode, String exceptionMessage,
Object[] arguments) {
super(message);
this.exceptionCode = exceptionCode;
this.exceptionMessage = exceptionMessage;
this.arguments = arguments;
}
public ApplicationException(String message, Throwable cause, ExceptionCode exceptionCode, String exceptionMessage,
Object[] arguments) {
super(message, cause);
this.exceptionCode = exceptionCode;
this.exceptionMessage = exceptionMessage;
this.arguments = arguments;
}
public ApplicationException(Throwable cause, ExceptionCode exceptionCode, String exceptionMessage,
Object[] arguments) {
super(cause);
this.exceptionCode = exceptionCode;
this.exceptionMessage = exceptionMessage;
this.arguments = arguments;
}
public ApplicationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace,
ExceptionCode exceptionCode, String exceptionMessage, Object[] arguments) {
super(message, cause, enableSuppression, writableStackTrace);
this.exceptionCode = exceptionCode;
this.exceptionMessage = exceptionMessage;
this.arguments = arguments;
}
/**
* The HTTP error code associated with this error.
*
* @return The HTTP error code associated with this error.
*/
public int getHttpErrorCode() {
if (exceptionCode != null && exceptionCode.getHttpCode() != null) {
try {
return Integer.parseInt(exceptionCode.getHttpCode());
} catch (NumberFormatException e) {
return 500;
}
}
return 500;
}
/**
* Gets the HTTP error code as string.
*
* @return The HTTP error code as string.
*/
public String getHttpCode() {
if (exceptionCode != null && exceptionCode.getHttpCode() != null) {
return exceptionCode.getHttpCode();
}
return "500";
}
/**
* Gets the value of exceptionCode. *
*
* @return the value of exceptionCode
*/
public ExceptionCode getExceptionCode() {
return exceptionCode;
}
public String getExceptionMessage() {
return exceptionMessage;
}
/**
* Gets the value of arguments. *
*
* @return the value of arguments
*/
public Object[] getArguments() {
return arguments;
}
}
@@ -0,0 +1,37 @@
package io.destiny.common.exception;
import io.destiny.common.utils.EnumSupport;
/**
* 加密异常码
*/
public enum EncryptExceptionCode implements ExceptionCode, EnumSupport {
ENCRYPT_FAIL("500", "5001", "加密失败"),
DECRYPT_FAIL("500", "5002", "解密失败");
private final String httpCode;
private final String businessCode;
private final String displayName;
EncryptExceptionCode(String httpCode, String businessCode, String displayName) {
this.httpCode = httpCode;
this.businessCode = businessCode;
this.displayName = displayName;
}
@Override
public String getHttpCode() {
return httpCode;
}
@Override
public String getCode() {
return businessCode;
}
@Override
public String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,17 @@
package io.destiny.common.exception;
import java.io.Serializable;
/**
* @author zhangxiang
* @version 1.0
* @description 代码异常
* @date 2023/11/3 16:43
**/
public interface ExceptionCode extends Serializable {
String getHttpCode();
String getCode();
String getDisplayName();
}
@@ -0,0 +1,97 @@
package io.destiny.common.exception;
import io.destiny.common.response.Result;
import io.destiny.common.utils.HttpStatusUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeoutException;
public class ReactiveGlobalExceptionHandler implements WebExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(ReactiveGlobalExceptionHandler.class);
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
if (ex instanceof ApplicationException) {
return handleApplicationException(exchange, (ApplicationException) ex);
}
if (ex instanceof ResponseStatusException) {
return handleResponseStatusException(exchange, (ResponseStatusException) ex);
}
if (ex instanceof TimeoutException || ex.getCause() instanceof TimeoutException) {
return handleTimeoutException(exchange, ex);
}
if (ex instanceof IllegalArgumentException) {
return handleValidationException(exchange, (IllegalArgumentException) ex);
}
return handleException(exchange, ex);
}
private Mono<Void> handleApplicationException(ServerWebExchange exchange, ApplicationException ex) {
String httpCode = ex.getHttpCode();
String businessCode = ex.getExceptionCode() != null ? ex.getExceptionCode().getCode() : "500";
String message = ex.getExceptionMessage() != null ? ex.getExceptionMessage() :
(ex.getExceptionCode() != null ? ex.getExceptionCode().getDisplayName() : "系统异常");
logger.warn("Application exception: HTTP={}, Business={}, Message={}", httpCode, businessCode, ex.getMessage());
HttpStatus status = HttpStatusUtils.getHttpStatusByCode(httpCode);
exchange.getResponse().setStatusCode(status);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result = Result.error(businessCode, message);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(result.toString().getBytes())));
}
private Mono<Void> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException ex) {
HttpStatus status = (HttpStatus) ex.getStatusCode();
String message = ex.getReason() != null ? ex.getReason() : status.getReasonPhrase();
logger.warn("Response status exception: Status={}, Message={}", status, message);
exchange.getResponse().setStatusCode(status);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result = Result.error(String.valueOf(status.value()), message);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(result.toString().getBytes())));
}
private Mono<Void> handleTimeoutException(ServerWebExchange exchange, Throwable ex) {
logger.error("Timeout exception: ", ex);
exchange.getResponse().setStatusCode(HttpStatus.REQUEST_TIMEOUT);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result = Result.error("408", "请求超时,请稍后重试");
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(result.toString().getBytes())));
}
private Mono<Void> handleValidationException(ServerWebExchange exchange, IllegalArgumentException ex) {
logger.warn("Validation exception: {}", ex.getMessage());
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result = Result.error("400", ex.getMessage());
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(result.toString().getBytes())));
}
private Mono<Void> handleException(ServerWebExchange exchange, Throwable ex) {
logger.error("System exception: ", ex);
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result = Result.error("500", "系统异常,请稍后重试");
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(result.toString().getBytes())));
}
}
@@ -0,0 +1,85 @@
package io.destiny.common.primitive;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.EmailValidator;
import io.destiny.common.utils.EnumSupport;
import java.util.Objects;
public final class EmailAddress {
private static final EmailValidator EMAIL_VALIDATOR = EmailValidator.getInstance();
private final String value;
private final Type type;
public String getValue() {
return value;
}
public Type getType() {
return type;
}
private EmailAddress(String value, Type type) {
this.value = value;
this.type = type;
}
public static EmailAddress of(String value) {
if (StringUtils.isEmpty(value)) {
return null;
}
validate(value);
return new EmailAddress(value, Type.EMAIL);
}
private static void validate(String value) {
if (!EMAIL_VALIDATOR.isValid(value)) {
throw new IllegalArgumentException("邮箱地址格式不正确");
}
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
EmailAddress that = (EmailAddress) o;
return Objects.equals(value, that.value) && type == that.type;
}
@Override
public int hashCode() {
return Objects.hash(value, type);
}
@Override
public String toString() {
return value;
}
public enum Type implements EnumSupport {
EMAIL("email", "邮箱");
private final String code;
private final String displayName;
Type(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
@Override
public String getCode() {
return code;
}
@Override
public String getDisplayName() {
return displayName;
}
}
}
@@ -0,0 +1,68 @@
package io.destiny.common.primitive;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.InetAddressValidator;
public final class IpAddress {
private static final InetAddressValidator INET_ADDRESS_VALIDATOR = InetAddressValidator.getInstance();
private final String value;
private final Type type;
public String getValue() {
return value;
}
public Type getType() {
return type;
}
private IpAddress(String value, Type type) {
this.value = value;
this.type = type;
}
public static IpAddress of(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
Type type = validate(value);
return new IpAddress(value, type);
}
private static Type validate(String value) {
if (INET_ADDRESS_VALIDATOR.isValidInet4Address(value)) {
return Type.IPV4;
}
if (INET_ADDRESS_VALIDATOR.isValidInet6Address(value)) {
return Type.IPV6;
}
throw new IllegalArgumentException("IP地址不合法");
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
IpAddress ipAddress = (IpAddress) o;
return value.equals(ipAddress.value) && type == ipAddress.type;
}
@Override
public int hashCode() {
return value.hashCode() * 31 + type.hashCode();
}
@Override
public String toString() {
return value;
}
public enum Type {
IPV4,
IPV6
}
}
@@ -0,0 +1,66 @@
package io.destiny.common.primitive;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Pattern;
public final class MacAddress {
private static final String REGEX_MAC = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$";
private static final Pattern MAC_PATTERN = Pattern.compile(REGEX_MAC);
private final String value;
private final Type type;
public String getValue() {
return value;
}
public Type getType() {
return type;
}
private MacAddress(String value, Type type) {
this.value = value;
this.type = type;
}
public static MacAddress of(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
Type type = validate(value);
return new MacAddress(value, type);
}
private static Type validate(String value) {
if (MAC_PATTERN.matcher(value).matches()) {
return Type.MAC;
}
throw new IllegalArgumentException("MAC地址不合法");
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
MacAddress macAddress = (MacAddress) o;
return value.equals(macAddress.value) && type == macAddress.type;
}
@Override
public int hashCode() {
return value.hashCode() * 31 + type.hashCode();
}
@Override
public String toString() {
return value;
}
public enum Type {
MAC
}
}
@@ -0,0 +1,159 @@
package io.destiny.common.primitive;
import org.apache.commons.lang3.StringUtils;
import io.destiny.common.utils.EnumSupport;
import java.util.regex.Pattern;
public final class PhoneNumber {
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
private static final Pattern LANDLINE_PATTERN = Pattern.compile("^0\\d{2,3}-\\d{8}$");
private static final Pattern LANDLINE_WITHOUT_AREA_CODE_PATTERN = Pattern.compile("^\\d{7,8}$");
private final String value;
private final Type type;
public String getValue() {
return value;
}
public Type getType() {
return type;
}
private PhoneNumber(String value, Type type) {
this.value = value;
this.type = type;
}
/**
* 根据号码长度自动选择类型
*
* @param value 号码
* @return 电话号码
*/
public static PhoneNumber of(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
if (value.length() == 11) {
return mobileOf(value);
} else if (value.length() == 12 || value.length() == 13) {
return landlineOf(value);
} else if (value.length() == 7 || value.length() == 8) {
return landlineOfWithoutAreaCode(value);
} else {
throw new IllegalArgumentException("号码格式不正确");
}
}
public static PhoneNumber mobileOf(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("手机号码不能为空");
}
validateMobile(value);
return new PhoneNumber(value, Type.MOBILE);
}
public static PhoneNumber landlineOf(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("座机号码不能为空");
}
validateLandline(value);
return new PhoneNumber(value, Type.LANDLINE);
}
public static PhoneNumber landlineOfWithoutAreaCode(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("座机号码不能为空");
}
validateLandlineWithoutAreaCode(value);
return new PhoneNumber(value, Type.LANDLINE);
}
private static void validateMobile(String value) {
if (!MOBILE_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("手机号码格式不正确");
}
}
private static void validateLandline(String value) {
if (!LANDLINE_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("座机号码格式不正确");
}
}
private static void validateLandlineWithoutAreaCode(String value) {
if (!LANDLINE_WITHOUT_AREA_CODE_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("座机号码格式不正确");
}
}
public String getAreaCode() {
if (type == Type.LANDLINE) {
String[] parts = value.split("-");
if (parts.length > 1) {
return parts[0];
}
}
return "";
}
public String getMainNumber() {
if (type == Type.LANDLINE) {
String[] parts = value.split("-");
if (parts.length > 1) {
return parts[1];
} else {
return value;
}
} else {
return value;
}
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
PhoneNumber that = (PhoneNumber) o;
return value.equals(that.value) && type == that.type;
}
@Override
public int hashCode() {
return value.hashCode() * 31 + type.hashCode();
}
@Override
public String toString() {
return value;
}
public enum Type implements EnumSupport {
MOBILE("mobile", "手机"),
LANDLINE("landline", "座机");
private final String code;
private final String displayName;
Type(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
@Override
public String getCode() {
return code;
}
@Override
public String getDisplayName() {
return displayName;
}
}
}
@@ -0,0 +1,85 @@
package io.destiny.common.request;
import org.apache.commons.lang3.StringUtils;
public class PageQuery {
private int pageNum;
private int pageSize;
private String sortField;
private String sortOrder;
public PageQuery() {
this.pageNum = 1;
this.pageSize = 10;
this.sortOrder = "ASC";
}
public PageQuery(int pageNum, int pageSize) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.sortOrder = "ASC";
}
public PageQuery(int pageNum, int pageSize, String sortField, String sortOrder) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.sortField = sortField;
this.sortOrder = sortOrder;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public String getSortField() {
return sortField;
}
public void setSortField(String sortField) {
this.sortField = sortField;
}
public String getSortOrder() {
return sortOrder;
}
public void setSortOrder(String sortOrder) {
this.sortOrder = sortOrder;
}
public int getOffset() {
return (pageNum - 1) * pageSize;
}
public int getLimit() {
return pageSize;
}
public boolean hasSort() {
return StringUtils.isNotBlank(sortField);
}
public static PageQuery of(int pageNum, int pageSize) {
return new PageQuery(pageNum, pageSize);
}
public static PageQuery of(int pageNum, int pageSize, String sortField, String sortOrder) {
return new PageQuery(pageNum, pageSize, sortField, sortOrder);
}
}
@@ -0,0 +1,45 @@
package io.destiny.common.response;
import java.util.List;
/**
* @author zhangxiang
* @version 1.0
* @description 分页对象
* @date 2024/2/21 16:23
**/
public class PageModel<T> {
/**
* 总条数
*/
private Long count;
/**
* 数据对象
*/
private List<T> data;
public Long getCount() {
return count;
}
public void setCount(Long count) {
this.count = count;
}
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
this.data = data;
}
public PageModel(Long count, List<T> data) {
this.count = count;
this.data = data;
}
}
@@ -0,0 +1,77 @@
package io.destiny.common.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "统一响应结果")
public class Result<T> {
@Schema(description = "响应状态码", example = "200")
private String code;
@Schema(description = "响应消息", example = "Success")
private String message;
@Schema(description = "响应数据")
private T data;
public Result() {
}
public Result(String code, String message) {
this.code = code;
this.message = message;
}
public Result(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Result<T> success() {
return new Result<>("200", "Success");
}
public static <T> Result<T> success(T data) {
return new Result<>("200", "Success", data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<>("200", message, data);
}
public static <T> Result<T> error(String code, String message) {
return new Result<>(code, message);
}
public static <T> Result<T> error(String message) {
return new Result<>("500", message);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
@@ -0,0 +1,115 @@
package io.destiny.common.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
public class JwtAuthenticationFilter implements WebFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private static final String USER_ID_HEADER = "X-User-Id";
private static final String USERNAME_HEADER = "X-Username";
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
System.out.println("JwtAuthenticationFilter checking path: " + path);
logger.debug("JwtAuthenticationFilter checking path: {}", path);
if (path.startsWith("/sys/auth/login") ||
path.startsWith("/sys/auth/register") ||
path.startsWith("/sys/auth/refresh") ||
path.startsWith("/sys/auth/logout") ||
path.startsWith("/api/auth/login") ||
path.startsWith("/api/auth/register") ||
path.startsWith("/swagger-ui") ||
path.startsWith("/v3/api-docs") ||
path.startsWith("/actuator") ||
path.equals("/health")) {
System.out.println("Skipping authentication for path: " + path);
logger.debug("Skipping authentication for path: {}", path);
return chain.filter(exchange);
}
String authHeader = request.getHeaders().getFirst(AUTHORIZATION_HEADER);
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
logger.warn("Missing or invalid authorization header for path: {}", path);
return unauthorized(exchange.getResponse());
}
String token = authHeader.substring(BEARER_PREFIX.length());
if (!jwtTokenProvider.validateToken(token)) {
logger.warn("Invalid token for path: {}", path);
return unauthorized(exchange.getResponse());
}
if (jwtTokenProvider.isTokenExpired(token)) {
logger.warn("Expired token for path: {}", path);
return unauthorized(exchange.getResponse());
}
Long userId = jwtTokenProvider.getUserIdFromToken(token);
String username = jwtTokenProvider.getUsernameFromToken(token);
if (userId == null || username == null) {
logger.warn("Unable to extract user information from token for path: {}", path);
return unauthorized(exchange.getResponse());
}
ServerHttpRequest modifiedRequest = request.mutate()
.header(USER_ID_HEADER, userId.toString())
.header(USERNAME_HEADER, username)
.build();
ServerWebExchange modifiedExchange = exchange.mutate()
.request(modifiedRequest)
.build();
logger.debug("Successfully authenticated user: {} for path: {}", username, path);
return chain.filter(modifiedExchange);
}
private boolean shouldSkipAuthentication(String path) {
return path.startsWith("/api/auth/login") ||
path.startsWith("/api/auth/register") ||
path.startsWith("/sys/auth/login") ||
path.startsWith("/sys/auth/register") ||
path.startsWith("/sys/auth/refresh") ||
path.startsWith("/sys/auth/logout") ||
path.startsWith("/swagger-ui") ||
path.startsWith("/v3/api-docs") ||
path.startsWith("/actuator") ||
path.equals("/health");
}
private Mono<Void> unauthorized(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json");
String body = "{\"code\":401,\"message\":\"Unauthorized\"}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(body.getBytes())));
}
}
@@ -0,0 +1,146 @@
package io.destiny.common.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${jwt.secret:this-is-a-secure-jwt-secret-key-that-must-be-at-least-64-characters-long-for-hs512-algorithm}")
private String secret;
@Value("${jwt.expiration:86400000}")
private Long expiration;
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return createToken(claims, userId.toString());
}
public String generateToken(Long userId, String username, Map<String, Object> additionalClaims) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
if (additionalClaims != null) {
claims.putAll(additionalClaims);
}
return createToken(claims, userId.toString());
}
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public Long getUserIdFromToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("userId", Long.class);
} catch (Exception e) {
logger.error("Failed to get user id from token", e);
return null;
}
}
public String getUsernameFromToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("username", String.class);
} catch (Exception e) {
logger.error("Failed to get username from token", e);
return null;
}
}
public Claims getClaimsFromToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
logger.error("Failed to get claims from token", e);
return null;
}
}
public Boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
logger.error("Invalid JWT token", e);
return false;
}
}
public Boolean isTokenExpired(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
logger.error("Failed to check token expiration", e);
return true;
}
}
public Long getExpirationTime(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
Date expiration = claims.getExpiration();
return expiration.getTime();
} catch (Exception e) {
logger.error("Failed to get expiration time from token", e);
return null;
}
}
}
@@ -0,0 +1,171 @@
package io.destiny.common.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.*;
@Component
public class RbacAuthorizationFilter implements WebFilter {
private static final Logger logger = LoggerFactory.getLogger(RbacAuthorizationFilter.class);
private static final String USER_ID_HEADER = "X-User-Id";
private static final String USER_ROLES_HEADER = "X-User-Roles";
private final Map<String, Set<String>> rolePermissions = new HashMap<>();
private final Map<String, Set<String>> pathPermissions = new HashMap<>();
public RbacAuthorizationFilter() {
initializePermissions();
}
private void initializePermissions() {
rolePermissions.put("ADMIN", Set.of(
"users:read", "users:write", "users:delete",
"statistics:read",
"fortune:read", "fortune:write",
"settings:read", "settings:write"
));
rolePermissions.put("MANAGER", Set.of(
"users:read",
"statistics:read",
"fortune:read"
));
rolePermissions.put("OPERATOR", Set.of(
"fortune:read"
));
pathPermissions.put("/api/users", Set.of("users:read", "users:write"));
pathPermissions.put("/api/users/{id}", Set.of("users:read", "users:write", "users:delete"));
pathPermissions.put("/api/statistics", Set.of("statistics:read"));
pathPermissions.put("/api/fortune", Set.of("fortune:read", "fortune:write"));
pathPermissions.put("/api/settings", Set.of("settings:read", "settings:write"));
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
String method = request.getMethod().name();
if (shouldSkipAuthorization(path)) {
logger.debug("Skipping authorization for path: {}", path);
return chain.filter(exchange);
}
List<String> userRolesHeader = request.getHeaders().get(USER_ROLES_HEADER);
if (userRolesHeader == null || userRolesHeader.isEmpty()) {
logger.warn("No roles found in request headers for path: {}", path);
return forbidden(exchange.getResponse());
}
Set<String> requiredPermissions = getRequiredPermissions(path, method);
if (requiredPermissions.isEmpty()) {
logger.debug("No specific permissions required for path: {}", path);
return chain.filter(exchange);
}
Set<String> userRoles = parseRoles(userRolesHeader);
if (userRoles.isEmpty()) {
logger.warn("No valid roles found for path: {}", path);
return forbidden(exchange.getResponse());
}
boolean hasPermission = checkPermissions(userRoles, requiredPermissions);
if (!hasPermission) {
logger.warn("User with roles {} does not have required permissions {} for path: {}",
userRoles, requiredPermissions, path);
return forbidden(exchange.getResponse());
}
logger.debug("User with roles {} authorized for path: {}", userRoles, path);
return chain.filter(exchange);
}
private boolean shouldSkipAuthorization(String path) {
return path.startsWith("/api/auth/login") ||
path.startsWith("/api/auth/register") ||
path.startsWith("/sys/auth/login") ||
path.startsWith("/sys/auth/register") ||
path.startsWith("/sys/auth/refresh") ||
path.startsWith("/sys/auth/logout") ||
path.startsWith("/swagger-ui") ||
path.startsWith("/v3/api-docs") ||
path.startsWith("/actuator") ||
path.equals("/health");
}
private Set<String> getRequiredPermissions(String path, String method) {
String normalizedPath = normalizePath(path);
Set<String> permissions = pathPermissions.get(normalizedPath);
if (permissions == null) {
return Collections.emptySet();
}
if (method.equals("GET")) {
return permissions.stream()
.filter(p -> p.contains(":read"))
.collect(java.util.stream.Collectors.toSet());
} else if (method.equals("POST") || method.equals("PUT")) {
return permissions.stream()
.filter(p -> p.contains(":write"))
.collect(java.util.stream.Collectors.toSet());
} else if (method.equals("DELETE")) {
return permissions.stream()
.filter(p -> p.contains(":delete"))
.collect(java.util.stream.Collectors.toSet());
}
return permissions;
}
private String normalizePath(String path) {
return path.replaceAll("/\\d+", "/{id}");
}
private Set<String> parseRoles(List<String> rolesHeader) {
Set<String> roles = new HashSet<>();
for (String header : rolesHeader) {
String[] roleArray = header.split(",");
for (String role : roleArray) {
String trimmedRole = role.trim();
if (rolePermissions.containsKey(trimmedRole)) {
roles.add(trimmedRole);
}
}
}
return roles;
}
private boolean checkPermissions(Set<String> userRoles, Set<String> requiredPermissions) {
Set<String> userPermissions = new HashSet<>();
for (String role : userRoles) {
Set<String> rolePerms = rolePermissions.get(role);
if (rolePerms != null) {
userPermissions.addAll(rolePerms);
}
}
return userPermissions.containsAll(requiredPermissions);
}
private Mono<Void> forbidden(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json");
String body = "{\"code\":403,\"message\":\"Forbidden\"}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(body.getBytes())));
}
}
@@ -0,0 +1,33 @@
package io.destiny.common.serializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import io.destiny.common.annotation.SensitiveType;
import io.destiny.common.utils.SensitiveDataUtils;
import java.io.IOException;
public class SensitiveDataJsonSerializer extends JsonSerializer<String> {
private final SensitiveType type;
public SensitiveDataJsonSerializer() {
this.type = SensitiveType.CUSTOM;
}
public SensitiveDataJsonSerializer(SensitiveType type) {
this.type = type;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
String maskedValue = SensitiveDataUtils.mask(value, type);
gen.writeString(maskedValue);
}
}
@@ -0,0 +1,33 @@
package io.destiny.common.serializer;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import io.destiny.common.annotation.SensitiveType;
import io.destiny.common.utils.SensitiveDataUtils;
import java.io.IOException;
public class SensitiveDataSerializer extends JsonSerializer<String> {
private final SensitiveType type;
public SensitiveDataSerializer() {
this.type = SensitiveType.CUSTOM;
}
public SensitiveDataSerializer(SensitiveType type) {
this.type = type;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
String maskedValue = SensitiveDataUtils.mask(value, type);
gen.writeString(maskedValue);
}
}
@@ -0,0 +1,73 @@
package io.destiny.common.service;
import io.destiny.common.utils.SM4Utils;
public class SensitiveDataEncryptor {
private static final String ENCRYPTED_PREFIX = "ENC(";
private static final String ENCRYPTED_SUFFIX = ")";
public static String encrypt(String plainText) {
if (plainText == null || plainText.isEmpty()) {
return plainText;
}
try {
String encrypted = SM4Utils.encryptString(plainText);
return ENCRYPTED_PREFIX + encrypted + ENCRYPTED_SUFFIX;
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
public static String decrypt(String encryptedText) {
if (encryptedText == null || encryptedText.isEmpty()) {
return encryptedText;
}
if (!isEncrypted(encryptedText)) {
return encryptedText;
}
try {
String encrypted = encryptedText.substring(
ENCRYPTED_PREFIX.length(),
encryptedText.length() - ENCRYPTED_SUFFIX.length()
);
return SM4Utils.decryptString(encrypted);
} catch (Exception e) {
throw new RuntimeException("解密失败", e);
}
}
public static boolean isEncrypted(String text) {
if (text == null || text.isEmpty()) {
return false;
}
return text.startsWith(ENCRYPTED_PREFIX) && text.endsWith(ENCRYPTED_SUFFIX);
}
public static String encryptIfNotEncrypted(String plainText) {
if (plainText == null || plainText.isEmpty()) {
return plainText;
}
if (isEncrypted(plainText)) {
return plainText;
}
return encrypt(plainText);
}
public static String decryptIfEncrypted(String encryptedText) {
if (encryptedText == null || encryptedText.isEmpty()) {
return encryptedText;
}
if (!isEncrypted(encryptedText)) {
return encryptedText;
}
return decrypt(encryptedText);
}
}
@@ -0,0 +1,45 @@
package io.destiny.common.utils;
/**
* @author zhangxiang
* @version 1.0
* @description 枚举工具类
* @date 2024/1/11 12:18
**/
public interface EnumSupport {
/**
* 根据代码获取枚举值
*
* @param code 代码
* @param enumClass 枚举类
* @return 枚举值
*/
static <T extends Enum<T> & EnumSupport> T valueOfCode(String code, Class<T> enumClass) {
if (code == null || code.isEmpty()) {
return null;
}
for (T enumConstant : enumClass.getEnumConstants()) {
if (code.equals(enumConstant.getCode())) {
return enumConstant;
}
}
throw new IllegalArgumentException("No enum constant " + enumClass.getCanonicalName() + "." + code);
}
/**
* 获取枚举代码
*
* @return
*/
String getCode();
/**
* 获取枚举显示名称
*
* @return
*/
String getDisplayName();
}
@@ -0,0 +1,17 @@
package io.destiny.common.utils;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportField {
String name() default "";
int order() default 0;
String format() default "";
}
@@ -0,0 +1,19 @@
package io.destiny.common.utils;
import org.springframework.http.HttpStatus;
public class HttpStatusUtils {
public static HttpStatus getHttpStatusByCode(String code) {
if (code == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
int statusCode = Integer.parseInt(code);
return HttpStatus.valueOf(statusCode);
} catch (NumberFormatException e) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
}
@@ -0,0 +1,123 @@
package io.destiny.common.utils;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.exception.EncryptExceptionCode;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
public class SM4Utils {
private static final String ALGORITHM_NAME = "SM4";
private static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
private static final String DEFAULT_KEY = "nXwqj7JAe@8zbD4D";
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 加密
*
* @param data 数据
* @param key 秘钥
* @return 密文
*/
public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, ALGORITHM_NAME);
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(data);
return encryptedBytes;
}
/**
* 加密
*
* @param data 数据
* @return 密文
*/
public static byte[] encrypt(byte[] data) throws Exception {
SecretKey secretKey = new SecretKeySpec(DEFAULT_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME);
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(data);
return encryptedBytes;
}
/**
* 加密
*
* @param data 数据
* @return 密文
*/
public static String encryptString(String data) {
try {
SecretKey secretKey = new SecretKeySpec(DEFAULT_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME);
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64String(encryptedBytes);
} catch (Exception e) {
throw new ApplicationException(EncryptExceptionCode.ENCRYPT_FAIL,
EncryptExceptionCode.ENCRYPT_FAIL.getDisplayName());
}
}
/**
* 解密
*
* @param data 数据
* @param key 秘钥
* @return 明文
*/
public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
SecretKey secretKey = new SecretKeySpec(key, ALGORITHM_NAME);
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(data);
return decryptedBytes;
}
/**
* 解密
*
* @param data 数据
* @return 明文
*/
public static String decrypt(byte[] data) {
try {
SecretKey secretKey = new SecretKeySpec(DEFAULT_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM_NAME);
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING,
BouncyCastleProvider.PROVIDER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(data);
String decryptStr = new String(decryptedBytes);
return decryptStr;
} catch (Exception e) {
throw new ApplicationException(EncryptExceptionCode.DECRYPT_FAIL,
EncryptExceptionCode.DECRYPT_FAIL.getDisplayName());
}
}
/**
* 解密
*/
public static String decryptString(String data) {
return decrypt(Base64.decodeBase64(data));
}
}
@@ -0,0 +1,100 @@
package io.destiny.common.utils;
import io.destiny.common.annotation.SensitiveType;
public class SensitiveDataUtils {
private static final String MASK_CHAR = "*";
public static String mask(String data, SensitiveType type) {
if (data == null || data.isEmpty()) {
return data;
}
switch (type) {
case PHONE:
return maskPhone(data);
case EMAIL:
return maskEmail(data);
case ID_CARD:
return maskIdCard(data);
case BANK_CARD:
return maskBankCard(data);
case PASSWORD:
return maskPassword(data);
case CUSTOM:
return maskCustom(data, type.getPrefixLen(), type.getSuffixLen());
default:
return data;
}
}
public static String maskPhone(String phone) {
if (phone == null || phone.length() < 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
int atIndex = email.indexOf("@");
String prefix = email.substring(0, atIndex);
String suffix = email.substring(atIndex);
if (prefix.length() <= 2) {
return prefix.charAt(0) + "***" + suffix;
}
return prefix.substring(0, 2) + "***" + suffix;
}
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 15) {
return idCard;
}
return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4);
}
public static String maskBankCard(String bankCard) {
if (bankCard == null || bankCard.length() < 10) {
return bankCard;
}
return bankCard.substring(0, 6) + "******" + bankCard.substring(bankCard.length() - 4);
}
public static String maskPassword(String password) {
if (password == null) {
return null;
}
return "******";
}
public static String maskCustom(String data, int prefixLen, int suffixLen) {
if (data == null || data.isEmpty()) {
return data;
}
if (data.length() <= prefixLen + suffixLen) {
return repeat(MASK_CHAR, data.length());
}
String prefix = data.substring(0, prefixLen);
String suffix = data.substring(data.length() - suffixLen);
int maskLen = data.length() - prefixLen - suffixLen;
return prefix + repeat(MASK_CHAR, maskLen) + suffix;
}
private static String repeat(String str, int count) {
if (count <= 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(str);
}
return sb.toString();
}
}
@@ -0,0 +1,352 @@
package io.destiny.common.utils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Properties;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Twitter的Snowflake算法实现(性能优化版)
*
* 优化点:
* 1. 使用自适应等待策略,减少CPU空转
* 2. 时间戳缓存机制,降低系统调用频率
* 3. 增强CAS重试策略,提升并发性能
* 4. 完善异常处理和资源管理
*
* @author zhangxiang
* @version 2.0
* @date 2020/2/20 22:09
*/
public final class SnowflakeId {
private static final Logger logger = LoggerFactory.getLogger(SnowflakeId.class);
// 配置常量
private static final String CONFIG_FILE = "snowflake.properties";
private static final String ENV_WORKER_ID = "SNOWFLAKE_WORKER_ID";
private static final String DEFAULT_WORKER_FILE = ".snowflake_worker";
private static final long DEFAULT_EPOCH = 1582136402000L;
private static final int DEFAULT_WORKER_BITS = 10;
private static final int DEFAULT_SEQ_BITS = 12;
private static final int MAX_RETRIES = 10;
private static final long MAX_BACKWARD_MS = 50;
private static final int SPIN_THRESHOLD = 100;
private static final long TIME_CACHE_DURATION_MS = 16;
// 原子状态
private static final AtomicLong lastTimestamp = new AtomicLong(-1L);
private static final AtomicLong sequence = new AtomicLong(0);
private static volatile SnowflakeConfig config;
private static volatile long workerId;
private static volatile long lastTimeCacheMs;
private static volatile int timeCacheHits;
// 初始化块
static {
configure(DEFAULT_WORKER_BITS, DEFAULT_SEQ_BITS, DEFAULT_EPOCH);
}
private static void configure(int workerBits, int seqBits, long epoch) {
validateBits(workerBits, seqBits);
config = new SnowflakeConfig(epoch, workerBits, seqBits);
workerId = resolveWorkerId(config.maxWorkerId);
lastTimeCacheMs = 0;
timeCacheHits = 0;
}
// 核心API
public static long nextId() {
for (int i = 0; i < MAX_RETRIES; i++) {
try {
return nextIdInternal();
} catch (ClockBackwardException e) {
long backwardMs = e.getBackwardMs();
if (backwardMs > MAX_BACKWARD_MS) {
throw e;
}
if (i < SPIN_THRESHOLD) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
} else {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10));
}
}
}
throw new IllegalStateException("Failed to generate ID after " + MAX_RETRIES + " retries");
}
private static long nextIdInternal() {
long currentTs = timeGen();
long lastTs;
long seq;
do {
lastTs = lastTimestamp.get();
if (currentTs < lastTs) {
long backwardMs = lastTs - currentTs;
if (backwardMs <= MAX_BACKWARD_MS) {
lastTimestamp.set(currentTs);
lastTs = currentTs;
} else {
throw new ClockBackwardException(backwardMs);
}
}
if (currentTs == lastTs) {
seq = sequence.incrementAndGet() & config.sequenceMask;
if (seq == 0) {
currentTs = waitNextMillis(currentTs);
}
} else {
seq = 0;
}
} while (!lastTimestamp.compareAndSet(lastTs, currentTs));
return ((currentTs - config.epoch) << config.timestampShift)
| (workerId << config.workerShift)
| seq;
}
private static long waitNextMillis(long currentTs) {
long deadline = currentTs + 2;
int spinCount = 0;
while (currentTs <= lastTimestamp.get()) {
if (currentTs >= deadline) {
return currentTs;
}
if (spinCount < 10) {
spinCount++;
} else if (spinCount < 50) {
LockSupport.parkNanos(100_000);
spinCount++;
} else {
LockSupport.parkNanos(500_000);
}
currentTs = timeGen();
}
return currentTs;
}
private static long timeGen() {
long now = System.currentTimeMillis();
long cached = lastTimeCacheMs;
if (now - cached < TIME_CACHE_DURATION_MS) {
timeCacheHits++;
return cached;
}
synchronized (SnowflakeId.class) {
cached = lastTimeCacheMs;
if (now - cached < TIME_CACHE_DURATION_MS) {
timeCacheHits++;
return cached;
}
lastTimeCacheMs = now;
return now;
}
}
public static int getTimeCacheHits() {
return timeCacheHits;
}
public static void resetTimeCache() {
synchronized (SnowflakeId.class) {
lastTimeCacheMs = 0;
timeCacheHits = 0;
}
}
private static void validateBits(int workerBits, int seqBits) {
if (workerBits < 0 || workerBits > 22) {
throw new IllegalArgumentException("WorkerID位数必须在0-22之间");
}
if (seqBits < 0 || seqBits > 22) {
throw new IllegalArgumentException("序列号位数必须在0-22之间");
}
if (workerBits + seqBits > 22) {
throw new IllegalArgumentException("WorkerID和序列号位数总和不能超过22位,当前为: " + (workerBits + seqBits));
}
if (workerBits + seqBits == 0) {
throw new IllegalArgumentException("WorkerID和序列号位数总和不能为0");
}
}
private static long resolveWorkerId(long maxWorkerId) {
Long id = fromEnv();
if (id == null) {
id = fromConfig();
}
if (id == null) {
id = fromFile();
}
if (id == null) {
id = generateNewId();
}
if (id < 0 || id > maxWorkerId) {
throw new IllegalStateException("WorkerID超出有效范围: " + id + " (有效范围: 0-" + maxWorkerId + ")");
}
return id;
}
private static Long fromEnv() {
String env = System.getenv(ENV_WORKER_ID);
if (env != null) {
try {
return Long.parseLong(env.trim());
} catch (NumberFormatException e) {
logger.warn("Failed to parse worker ID from environment variable {}: {}", ENV_WORKER_ID, env);
}
}
return null;
}
private static Long fromConfig() {
try (InputStream is = SnowflakeId.class.getClassLoader().getResourceAsStream(CONFIG_FILE)) {
if (is == null) {
return null;
}
Properties props = new Properties();
props.load(is);
String val = props.getProperty("worker.id");
if (val != null) {
try {
return Long.parseLong(val.trim());
} catch (NumberFormatException e) {
logger.warn("Failed to parse worker ID from config file: {}", val);
}
}
} catch (IOException e) {
logger.debug("Failed to read config file: {}", CONFIG_FILE);
}
return null;
}
private static Long fromFile() {
Path path = Paths.get(DEFAULT_WORKER_FILE);
if (!Files.exists(path)) {
return null;
}
try (BufferedReader reader = Files.newBufferedReader(path)) {
String content = reader.readLine();
if (content != null && !content.trim().isEmpty()) {
try {
return Long.parseLong(content.trim());
} catch (NumberFormatException e) {
logger.warn("Failed to parse worker ID from file {}: {}", DEFAULT_WORKER_FILE, content);
}
}
} catch (IOException e) {
logger.debug("Failed to read worker ID from file: {}", DEFAULT_WORKER_FILE);
}
return null;
}
private static long generateNewId() {
long newId = ThreadLocalRandom.current().nextLong(config.maxWorkerId + 1);
Path path = Paths.get(DEFAULT_WORKER_FILE);
try (BufferedWriter writer = Files.newBufferedWriter(path,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING)) {
writer.write(String.valueOf(newId));
} catch (IOException e) {
logger.warn("Failed to write worker ID to file: {}", DEFAULT_WORKER_FILE);
}
return newId;
}
public static void config(int workerBits, int seqBits, long epoch) {
configure(workerBits, seqBits, epoch);
}
public static long getWorkerId() {
return workerId;
}
public static SnowflakeIdInfo parseId(long id) {
long timestamp = (id >> config.timestampShift) + config.epoch;
long extractedWorkerId = (id >> config.workerShift) & config.sequenceMask;
long seq = id & config.sequenceMask;
return new SnowflakeIdInfo(timestamp, extractedWorkerId, seq);
}
public static class SnowflakeIdInfo {
private final long timestamp;
private final long workerId;
private final long sequence;
public SnowflakeIdInfo(long timestamp, long workerId, long sequence) {
this.timestamp = timestamp;
this.workerId = workerId;
this.sequence = sequence;
}
public long getTimestamp() {
return timestamp;
}
public long getWorkerId() {
return workerId;
}
public long getSequence() {
return sequence;
}
@Override
public String toString() {
return "SnowflakeIdInfo{timestamp=" + timestamp + ", workerId=" + workerId + ", sequence=" + sequence + "}";
}
}
private static class SnowflakeConfig {
final long epoch;
final int timestampShift;
final int workerShift;
final long sequenceMask;
final long maxWorkerId;
SnowflakeConfig(long epoch, int workerBits, int seqBits) {
this.epoch = epoch;
this.timestampShift = workerBits + seqBits;
this.workerShift = seqBits;
this.sequenceMask = ~(-1L << seqBits);
this.maxWorkerId = ~(-1L << workerBits);
}
}
public static class ClockBackwardException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final long backwardMs;
ClockBackwardException(long backwardMs) {
super("Clock moved backwards by " + backwardMs + "ms");
this.backwardMs = backwardMs;
}
public long getBackwardMs() {
return backwardMs;
}
}
}
@@ -0,0 +1,69 @@
package io.destiny.common.utils;
import org.springframework.web.util.HtmlUtils;
import java.util.regex.Pattern;
public class XssUtils {
private static final Pattern[] XSS_PATTERNS = {
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("onerror(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("onclick(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("onmouseover(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("onfocus(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("onblur(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
Pattern.compile("on(\\w+)(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
};
public static String clean(String input) {
if (input == null || input.isEmpty()) {
return input;
}
String cleanInput = input;
for (Pattern pattern : XSS_PATTERNS) {
cleanInput = pattern.matcher(cleanInput).replaceAll("");
}
cleanInput = HtmlUtils.htmlEscape(cleanInput);
return cleanInput;
}
public static String cleanForHtml(String input) {
if (input == null || input.isEmpty()) {
return input;
}
return HtmlUtils.htmlEscape(input);
}
public static String cleanForJavaScript(String input) {
if (input == null || input.isEmpty()) {
return input;
}
return input.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("'", "\\'")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
public static String cleanForUrl(String input) {
if (input == null || input.isEmpty()) {
return input;
}
return HtmlUtils.htmlEscape(input);
}
}
@@ -0,0 +1,2 @@
# ID: 0~1023
worker.id=0
@@ -0,0 +1,299 @@
package io.destiny.common.exception;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("ApplicationException应用异常测试")
class ApplicationExceptionTest {
@Test
@DisplayName("应该使用异常码创建异常")
void testConstructorWithExceptionCode() {
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "400";
}
@Override
public String getCode() {
return "BAD_REQUEST";
}
@Override
public String getDisplayName() {
return "Bad Request";
}
};
ApplicationException exception = new ApplicationException(code);
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
assertNull(exception.getExceptionMessage(), "异常消息应该为null");
assertNull(exception.getArguments(), "参数应该为null");
}
@Test
@DisplayName("应该使用消息创建异常")
void testConstructorWithMessage() {
String message = "测试异常消息";
ApplicationException exception = new ApplicationException(message);
assertEquals(message, exception.getExceptionMessage(), "异常消息应该正确");
assertNull(exception.getExceptionCode(), "异常码应该为null");
assertNull(exception.getArguments(), "参数应该为null");
}
@Test
@DisplayName("应该使用异常码和消息创建异常")
void testConstructorWithExceptionCodeAndMessage() {
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "500";
}
@Override
public String getCode() {
return "INTERNAL_ERROR";
}
@Override
public String getDisplayName() {
return "Internal Error";
}
};
String message = "内部错误";
ApplicationException exception = new ApplicationException(code, message);
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
assertEquals(message, exception.getExceptionMessage(), "异常消息应该正确");
}
@Test
@DisplayName("应该使用异常码和参数创建异常")
void testConstructorWithExceptionCodeAndArguments() {
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "404";
}
@Override
public String getCode() {
return "NOT_FOUND";
}
@Override
public String getDisplayName() {
return "Not Found";
}
};
Object[] arguments = {"resource", "123"};
ApplicationException exception = new ApplicationException(code, arguments);
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
assertArrayEquals(arguments, exception.getArguments(), "参数应该正确");
}
@Test
@DisplayName("应该使用原因和异常码创建异常")
void testConstructorWithCauseAndExceptionCode() {
Throwable cause = new RuntimeException("原始异常");
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "500";
}
@Override
public String getCode() {
return "SERVER_ERROR";
}
@Override
public String getDisplayName() {
return "Server Error";
}
};
ApplicationException exception = new ApplicationException(cause, code);
assertEquals(cause, exception.getCause(), "原因应该正确");
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
}
@Test
@DisplayName("应该使用原因、异常码和参数创建异常")
void testConstructorWithCauseExceptionCodeAndArguments() {
Throwable cause = new RuntimeException("原始异常");
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "500";
}
@Override
public String getCode() {
return "SERVER_ERROR";
}
@Override
public String getDisplayName() {
return "Server Error";
}
};
Object[] arguments = {"param1", "param2"};
ApplicationException exception = new ApplicationException(cause, code, arguments);
assertEquals(cause, exception.getCause(), "原因应该正确");
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
assertArrayEquals(arguments, exception.getArguments(), "参数应该正确");
}
@Test
@DisplayName("应该正确获取HTTP错误码")
void testGetHttpErrorCode() {
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "403";
}
@Override
public String getCode() {
return "FORBIDDEN";
}
@Override
public String getDisplayName() {
return "Forbidden";
}
};
ApplicationException exception = new ApplicationException(code);
assertEquals(403, exception.getHttpErrorCode(), "HTTP错误码应该正确");
}
@Test
@DisplayName("应该正确获取HTTP错误码字符串")
void testGetHttpCode() {
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "401";
}
@Override
public String getCode() {
return "UNAUTHORIZED";
}
@Override
public String getDisplayName() {
return "Unauthorized";
}
};
ApplicationException exception = new ApplicationException(code);
assertEquals("401", exception.getHttpCode(), "HTTP错误码字符串应该正确");
}
@Test
@DisplayName("当异常码为null时应该返回默认HTTP错误码")
void testDefaultHttpErrorCode() {
ApplicationException exception = new ApplicationException("测试消息");
assertEquals(500, exception.getHttpErrorCode(), "默认HTTP错误码应该是500");
assertEquals("500", exception.getHttpCode(), "默认HTTP错误码字符串应该是500");
}
@Test
@DisplayName("当HTTP码格式错误时应该返回默认值")
void testInvalidHttpCodeFormat() {
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "invalid";
}
@Override
public String getCode() {
return "INVALID";
}
@Override
public String getDisplayName() {
return "Invalid";
}
};
ApplicationException exception = new ApplicationException(code);
assertEquals(500, exception.getHttpErrorCode(), "格式错误的HTTP码应该返回500");
}
@Test
@DisplayName("应该正确设置和获取异常码")
void testSetAndGetExceptionCode() {
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "400";
}
@Override
public String getCode() {
return "BAD_REQUEST";
}
@Override
public String getDisplayName() {
return "Bad Request";
}
};
ApplicationException exception = new ApplicationException(code);
assertEquals(code, exception.getExceptionCode(), "异常码应该正确");
}
@Test
@DisplayName("应该正确设置和获取异常消息")
void testSetAndGetExceptionMessage() {
String message = "测试消息";
ApplicationException exception = new ApplicationException(message);
assertEquals(message, exception.getExceptionMessage(), "异常消息应该正确");
}
@Test
@DisplayName("应该正确设置和获取参数")
void testSetAndGetArguments() {
Object[] arguments = {"arg1", "arg2", "arg3"};
ExceptionCode code = new ExceptionCode() {
@Override
public String getHttpCode() {
return "500";
}
@Override
public String getCode() {
return "ERROR";
}
@Override
public String getDisplayName() {
return "Error";
}
};
ApplicationException exception = new ApplicationException(code, arguments);
assertArrayEquals(arguments, exception.getArguments(), "参数应该正确");
}
}
@@ -0,0 +1,135 @@
package io.destiny.common.primitive;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("EmailAddress邮箱地址测试")
class EmailAddressTest {
@Test
@DisplayName("应该正确创建邮箱地址")
void testCreateEmailAddress() {
EmailAddress email = EmailAddress.of("test@example.com");
assertNotNull(email, "邮箱地址不应该为null");
assertEquals("test@example.com", email.getValue(), "邮箱地址值应该正确");
assertEquals(EmailAddress.Type.EMAIL, email.getType(), "类型应该是邮箱");
}
@Test
@DisplayName("应该正确验证有效的邮箱地址")
void testValidEmailAddresses() {
String[] validEmails = {
"test@example.com",
"user.name@example.com",
"user+tag@example.com",
"user123@example.co.uk",
"test_user@test-domain.com"
};
for (String email : validEmails) {
EmailAddress emailAddress = EmailAddress.of(email);
assertNotNull(emailAddress, email + " 应该是有效的邮箱地址");
assertEquals(email, emailAddress.getValue(), email + " 值应该正确");
}
}
@Test
@DisplayName("应该拒绝无效的邮箱地址")
void testInvalidEmailAddresses() {
String[] invalidEmails = {
"invalid-email",
"test@",
"@example.com",
"test example.com",
"test@example",
"test@.com",
"test@exa mple.com"
};
for (String email : invalidEmails) {
assertThrows(IllegalArgumentException.class, () -> {
EmailAddress.of(email);
}, email + " 应该是无效的邮箱地址");
}
}
@Test
@DisplayName("空值应该返回null")
void testNullValue() {
assertNull(EmailAddress.of(null), "null应该返回null");
assertNull(EmailAddress.of(""), "空字符串应该返回null");
}
@Test
@DisplayName("应该正确比较相等性")
void testEquals() {
EmailAddress email1 = EmailAddress.of("test@example.com");
EmailAddress email2 = EmailAddress.of("test@example.com");
EmailAddress email3 = EmailAddress.of("other@example.com");
assertEquals(email1, email2, "相同邮箱地址应该相等");
assertNotEquals(email1, email3, "不同邮箱地址不应该相等");
assertNotEquals(email1, null, "不应该等于null");
assertNotEquals(email1, "test@example.com", "不应该等于字符串");
}
@Test
@DisplayName("应该正确计算哈希码")
void testHashCode() {
EmailAddress email1 = EmailAddress.of("test@example.com");
EmailAddress email2 = EmailAddress.of("test@example.com");
assertEquals(email1.hashCode(), email2.hashCode(), "相同邮箱地址的哈希码应该相同");
}
@Test
@DisplayName("应该正确转换为字符串")
void testToString() {
EmailAddress email = EmailAddress.of("test@example.com");
assertEquals("test@example.com", email.toString(), "toString应该返回邮箱地址值");
}
@Test
@DisplayName("枚举类型应该正确返回代码和显示名称")
void testEnumType() {
assertEquals("email", EmailAddress.Type.EMAIL.getCode(), "邮箱类型代码应该正确");
assertEquals("邮箱", EmailAddress.Type.EMAIL.getDisplayName(), "邮箱类型显示名称应该正确");
}
@Test
@DisplayName("应该支持大小写不敏感的邮箱地址")
void testCaseInsensitiveEmail() {
EmailAddress email1 = EmailAddress.of("TEST@EXAMPLE.COM");
EmailAddress email2 = EmailAddress.of("test@example.com");
assertNotNull(email1, "大写邮箱地址应该有效");
assertNotNull(email2, "小写邮箱地址应该有效");
assertEquals("TEST@EXAMPLE.COM", email1.getValue(), "邮箱地址值应该保持原样");
assertEquals("test@example.com", email2.getValue(), "邮箱地址值应该保持原样");
}
@Test
@DisplayName("应该支持带子域名的邮箱地址")
void testSubdomainEmail() {
EmailAddress email1 = EmailAddress.of("user@mail.example.com");
EmailAddress email2 = EmailAddress.of("user@sub.mail.example.com");
assertNotNull(email1, "带子域名的邮箱地址应该有效");
assertNotNull(email2, "带多级子域名的邮箱地址应该有效");
}
@Test
@DisplayName("应该支持特殊字符的邮箱地址")
void testSpecialCharactersEmail() {
EmailAddress email1 = EmailAddress.of("user.name+tag@example.com");
EmailAddress email2 = EmailAddress.of("user_name@example.com");
EmailAddress email3 = EmailAddress.of("user-name@example.com");
assertNotNull(email1, "带点的邮箱地址应该有效");
assertNotNull(email2, "带下划线的邮箱地址应该有效");
assertNotNull(email3, "带连字符的邮箱地址应该有效");
}
}
@@ -0,0 +1,192 @@
package io.destiny.common.primitive;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("PhoneNumber电话号码测试")
class PhoneNumberTest {
@Test
@DisplayName("应该正确创建手机号码")
void testCreateMobilePhone() {
PhoneNumber phone = PhoneNumber.mobileOf("13800138000");
assertNotNull(phone, "手机号码不应该为null");
assertEquals("13800138000", phone.getValue(), "手机号码值应该正确");
assertEquals(PhoneNumber.Type.MOBILE, phone.getType(), "类型应该是手机");
}
@Test
@DisplayName("应该正确创建座机号码")
void testCreateLandlinePhone() {
PhoneNumber phone = PhoneNumber.landlineOf("010-12345678");
assertNotNull(phone, "座机号码不应该为null");
assertEquals("010-12345678", phone.getValue(), "座机号码值应该正确");
assertEquals(PhoneNumber.Type.LANDLINE, phone.getType(), "类型应该是座机");
}
@Test
@DisplayName("应该正确创建无区号座机号码")
void testCreateLandlineWithoutAreaCode() {
PhoneNumber phone = PhoneNumber.landlineOfWithoutAreaCode("12345678");
assertNotNull(phone, "座机号码不应该为null");
assertEquals("12345678", phone.getValue(), "座机号码值应该正确");
assertEquals(PhoneNumber.Type.LANDLINE, phone.getType(), "类型应该是座机");
}
@Test
@DisplayName("应该根据号码长度自动选择类型")
void testAutoDetectType() {
PhoneNumber mobile = PhoneNumber.of("13912345678");
assertEquals(PhoneNumber.Type.MOBILE, mobile.getType(), "11位应该是手机");
PhoneNumber landline = PhoneNumber.of("021-12345678");
assertEquals(PhoneNumber.Type.LANDLINE, landline.getType(), "12位应该是座机");
PhoneNumber landline2 = PhoneNumber.of("0123-12345678");
assertEquals(PhoneNumber.Type.LANDLINE, landline2.getType(), "13位应该是座机");
PhoneNumber landline3 = PhoneNumber.of("1234567");
assertEquals(PhoneNumber.Type.LANDLINE, landline3.getType(), "7位应该是座机");
PhoneNumber landline4 = PhoneNumber.of("12345678");
assertEquals(PhoneNumber.Type.LANDLINE, landline4.getType(), "8位应该是座机");
}
@Test
@DisplayName("应该正确获取区号")
void testGetAreaCode() {
PhoneNumber phone1 = PhoneNumber.landlineOf("010-12345678");
assertEquals("010", phone1.getAreaCode(), "区号应该正确");
PhoneNumber phone2 = PhoneNumber.landlineOf("021-87654321");
assertEquals("021", phone2.getAreaCode(), "区号应该正确");
PhoneNumber mobile = PhoneNumber.mobileOf("13800138000");
assertEquals("", mobile.getAreaCode(), "手机号码区号应该为空");
}
@Test
@DisplayName("应该正确获取主号码")
void testGetMainNumber() {
PhoneNumber phone1 = PhoneNumber.landlineOf("010-12345678");
assertEquals("12345678", phone1.getMainNumber(), "主号码应该正确");
PhoneNumber mobile = PhoneNumber.mobileOf("13800138000");
assertEquals("13800138000", mobile.getMainNumber(), "手机主号码应该正确");
PhoneNumber phone2 = PhoneNumber.landlineOfWithoutAreaCode("87654321");
assertEquals("87654321", phone2.getMainNumber(), "无区号座机主号码应该正确");
}
@Test
@DisplayName("应该正确比较相等性")
void testEquals() {
PhoneNumber phone1 = PhoneNumber.mobileOf("13800138000");
PhoneNumber phone2 = PhoneNumber.mobileOf("13800138000");
PhoneNumber phone3 = PhoneNumber.mobileOf("13900139000");
assertEquals(phone1, phone2, "相同号码应该相等");
assertNotEquals(phone1, phone3, "不同号码不应该相等");
assertNotEquals(phone1, null, "不应该等于null");
assertNotEquals(phone1, "13800138000", "不应该等于字符串");
}
@Test
@DisplayName("应该正确计算哈希码")
void testHashCode() {
PhoneNumber phone1 = PhoneNumber.mobileOf("13800138000");
PhoneNumber phone2 = PhoneNumber.mobileOf("13800138000");
assertEquals(phone1.hashCode(), phone2.hashCode(), "相同号码的哈希码应该相同");
}
@Test
@DisplayName("应该正确转换为字符串")
void testToString() {
PhoneNumber phone = PhoneNumber.mobileOf("13800138000");
assertEquals("13800138000", phone.toString(), "toString应该返回号码值");
}
@Test
@DisplayName("空值应该返回null")
void testNullValue() {
assertNull(PhoneNumber.of(null), "null应该返回null");
assertNull(PhoneNumber.of(""), "空字符串应该返回null");
assertNull(PhoneNumber.of(" "), "空白字符串应该返回null");
}
@Test
@DisplayName("应该拒绝无效的手机号码")
void testInvalidMobilePhone() {
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.mobileOf("12345678901");
}, "无效的手机号码应该抛出异常");
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.mobileOf("1380013800");
}, "长度不足的手机号码应该抛出异常");
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.mobileOf(null);
}, "null手机号码应该抛出异常");
}
@Test
@DisplayName("应该拒绝无效的座机号码")
void testInvalidLandlinePhone() {
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.landlineOf("12345678");
}, "无效的座机号码应该抛出异常");
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.landlineOf("010-1234567");
}, "长度不足的座机号码应该抛出异常");
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.landlineOf(null);
}, "null座机号码应该抛出异常");
}
@Test
@DisplayName("应该拒绝无效的无区号座机号码")
void testInvalidLandlineWithoutAreaCode() {
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.landlineOfWithoutAreaCode("123456");
}, "长度不足的座机号码应该抛出异常");
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.landlineOfWithoutAreaCode("123456789");
}, "长度过长的座机号码应该抛出异常");
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.landlineOfWithoutAreaCode(null);
}, "null座机号码应该抛出异常");
}
@Test
@DisplayName("应该拒绝无效长度的号码")
void testInvalidLength() {
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.of("123456");
}, "6位号码应该抛出异常");
assertThrows(IllegalArgumentException.class, () -> {
PhoneNumber.of("12345678901234");
}, "14位号码应该抛出异常");
}
@Test
@DisplayName("枚举类型应该正确返回代码和显示名称")
void testEnumType() {
assertEquals("mobile", PhoneNumber.Type.MOBILE.getCode(), "手机类型代码应该正确");
assertEquals("手机", PhoneNumber.Type.MOBILE.getDisplayName(), "手机类型显示名称应该正确");
assertEquals("landline", PhoneNumber.Type.LANDLINE.getCode(), "座机类型代码应该正确");
assertEquals("座机", PhoneNumber.Type.LANDLINE.getDisplayName(), "座机类型显示名称应该正确");
}
}
@@ -0,0 +1,129 @@
package io.destiny.common.request;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("PageQuery分页查询测试")
class PageQueryTest {
@Test
@DisplayName("应该使用默认构造函数创建分页查询")
void testDefaultConstructor() {
PageQuery query = new PageQuery();
assertEquals(1, query.getPageNum(), "默认页码应该是1");
assertEquals(10, query.getPageSize(), "默认每页大小应该是10");
assertNull(query.getSortField(), "默认排序字段应该是null");
assertEquals("ASC", query.getSortOrder(), "默认排序顺序应该是ASC");
}
@Test
@DisplayName("应该使用页码和页大小构造分页查询")
void testConstructorWithPageNumAndPageSize() {
PageQuery query = new PageQuery(2, 20);
assertEquals(2, query.getPageNum(), "页码应该正确");
assertEquals(20, query.getPageSize(), "每页大小应该正确");
assertNull(query.getSortField(), "排序字段应该是null");
assertEquals("ASC", query.getSortOrder(), "排序顺序应该是ASC");
}
@Test
@DisplayName("应该使用全参构造函数创建分页查询")
void testConstructorWithAllParams() {
PageQuery query = new PageQuery(3, 30, "createTime", "DESC");
assertEquals(3, query.getPageNum(), "页码应该正确");
assertEquals(30, query.getPageSize(), "每页大小应该正确");
assertEquals("createTime", query.getSortField(), "排序字段应该正确");
assertEquals("DESC", query.getSortOrder(), "排序顺序应该正确");
}
@Test
@DisplayName("应该正确计算偏移量")
void testGetOffset() {
PageQuery query1 = new PageQuery(1, 10);
assertEquals(0, query1.getOffset(), "第一页的偏移量应该是0");
PageQuery query2 = new PageQuery(2, 10);
assertEquals(10, query2.getOffset(), "第二页的偏移量应该是10");
PageQuery query3 = new PageQuery(3, 20);
assertEquals(40, query3.getOffset(), "第三页每页20条的偏移量应该是40");
}
@Test
@DisplayName("应该正确获取限制数量")
void testGetLimit() {
PageQuery query1 = new PageQuery(1, 10);
assertEquals(10, query1.getLimit(), "限制数量应该等于每页大小");
PageQuery query2 = new PageQuery(2, 20);
assertEquals(20, query2.getLimit(), "限制数量应该等于每页大小");
}
@Test
@DisplayName("应该正确判断是否有排序")
void testHasSort() {
PageQuery query1 = new PageQuery(1, 10);
assertFalse(query1.hasSort(), "没有排序字段时应该返回false");
PageQuery query2 = new PageQuery(1, 10, "createTime", "ASC");
assertTrue(query2.hasSort(), "有排序字段时应该返回true");
PageQuery query3 = new PageQuery(1, 10, "", "ASC");
assertFalse(query3.hasSort(), "排序字段为空字符串时应该返回false");
}
@Test
@DisplayName("应该使用静态工厂方法创建分页查询")
void testStaticFactoryMethod() {
PageQuery query1 = PageQuery.of(2, 20);
assertEquals(2, query1.getPageNum(), "页码应该正确");
assertEquals(20, query1.getPageSize(), "每页大小应该正确");
PageQuery query2 = PageQuery.of(3, 30, "updateTime", "DESC");
assertEquals(3, query2.getPageNum(), "页码应该正确");
assertEquals(30, query2.getPageSize(), "每页大小应该正确");
assertEquals("updateTime", query2.getSortField(), "排序字段应该正确");
assertEquals("DESC", query2.getSortOrder(), "排序顺序应该正确");
}
@Test
@DisplayName("应该正确设置和获取页码")
void testSetAndGetPageNum() {
PageQuery query = new PageQuery();
query.setPageNum(5);
assertEquals(5, query.getPageNum(), "页码应该正确设置和获取");
}
@Test
@DisplayName("应该正确设置和获取每页大小")
void testSetAndGetPageSize() {
PageQuery query = new PageQuery();
query.setPageSize(50);
assertEquals(50, query.getPageSize(), "每页大小应该正确设置和获取");
}
@Test
@DisplayName("应该正确设置和获取排序字段")
void testSetAndGetSortField() {
PageQuery query = new PageQuery();
query.setSortField("name");
assertEquals("name", query.getSortField(), "排序字段应该正确设置和获取");
}
@Test
@DisplayName("应该正确设置和获取排序顺序")
void testSetAndGetSortOrder() {
PageQuery query = new PageQuery();
query.setSortOrder("DESC");
assertEquals("DESC", query.getSortOrder(), "排序顺序应该正确设置和获取");
}
}
@@ -0,0 +1,90 @@
package io.destiny.common.response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("PageModel分页模型测试")
class PageModelTest {
@Test
@DisplayName("应该创建分页模型")
void testCreatePageModel() {
Long count = 100L;
java.util.List<String> data = Arrays.asList("item1", "item2", "item3");
PageModel<String> pageModel = new PageModel<>(count, data);
assertEquals(count, pageModel.getCount(), "总数应该正确");
assertEquals(data, pageModel.getData(), "数据列表应该正确");
}
@Test
@DisplayName("应该正确设置和获取总数")
void testSetAndGetCount() {
PageModel<String> pageModel = new PageModel<>(0L, Collections.emptyList());
Long count = 200L;
pageModel.setCount(count);
assertEquals(count, pageModel.getCount(), "总数应该正确设置和获取");
}
@Test
@DisplayName("应该正确设置和获取数据列表")
void testSetAndGetData() {
PageModel<String> pageModel = new PageModel<>(0L, Collections.emptyList());
java.util.List<String> data = Arrays.asList("test1", "test2");
pageModel.setData(data);
assertEquals(data, pageModel.getData(), "数据列表应该正确设置和获取");
}
@Test
@DisplayName("应该支持空数据列表")
void testEmptyDataList() {
PageModel<String> pageModel = new PageModel<>(0L, Collections.emptyList());
assertEquals(0L, pageModel.getCount(), "总数应该是0");
assertTrue(pageModel.getData().isEmpty(), "数据列表应该是空的");
}
@Test
@DisplayName("应该支持null数据列表")
void testNullDataList() {
PageModel<String> pageModel = new PageModel<>(0L, null);
assertEquals(0L, pageModel.getCount(), "总数应该是0");
assertNull(pageModel.getData(), "数据列表应该是null");
}
@Test
@DisplayName("应该支持泛型数据")
void testGenericData() {
PageModel<Integer> intPageModel = new PageModel<>(10L, Arrays.asList(1, 2, 3));
assertEquals(10L, intPageModel.getCount(), "应该支持Integer类型");
assertEquals(3, intPageModel.getData().size(), "数据列表大小应该正确");
PageModel<Boolean> boolPageModel = new PageModel<>(5L, Arrays.asList(true, false));
assertEquals(5L, boolPageModel.getCount(), "应该支持Boolean类型");
assertEquals(2, boolPageModel.getData().size(), "数据列表大小应该正确");
}
@Test
@DisplayName("应该支持大数据量")
void testLargeData() {
Long largeCount = 1000000L;
java.util.List<String> largeData = new java.util.ArrayList<>();
for (int i = 0; i < 1000; i++) {
largeData.add("item" + i);
}
PageModel<String> pageModel = new PageModel<>(largeCount, largeData);
assertEquals(largeCount, pageModel.getCount(), "应该支持大总数");
assertEquals(1000, pageModel.getData().size(), "应该支持大数据列表");
}
}
@@ -0,0 +1,131 @@
package io.destiny.common.response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Result统一响应结果测试")
class ResultTest {
@Test
@DisplayName("应该创建空的成功响应")
void testSuccessWithoutData() {
Result<String> result = Result.success();
assertEquals("200", result.getCode(), "状态码应该是200");
assertEquals("Success", result.getMessage(), "消息应该是Success");
assertNull(result.getData(), "数据应该为null");
}
@Test
@DisplayName("应该创建带数据的成功响应")
void testSuccessWithData() {
String data = "test data";
Result<String> result = Result.success(data);
assertEquals("200", result.getCode(), "状态码应该是200");
assertEquals("Success", result.getMessage(), "消息应该是Success");
assertEquals(data, result.getData(), "数据应该正确");
}
@Test
@DisplayName("应该创建带自定义消息的成功响应")
void testSuccessWithMessage() {
String message = "操作成功";
String data = "test data";
Result<String> result = Result.success(message, data);
assertEquals("200", result.getCode(), "状态码应该是200");
assertEquals(message, result.getMessage(), "消息应该正确");
assertEquals(data, result.getData(), "数据应该正确");
}
@Test
@DisplayName("应该创建带状态码的错误响应")
void testErrorWithCode() {
String code = "400";
String message = "请求参数错误";
Result<String> result = Result.error(code, message);
assertEquals(code, result.getCode(), "状态码应该正确");
assertEquals(message, result.getMessage(), "消息应该正确");
assertNull(result.getData(), "数据应该为null");
}
@Test
@DisplayName("应该创建默认错误响应")
void testErrorWithMessage() {
String message = "服务器内部错误";
Result<String> result = Result.error(message);
assertEquals("500", result.getCode(), "状态码应该是500");
assertEquals(message, result.getMessage(), "消息应该正确");
assertNull(result.getData(), "数据应该为null");
}
@Test
@DisplayName("应该正确设置和获取状态码")
void testSetAndGetCode() {
Result<String> result = new Result<>();
String code = "201";
result.setCode(code);
assertEquals(code, result.getCode(), "状态码应该正确设置和获取");
}
@Test
@DisplayName("应该正确设置和获取消息")
void testSetAndGetMessage() {
Result<String> result = new Result<>();
String message = "测试消息";
result.setMessage(message);
assertEquals(message, result.getMessage(), "消息应该正确设置和获取");
}
@Test
@DisplayName("应该正确设置和获取数据")
void testSetAndGetData() {
Result<String> result = new Result<>();
String data = "测试数据";
result.setData(data);
assertEquals(data, result.getData(), "数据应该正确设置和获取");
}
@Test
@DisplayName("应该使用构造函数创建响应")
void testConstructorWithCodeAndMessage() {
String code = "200";
String message = "成功";
Result<String> result = new Result<>(code, message);
assertEquals(code, result.getCode(), "状态码应该正确");
assertEquals(message, result.getMessage(), "消息应该正确");
assertNull(result.getData(), "数据应该为null");
}
@Test
@DisplayName("应该使用全参构造函数创建响应")
void testConstructorWithAllParams() {
String code = "200";
String message = "成功";
String data = "测试数据";
Result<String> result = new Result<>(code, message, data);
assertEquals(code, result.getCode(), "状态码应该正确");
assertEquals(message, result.getMessage(), "消息应该正确");
assertEquals(data, result.getData(), "数据应该正确");
}
@Test
@DisplayName("应该支持泛型数据")
void testGenericData() {
Result<Integer> intResult = Result.success(123);
assertEquals(123, intResult.getData(), "应该支持Integer类型");
Result<Boolean> boolResult = Result.success(true);
assertEquals(true, boolResult.getData(), "应该支持Boolean类型");
}
}
@@ -0,0 +1,133 @@
package io.destiny.common.security;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class JwtAuthenticationFilterTest {
private JwtAuthenticationFilter filter;
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private WebFilterChain chain;
private ServerWebExchange exchange;
@BeforeEach
void setUp() {
filter = new JwtAuthenticationFilter(jwtTokenProvider);
}
@Test
void testFilter_ValidToken_ShouldPass() {
String token = "valid-jwt-token";
MockServerHttpRequest request = MockServerHttpRequest.get("/api/fortune/daily")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(jwtTokenProvider.validateToken(token)).thenReturn(true);
when(jwtTokenProvider.isTokenExpired(token)).thenReturn(false);
when(jwtTokenProvider.getUserIdFromToken(token)).thenReturn(1L);
when(jwtTokenProvider.getUsernameFromToken(token)).thenReturn("testuser");
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
verify(jwtTokenProvider, times(1)).validateToken(token);
verify(jwtTokenProvider, times(1)).isTokenExpired(token);
}
@Test
void testFilter_MissingToken_ShouldReturnUnauthorized() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/fortune/daily").build();
exchange = MockServerWebExchange.from(request).mutate().build();
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_InvalidToken_ShouldReturnUnauthorized() {
String token = "invalid-token";
MockServerHttpRequest request = MockServerHttpRequest.get("/api/fortune/daily")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(jwtTokenProvider.validateToken(token)).thenReturn(false);
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_ExpiredToken_ShouldReturnUnauthorized() {
String token = "expired-token";
MockServerHttpRequest request = MockServerHttpRequest.get("/api/fortune/daily")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(jwtTokenProvider.validateToken(token)).thenReturn(true);
when(jwtTokenProvider.isTokenExpired(token)).thenReturn(true);
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
assertEquals(HttpStatus.UNAUTHORIZED, exchange.getResponse().getStatusCode());
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_LoginPath_ShouldSkipAuthentication() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
verify(jwtTokenProvider, never()).validateToken(anyString());
}
@Test
void testFilter_RegisterPath_ShouldSkipAuthentication() {
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
verify(jwtTokenProvider, never()).validateToken(anyString());
}
}
@@ -0,0 +1,189 @@
package io.destiny.common.security;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class RbacAuthorizationFilterTest {
private RbacAuthorizationFilter filter;
private WebFilterChain chain;
private ServerWebExchange exchange;
@BeforeEach
void setUp() {
filter = new RbacAuthorizationFilter();
chain = mock(WebFilterChain.class);
}
@Test
void testFilter_AdminRole_WithValidPermission_ShouldPass() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header("X-User-Roles", "ADMIN")
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
assertNotEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
}
@Test
void testFilter_OperatorRole_WithInsufficientPermission_ShouldReturnForbidden() {
MockServerHttpRequest request = MockServerHttpRequest.post("/api/users")
.header("X-User-Roles", "OPERATOR")
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
assertEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_ManagerRole_WithReadPermission_ShouldPass() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users")
.header("X-User-Roles", "MANAGER")
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
assertNotEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
}
@Test
void testFilter_NoRoles_ShouldReturnForbidden() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/users").build();
exchange = MockServerWebExchange.from(request).mutate().build();
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
assertEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
verify(chain, never()).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_LoginPath_ShouldSkipAuthorization() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/auth/login").build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_RegisterPath_ShouldSkipAuthorization() {
MockServerHttpRequest request = MockServerHttpRequest.post("/api/auth/register").build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_SwaggerPath_ShouldSkipAuthorization() {
MockServerHttpRequest request = MockServerHttpRequest.get("/swagger-ui/index.html").build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_ActuatorPath_ShouldSkipAuthorization() {
MockServerHttpRequest request = MockServerHttpRequest.get("/actuator/health").build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
}
@Test
void testFilter_MultipleRoles_WithSufficientPermission_ShouldPass() {
MockServerHttpRequest request = MockServerHttpRequest.get("/api/statistics")
.header("X-User-Roles", "MANAGER,OPERATOR")
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
assertNotEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
}
@Test
void testFilter_DeleteRequest_WithAdminRole_ShouldPass() {
MockServerHttpRequest request = MockServerHttpRequest.delete("/api/users/123")
.header("X-User-Roles", "ADMIN")
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
when(chain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
verify(chain, times(1)).filter(any(ServerWebExchange.class));
assertNotEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
}
@Test
void testFilter_DeleteRequest_WithManagerRole_ShouldReturnForbidden() {
MockServerHttpRequest request = MockServerHttpRequest.delete("/api/users/123")
.header("X-User-Roles", "MANAGER")
.build();
exchange = MockServerWebExchange.from(request).mutate().build();
Mono<Void> result = filter.filter(exchange, chain);
assertNotNull(result);
assertEquals(HttpStatus.FORBIDDEN, exchange.getResponse().getStatusCode());
verify(chain, never()).filter(any(ServerWebExchange.class));
}
}
@@ -0,0 +1,79 @@
package io.destiny.common.utils;
import io.destiny.common.primitive.EmailAddress;
import io.destiny.common.primitive.PhoneNumber;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("EnumSupport枚举工具类测试")
class EnumSupportTest {
@Test
@DisplayName("应该根据代码获取枚举值")
void testValueOfCode() {
PhoneNumber.Type mobileType = EnumSupport.valueOfCode("mobile", PhoneNumber.Type.class);
assertEquals(PhoneNumber.Type.MOBILE, mobileType, "应该正确获取手机类型");
PhoneNumber.Type landlineType = EnumSupport.valueOfCode("landline", PhoneNumber.Type.class);
assertEquals(PhoneNumber.Type.LANDLINE, landlineType, "应该正确获取座机类型");
EmailAddress.Type emailType = EnumSupport.valueOfCode("email", EmailAddress.Type.class);
assertEquals(EmailAddress.Type.EMAIL, emailType, "应该正确获取邮箱类型");
}
@Test
@DisplayName("空代码应该返回null")
void testNullCode() {
PhoneNumber.Type result = EnumSupport.valueOfCode(null, PhoneNumber.Type.class);
assertNull(result, "null代码应该返回null");
PhoneNumber.Type result2 = EnumSupport.valueOfCode("", PhoneNumber.Type.class);
assertNull(result2, "空字符串代码应该返回null");
}
@Test
@DisplayName("无效代码应该抛出异常")
void testInvalidCode() {
assertThrows(IllegalArgumentException.class, () -> {
EnumSupport.valueOfCode("invalid", PhoneNumber.Type.class);
}, "无效代码应该抛出异常");
assertThrows(IllegalArgumentException.class, () -> {
EnumSupport.valueOfCode("MOBILE", PhoneNumber.Type.class);
}, "大小写不匹配的代码应该抛出异常");
}
@Test
@DisplayName("枚举应该正确返回代码")
void testGetCode() {
assertEquals("mobile", PhoneNumber.Type.MOBILE.getCode(), "手机类型代码应该正确");
assertEquals("landline", PhoneNumber.Type.LANDLINE.getCode(), "座机类型代码应该正确");
assertEquals("email", EmailAddress.Type.EMAIL.getCode(), "邮箱类型代码应该正确");
}
@Test
@DisplayName("枚举应该正确返回显示名称")
void testGetDisplayName() {
assertEquals("手机", PhoneNumber.Type.MOBILE.getDisplayName(), "手机类型显示名称应该正确");
assertEquals("座机", PhoneNumber.Type.LANDLINE.getDisplayName(), "座机类型显示名称应该正确");
assertEquals("邮箱", EmailAddress.Type.EMAIL.getDisplayName(), "邮箱类型显示名称应该正确");
}
@Test
@DisplayName("应该支持所有枚举类型")
void testAllEnumTypes() {
PhoneNumber.Type[] phoneTypes = PhoneNumber.Type.values();
for (PhoneNumber.Type type : phoneTypes) {
PhoneNumber.Type result = EnumSupport.valueOfCode(type.getCode(), PhoneNumber.Type.class);
assertEquals(type, result, type.getCode() + " 应该正确获取");
}
EmailAddress.Type[] emailTypes = EmailAddress.Type.values();
for (EmailAddress.Type type : emailTypes) {
EmailAddress.Type result = EnumSupport.valueOfCode(type.getCode(), EmailAddress.Type.class);
assertEquals(type, result, type.getCode() + " 应该正确获取");
}
}
}
@@ -0,0 +1,158 @@
package io.destiny.common.utils;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("SM4Utils国密加密工具测试")
class SM4UtilsTest {
private static final String CUSTOM_KEY = "1234567890abcdef";
@Test
@DisplayName("应该使用默认密钥加密字符串")
void testEncryptStringWithDefaultKey() {
String plainText = "Hello, World!";
String encrypted = SM4Utils.encryptString(plainText);
assertNotNull(encrypted, "加密结果不应该为null");
assertNotEquals(plainText, encrypted, "加密后的字符串应该与原文不同");
assertFalse(encrypted.contains(plainText), "加密结果不应该包含原文");
}
@Test
@DisplayName("应该使用默认密钥解密字符串")
void testDecryptStringWithDefaultKey() {
String plainText = "Hello, World!";
String encrypted = SM4Utils.encryptString(plainText);
String decrypted = SM4Utils.decryptString(encrypted);
assertEquals(plainText, decrypted, "解密后的字符串应该与原文相同");
}
@Test
@DisplayName("应该使用自定义密钥加密字节数组")
void testEncryptBytesWithCustomKey() throws Exception {
String plainText = "Test data";
byte[] plainBytes = plainText.getBytes();
byte[] keyBytes = CUSTOM_KEY.getBytes();
byte[] encrypted = SM4Utils.encrypt(plainBytes, keyBytes);
assertNotNull(encrypted, "加密结果不应该为null");
assertNotEquals(plainBytes, encrypted, "加密后的字节数组应该与原文不同");
}
@Test
@DisplayName("应该使用自定义密钥解密字节数组")
void testDecryptBytesWithCustomKey() throws Exception {
String plainText = "Test data";
byte[] plainBytes = plainText.getBytes();
byte[] keyBytes = CUSTOM_KEY.getBytes();
byte[] encrypted = SM4Utils.encrypt(plainBytes, keyBytes);
byte[] decrypted = SM4Utils.decrypt(encrypted, keyBytes);
assertArrayEquals(plainBytes, decrypted, "解密后的字节数组应该与原文相同");
}
@Test
@DisplayName("应该使用默认密钥加密字节数组")
void testEncryptBytesWithDefaultKey() throws Exception {
String plainText = "Test data";
byte[] plainBytes = plainText.getBytes();
byte[] encrypted = SM4Utils.encrypt(plainBytes);
assertNotNull(encrypted, "加密结果不应该为null");
assertNotEquals(plainBytes, encrypted, "加密后的字节数组应该与原文不同");
}
@Test
@DisplayName("应该使用默认密钥解密字节数组")
void testDecryptBytesWithDefaultKey() throws Exception {
String plainText = "Test data";
byte[] plainBytes = plainText.getBytes();
byte[] encrypted = SM4Utils.encrypt(plainBytes);
String decrypted = SM4Utils.decrypt(encrypted);
assertEquals(plainText, decrypted, "解密后的字符串应该与原文相同");
}
@Test
@DisplayName("应该正确加密空字符串")
void testEncryptEmptyString() {
String plainText = "";
String encrypted = SM4Utils.encryptString(plainText);
String decrypted = SM4Utils.decryptString(encrypted);
assertEquals(plainText, decrypted, "空字符串加密解密后应该保持不变");
}
@Test
@DisplayName("应该正确加密中文字符串")
void testEncryptChineseString() {
String plainText = "你好,世界!";
String encrypted = SM4Utils.encryptString(plainText);
String decrypted = SM4Utils.decryptString(encrypted);
assertEquals(plainText, decrypted, "中文字符串加密解密后应该保持不变");
}
@Test
@DisplayName("应该正确加密特殊字符")
void testEncryptSpecialCharacters() {
String plainText = "!@#$%^&*()_+-=[]{}|;':\",./<>?";
String encrypted = SM4Utils.encryptString(plainText);
String decrypted = SM4Utils.decryptString(encrypted);
assertEquals(plainText, decrypted, "特殊字符加密解密后应该保持不变");
}
@Test
@DisplayName("应该正确加密长字符串")
void testEncryptLongString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("a");
}
String plainText = sb.toString();
String encrypted = SM4Utils.encryptString(plainText);
String decrypted = SM4Utils.decryptString(encrypted);
assertEquals(plainText, decrypted, "长字符串加密解密后应该保持不变");
}
@Test
@DisplayName("解密失败应该抛出ApplicationException")
void testDecryptFailure() {
byte[] invalidData = new byte[] { 1, 2, 3 };
assertThrows(ApplicationException.class, () -> {
SM4Utils.decrypt(invalidData);
}, "解密失败应该抛出ApplicationException");
}
@Test
@DisplayName("多次加密同一数据应该产生不同的密文")
void testEncryptMultipleTimes() {
String plainText = "Test data";
String encrypted1 = SM4Utils.encryptString(plainText);
String encrypted2 = SM4Utils.encryptString(plainText);
assertEquals(encrypted1, encrypted2, "使用相同密钥加密相同数据应该产生相同的密文");
}
@Test
@DisplayName("应该正确处理Base64编码的密文")
void testBase64EncodedCipherText() {
String plainText = "Test data";
String encrypted = SM4Utils.encryptString(plainText);
assertTrue(encrypted.length() > 0, "Base64编码的密文不应该为空");
}
}
@@ -0,0 +1,145 @@
package io.destiny.common.utils;
import io.destiny.common.annotation.SensitiveType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("敏感数据工具类测试")
class SensitiveDataUtilsTest {
@Test
@DisplayName("应该正确脱敏手机号")
void testMaskPhone() {
String phone = "13800138000";
String masked = SensitiveDataUtils.maskPhone(phone);
assertEquals("138****8000", masked);
assertEquals(11, masked.length());
assertTrue(masked.contains("****"));
}
@Test
@DisplayName("应该正确脱敏邮箱")
void testMaskEmail() {
String email = "zhangsan@example.com";
String masked = SensitiveDataUtils.maskEmail(email);
assertEquals("zh***@example.com", masked);
assertTrue(masked.contains("@"));
assertTrue(masked.contains("***"));
}
@Test
@DisplayName("应该正确脱敏身份证号")
void testMaskIdCard() {
String idCard = "110101199001011234";
String masked = SensitiveDataUtils.maskIdCard(idCard);
assertEquals("110101********1234", masked);
assertEquals(18, masked.length());
assertTrue(masked.contains("********"));
}
@Test
@DisplayName("应该正确脱敏银行卡号")
void testMaskBankCard() {
String bankCard = "6222021234567890123";
String masked = SensitiveDataUtils.maskBankCard(bankCard);
assertEquals("622202******0123", masked);
assertEquals(16, masked.length());
assertTrue(masked.contains("******"));
}
@Test
@DisplayName("应该正确脱敏密码")
void testMaskPassword() {
String password = "MyPassword123";
String masked = SensitiveDataUtils.maskPassword(password);
assertEquals("******", masked);
assertEquals(6, masked.length());
}
@Test
@DisplayName("应该根据类型脱敏数据")
void testMaskByType() {
String phone = "13800138000";
String email = "test@example.com";
String idCard = "110101199001011234";
String maskedPhone = SensitiveDataUtils.mask(phone, SensitiveType.PHONE);
String maskedEmail = SensitiveDataUtils.mask(email, SensitiveType.EMAIL);
String maskedIdCard = SensitiveDataUtils.mask(idCard, SensitiveType.ID_CARD);
assertEquals("138****8000", maskedPhone);
assertEquals("te***@example.com", maskedEmail);
assertEquals("110101********1234", maskedIdCard);
}
@Test
@DisplayName("应该正确处理空值")
void testMaskNull() {
String maskedNull = SensitiveDataUtils.mask(null, SensitiveType.PHONE);
String maskedEmpty = SensitiveDataUtils.mask("", SensitiveType.PHONE);
assertNull(maskedNull);
assertEquals("", maskedEmpty);
}
@Test
@DisplayName("应该正确处理短手机号")
void testMaskShortPhone() {
String shortPhone = "123";
String masked = SensitiveDataUtils.maskPhone(shortPhone);
assertEquals("123", masked);
}
@Test
@DisplayName("应该正确处理无效邮箱")
void testMaskInvalidEmail() {
String invalidEmail = "invalid-email";
String masked = SensitiveDataUtils.maskEmail(invalidEmail);
assertEquals("invalid-email", masked);
}
@Test
@DisplayName("应该正确处理短身份证号")
void testMaskShortIdCard() {
String shortIdCard = "123456";
String masked = SensitiveDataUtils.maskIdCard(shortIdCard);
assertEquals("123456", masked);
}
@Test
@DisplayName("应该正确处理短银行卡号")
void testMaskShortBankCard() {
String shortBankCard = "123456789";
String masked = SensitiveDataUtils.maskBankCard(shortBankCard);
assertEquals("123456789", masked);
}
@Test
@DisplayName("应该正确脱敏自定义数据")
void testMaskCustom() {
String data = "1234567890";
String masked = SensitiveDataUtils.maskCustom(data, 2, 2);
assertEquals("12******90", masked);
}
@Test
@DisplayName("应该正确处理长度不足的自定义数据")
void testMaskCustomShortData() {
String data = "1234";
String masked = SensitiveDataUtils.maskCustom(data, 2, 2);
assertEquals("****", masked);
}
}
@@ -0,0 +1,128 @@
package io.destiny.common.utils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("SnowflakeId雪花ID生成器测试")
class SnowflakeIdTest {
@BeforeEach
void setUp() {
SnowflakeId.resetTimeCache();
}
@Test
@DisplayName("应该生成唯一ID")
void testGenerateUniqueId() {
long id1 = SnowflakeId.nextId();
long id2 = SnowflakeId.nextId();
assertNotEquals(id1, id2, "生成的ID应该是唯一的");
assertTrue(id1 > 0, "生成的ID应该大于0");
assertTrue(id2 > 0, "生成的ID应该大于0");
}
@Test
@DisplayName("应该生成递增ID")
void testGenerateIncrementalId() {
long id1 = SnowflakeId.nextId();
long id2 = SnowflakeId.nextId();
long id3 = SnowflakeId.nextId();
assertTrue(id2 > id1, "ID应该是递增的");
assertTrue(id3 > id2, "ID应该是递增的");
}
@Test
@DisplayName("应该正确解析ID")
void testParseId() {
long id = SnowflakeId.nextId();
SnowflakeId.SnowflakeIdInfo info = SnowflakeId.parseId(id);
assertNotNull(info, "解析结果不应该为null");
assertNotNull(info.getTimestamp(), "时间戳不应该为null");
assertNotNull(info.getWorkerId(), "WorkerID不应该为null");
assertNotNull(info.getSequence(), "序列号不应该为null");
assertTrue(info.getTimestamp() > 0, "时间戳应该大于0");
}
@Test
@DisplayName("应该正确获取WorkerID")
void testGetWorkerId() {
long workerId = SnowflakeId.getWorkerId();
assertTrue(workerId >= 0, "WorkerID应该大于等于0");
}
@Test
@DisplayName("应该正确配置Snowflake")
void testConfig() {
SnowflakeId.config(5, 12, System.currentTimeMillis() - 10000000);
long id = SnowflakeId.nextId();
assertTrue(id > 0, "配置后应该能正常生成ID");
}
@Test
@DisplayName("应该正确获取时间缓存命中次数")
void testGetTimeCacheHits() {
int hitsBefore = SnowflakeId.getTimeCacheHits();
SnowflakeId.nextId();
SnowflakeId.nextId();
SnowflakeId.nextId();
int hitsAfter = SnowflakeId.getTimeCacheHits();
assertTrue(hitsAfter >= hitsBefore, "时间缓存命中次数应该增加或保持不变");
}
@Test
@DisplayName("应该正确重置时间缓存")
void testResetTimeCache() {
SnowflakeId.nextId();
SnowflakeId.resetTimeCache();
int hits = SnowflakeId.getTimeCacheHits();
assertEquals(0, hits, "重置后时间缓存命中次数应该为0");
}
@Test
@DisplayName("应该正确抛出时钟回退异常")
void testClockBackwardException() {
SnowflakeId.ClockBackwardException exception =
new SnowflakeId.ClockBackwardException(100);
assertEquals(100, exception.getBackwardMs(), "回退毫秒数应该正确");
assertTrue(exception.getMessage().contains("100"), "异常消息应该包含回退毫秒数");
}
@Test
@DisplayName("应该正确生成大量ID")
void testGenerateManyIds() {
int count = 1000;
java.util.Set<Long> ids = new java.util.HashSet<>();
for (int i = 0; i < count; i++) {
long id = SnowflakeId.nextId();
assertTrue(ids.add(id), "所有ID应该是唯一的");
}
assertEquals(count, ids.size(), "应该生成指定数量的唯一ID");
}
@Test
@DisplayName("SnowflakeIdInfo应该正确返回信息")
void testSnowflakeIdInfo() {
long id = SnowflakeId.nextId();
SnowflakeId.SnowflakeIdInfo info = SnowflakeId.parseId(id);
String infoString = info.toString();
assertNotNull(infoString, "toString不应该返回null");
assertTrue(infoString.contains("timestamp="), "toString应该包含timestamp");
assertTrue(infoString.contains("workerId="), "toString应该包含workerId");
assertTrue(infoString.contains("sequence="), "toString应该包含sequence");
}
}
@@ -0,0 +1,135 @@
package io.destiny.common.utils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("XSS防护工具类测试")
class XssUtilsTest {
@Test
@DisplayName("应该清理script标签")
void testCleanScriptTag() {
String input = "Hello <script>alert('XSS')</script> World";
String cleaned = XssUtils.clean(input);
assertFalse(cleaned.contains("<script>"), "不应该包含script标签");
assertFalse(cleaned.contains("</script>"), "不应该包含script结束标签");
assertTrue(cleaned.contains("Hello"), "应该保留Hello");
assertTrue(cleaned.contains("World"), "应该保留World");
}
@Test
@DisplayName("应该清理iframe标签")
void testCleanIframeTag() {
String input = "Content <iframe src='malicious.html'></iframe> End";
String cleaned = XssUtils.clean(input);
assertFalse(cleaned.contains("<iframe>"), "不应该包含iframe标签");
assertFalse(cleaned.contains("</iframe>"), "不应该包含iframe结束标签");
assertTrue(cleaned.contains("Content"), "应该保留Content");
assertTrue(cleaned.contains("End"), "应该保留End");
}
@Test
@DisplayName("应该清理javascript:协议")
void testCleanJavascriptProtocol() {
String input = "javascript:alert('XSS')";
String cleaned = XssUtils.clean(input);
assertFalse(cleaned.contains("javascript:"), "不应该包含javascript:协议");
assertTrue(cleaned.contains("alert"), "应该保留alert");
}
@Test
@DisplayName("应该清理on事件处理器")
void testCleanOnEventHandlers() {
String input = "<img src=x onerror=alert('XSS')>";
String cleaned = XssUtils.clean(input);
assertFalse(cleaned.contains("onerror="), "不应该包含onerror事件");
assertFalse(cleaned.contains("on"), "不应该包含on事件");
}
@Test
@DisplayName("应该清理表达式绑定")
void testCleanExpressionBinding() {
String input = "<img src=x onexpression=alert('XSS')>";
String cleaned = XssUtils.clean(input);
assertFalse(cleaned.contains("onexpression="), "不应该包含onexpression绑定");
assertFalse(cleaned.contains("expression("), "不应该包含expression绑定");
}
@Test
@DisplayName("应该正确处理空值")
void testCleanNull() {
String cleaned = XssUtils.clean(null);
assertNull(cleaned, "null应该返回null");
}
@Test
@DisplayName("应该正确处理空字符串")
void testCleanEmpty() {
String cleaned = XssUtils.clean("");
assertEquals("", cleaned, "空字符串应该返回空字符串");
}
@Test
@DisplayName("应该清理恶意URL")
void testCleanMaliciousUrl() {
String url = "javascript:alert(document.cookie)";
String cleaned = XssUtils.clean(url);
assertFalse(cleaned.contains("javascript:"), "不应该包含javascript:协议");
}
@Test
@DisplayName("应该保留合法URL")
void testCleanValidUrl() {
String url = "https://example.com/path?param=value";
String cleaned = XssUtils.clean(url);
assertEquals(url, cleaned, "合法URL应该保持不变");
}
@Test
@DisplayName("应该转义HTML特殊字符")
void testEscapeHtml() {
String input = "<script>alert('XSS')</script>";
String escaped = XssUtils.cleanForHtml(input);
assertFalse(escaped.contains("<script>"), "不应该包含未转义的<");
assertTrue(escaped.contains("&lt;"), "应该包含转义的<");
assertTrue(escaped.contains("&gt;"), "应该包含转义的>");
}
@Test
@DisplayName("应该转义引号")
void testEscapeQuotes() {
String input = "\"test\"&'data'";
String escaped = XssUtils.cleanForJavaScript(input);
assertTrue(escaped.contains("\\\""), "应该包含转义的\"");
assertTrue(escaped.contains("\\'"), "应该包含转义的'");
}
@Test
@DisplayName("应该正确处理空值转义")
void testEscapeNull() {
String escaped = XssUtils.cleanForHtml(null);
assertNull(escaped, "null应该返回null");
}
@Test
@DisplayName("应该正确处理空字符串转义")
void testEscapeEmpty() {
String escaped = XssUtils.cleanForHtml("");
assertEquals("", escaped, "空字符串应该返回空字符串");
}
}