This commit is contained in:
2025-12-02 13:21:18 +08:00
parent fab8c13cb3
commit ee6dd64f98
192 changed files with 25783 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
<?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>org.xyzh</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh.common</groupId>
<artifactId>common-all</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-auth</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-core</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-dto</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-utils</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,123 @@
<?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>org.xyzh</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh.common</groupId>
<artifactId>common-auth</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- Common Core dependency -->
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-core</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-utils</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-dto</artifactId>
</dependency>
<!-- JWT Dependencies -->
<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>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除默认的logback依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<!-- 排除默认的logback依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Log4j2 日志依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- JWT 依赖 -->
<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.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<!-- 排除默认的logback依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,32 @@
package org.xyzh.common.auth.annotation;
import java.lang.annotation.*;
/**
* @description HttpLogin.java文件描述 HTTP登录注解
* @filename HttpLogin.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HttpLogin {
/**
* @description 是否必需默认为true
* @return boolean
* @author yslg
* @since 2025-11-02
*/
boolean required() default true;
/**
* @description 当token无效时的错误消息
* @return String
* @author yslg
* @since 2025-11-02
*/
String message() default "用户未登录或登录已过期";
}

View File

@@ -0,0 +1,135 @@
package org.xyzh.common.auth.annotation.resovler;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.xyzh.common.auth.annotation.HttpLogin;
import org.xyzh.common.auth.token.TokenParser;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.utils.NonUtils;
import org.xyzh.redis.service.RedisService;
import jakarta.servlet.http.HttpServletRequest;
/**
* @description HttpLoginArgumentResolver.java文件描述 HTTP登录参数解析器
* @filename HttpLoginArgumentResolver.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Component
public class HttpLoginArgumentResolver implements HandlerMethodArgumentResolver {
private final TokenParser tokenParser;
private final RedisService redisService;
/**
* 使用构造器注入依赖TokenParser接口而非具体实现
* 这样避免了与auth模块的直接依赖解决循环依赖问题
*/
public HttpLoginArgumentResolver(TokenParser tokenParser,
RedisService redisService) {
this.tokenParser = tokenParser;
this.redisService = redisService;
}
private static final String TOKEN_PREFIX = "Bearer ";
private static final String REDIS_LOGIN_PREFIX = "login:token:";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(HttpLogin.class)
&& LoginDomain.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpLogin httpLogin = parameter.getParameterAnnotation(HttpLogin.class);
if (httpLogin == null) {
return null;
}
// 从请求头中获取token
String token = extractTokenFromRequest(webRequest);
if (NonUtils.isEmpty(token)) {
if (httpLogin.required()) {
throw new IllegalArgumentException(httpLogin.message());
}
return null;
}
try {
// 验证token格式和有效性
if (!tokenParser.validateToken(token, tokenParser.getUserIdFromToken(token))) {
if (httpLogin.required()) {
throw new IllegalArgumentException(httpLogin.message());
}
return null;
}
// 从Redis中获取LoginDomain
String userId = tokenParser.getUserIdFromToken(token);
String redisKey = REDIS_LOGIN_PREFIX + userId;
LoginDomain loginDomain = (LoginDomain) redisService.get(redisKey);
if (loginDomain == null) {
if (httpLogin.required()) {
throw new IllegalArgumentException(httpLogin.message());
}
return null;
}
// 更新token信息
loginDomain.setToken(token);
return loginDomain;
} catch (Exception e) {
if (httpLogin.required()) {
throw new IllegalArgumentException(httpLogin.message());
}
return null;
}
}
/**
* @description 从请求中提取token
* @param webRequest 请求对象
* @return String token
* @author yslg
* @since 2025-11-02
*/
private String extractTokenFromRequest(NativeWebRequest webRequest) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
return null;
}
// 优先从Authorization头获取
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (NonUtils.isNotEmpty(authHeader) && authHeader.startsWith(TOKEN_PREFIX)) {
return authHeader.substring(TOKEN_PREFIX.length());
}
// 从请求参数中获取token
String token = request.getParameter("token");
if (NonUtils.isNotEmpty(token)) {
return token;
}
// 从请求头中获取token
token = request.getHeader("token");
if (NonUtils.isNotEmpty(token)) {
return token;
}
return null;
}
}

View File

@@ -0,0 +1,160 @@
package org.xyzh.common.auth.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* 认证配置属性类
* 用于配置认证相关的属性,包括白名单路径
*
* @author yslg
*/
@Component
@ConfigurationProperties(prefix = "urban-lifeline.auth")
public class AuthProperties {
/**
* 是否启用认证过滤器
* 默认启用
*/
private boolean enabled = true;
/**
* 登录接口路径
* 支持不同服务自定义登录地址
*/
private String loginPath = "/urban-lifeline/auth/login";
/**
* 登出接口路径
*/
private String logoutPath = "/urban-lifeline/auth/logout";
/**
* 验证码获取接口路径
*/
private String captchaPath = "/urban-lifeline/auth/captcha";
/**
* 刷新 Token 接口路径
*/
private String refreshPath = "/urban-lifeline/auth/refresh";
/**
* 通用白名单路径列表(非认证接口)
* 例如Swagger、静态资源、健康检查等
*/
private List<String> whitelist = new ArrayList<>();
/**
* Token 请求头名称
* 默认: Authorization
*/
private String tokenHeader = "Authorization";
/**
* Token 前缀
* 默认: Bearer
*/
private String tokenPrefix = "Bearer ";
public AuthProperties() {
// 默认通用白名单Swagger 及静态资源等
whitelist.add("/swagger-ui/**");
whitelist.add("/swagger-ui.html");
whitelist.add("/v3/api-docs/**");
whitelist.add("/webjars/**");
whitelist.add("/favicon.ico");
whitelist.add("/error");
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getLoginPath() {
return loginPath;
}
public void setLoginPath(String loginPath) {
this.loginPath = loginPath;
}
public String getLogoutPath() {
return logoutPath;
}
public void setLogoutPath(String logoutPath) {
this.logoutPath = logoutPath;
}
public String getCaptchaPath() {
return captchaPath;
}
public void setCaptchaPath(String captchaPath) {
this.captchaPath = captchaPath;
}
public String getRefreshPath() {
return refreshPath;
}
public void setRefreshPath(String refreshPath) {
this.refreshPath = refreshPath;
}
public List<String> getWhitelist() {
return whitelist;
}
public void setWhitelist(List<String> whitelist) {
this.whitelist = whitelist;
}
/**
* 认证相关接口路径集合login / logout / captcha / refresh
* 供 SecurityConfig 和 JwtAuthenticationFilter 统一放行
*/
public List<String> getAuthPaths() {
List<String> authPaths = new ArrayList<>();
if (StringUtils.hasText(loginPath)) {
authPaths.add(loginPath);
}
if (StringUtils.hasText(logoutPath)) {
authPaths.add(logoutPath);
}
if (StringUtils.hasText(captchaPath)) {
authPaths.add(captchaPath);
}
if (StringUtils.hasText(refreshPath)) {
authPaths.add(refreshPath);
}
return authPaths;
}
public String getTokenHeader() {
return tokenHeader;
}
public void setTokenHeader(String tokenHeader) {
this.tokenHeader = tokenHeader;
}
public String getTokenPrefix() {
return tokenPrefix;
}
public void setTokenPrefix(String tokenPrefix) {
this.tokenPrefix = tokenPrefix;
}
}

View File

@@ -0,0 +1,55 @@
package org.xyzh.common.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.xyzh.common.auth.filter.JwtAuthenticationFilter;
import org.xyzh.common.auth.token.TokenParser;
import org.xyzh.redis.service.RedisService;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(TokenParser tokenParser,
AuthProperties authProperties,
RedisService redisService) {
return new JwtAuthenticationFilter(tokenParser, authProperties, redisService);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
AuthProperties authProperties,
JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
http
.csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> {
// 认证接口放行login / logout / captcha / refresh
if (authProperties.getAuthPaths() != null) {
authProperties.getAuthPaths().forEach(path -> authz.requestMatchers(path).permitAll());
}
// 通用白名单放行Swagger、静态资源等
if (authProperties.getWhitelist() != null) {
authProperties.getWhitelist().forEach(path -> authz.requestMatchers(path).permitAll());
}
authz
.requestMatchers("/error", "/favicon.ico").permitAll()
.anyRequest().authenticated();
})
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

View File

@@ -0,0 +1,34 @@
package org.xyzh.common.auth.config;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.xyzh.common.auth.annotation.resovler.HttpLoginArgumentResolver;
/**
* @description WebMvcConfig.java文件描述 WebMVC配置类
* @filename WebMvcConfig.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final HandlerMethodArgumentResolver httpLoginArgumentResolver;
/**
* 使用构造器注入
* 通过接口抽象解决了循环依赖问题,不再需要@Lazy注解
*/
public WebMvcConfig(HttpLoginArgumentResolver httpLoginArgumentResolver) {
this.httpLoginArgumentResolver = httpLoginArgumentResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(httpLoginArgumentResolver);
}
}

View File

@@ -0,0 +1,43 @@
package org.xyzh.common.auth.contants;
/**
* @description 认证相关常量类
* @filename AuthContants.java
* @author yslg
* @copyright yslg
* @since 2025-11-09
*/
public class AuthContants {
/**
* 用户ID请求属性键
*/
public static final String USER_ID_ATTRIBUTE = "userId";
/**
* 用户名请求属性键
*/
public static final String USERNAME_ATTRIBUTE = "username";
/**
* Token请求属性键
*/
public static final String TOKEN_ATTRIBUTE = "token";
/**
* JWT Claims 中的用户名键
*/
public static final String CLAIMS_USERNAME_KEY = "username";
/**
* JWT Claims 中的用户ID键
*/
public static final String CLAIMS_USER_ID_KEY = "userId";
/**
* 私有构造函数,防止实例化
*/
private AuthContants() {
throw new UnsupportedOperationException("常量类不允许实例化");
}
}

View File

@@ -0,0 +1,237 @@
package org.xyzh.common.auth.filter;
import com.alibaba.fastjson2.JSON;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.xyzh.common.auth.config.AuthProperties;
import org.xyzh.common.auth.contants.AuthContants;
import org.xyzh.common.auth.token.TokenParser;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.dto.sys.TbSysPermissionDTO;
import org.xyzh.redis.service.RedisService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
/**
* @description JWT认证过滤器用于检测用户请求是否登录支持白名单配置
* @filename JwtAuthenticationFilter.java
* @author yslg
* @copyright yslg
* @since 2025-11-09
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private final TokenParser tokenParser;
private final AuthProperties authProperties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final RedisService redisService;
private static final String REDIS_LOGIN_PREFIX = "login:token:";
public JwtAuthenticationFilter(TokenParser tokenParser, AuthProperties authProperties) {
this.tokenParser = tokenParser;
this.authProperties = authProperties;
this.redisService = null; // 占位,使用另一个构造函数注入
}
public JwtAuthenticationFilter(TokenParser tokenParser, AuthProperties authProperties, RedisService redisService) {
this.tokenParser = tokenParser;
this.authProperties = authProperties;
this.redisService = redisService;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws ServletException, IOException {
// 如果认证过滤器未启用,直接放行
if (!authProperties.isEnabled()) {
filterChain.doFilter(request, response);
return;
}
String requestPath = request.getRequestURI();
if (requestPath == null) {
requestPath = "";
}
// 去掉 context-path仅保留业务路径用于白名单匹配
String contextPath = request.getContextPath();
if (contextPath != null && !contextPath.isEmpty() && requestPath.startsWith(contextPath)) {
requestPath = requestPath.substring(contextPath.length());
}
// 检查是否在白名单中
if (isWhitelisted(requestPath)) {
log.debug("请求路径在白名单中,跳过认证: {}", requestPath);
filterChain.doFilter(request, response);
return;
}
// 从请求头获取Token
String token = extractToken(request);
if (!StringUtils.hasText(token)) {
log.warn("请求缺少Token: {}", requestPath);
handleUnauthorized(response, "未提供认证令牌,请先登录");
return;
}
try {
// 验证Token
if (tokenParser.isTokenExpired(token)) {
log.warn("Token已过期: {}", requestPath);
handleUnauthorized(response, "认证令牌已过期,请重新登录");
return;
}
// 获取用户ID
String userId = tokenParser.getUserIdFromToken(token);
if (!StringUtils.hasText(userId)) {
log.warn("Token中未找到用户ID: {}", requestPath);
handleUnauthorized(response, "认证令牌无效");
return;
}
// 验证Token有效性
if (!tokenParser.validateToken(token, userId)) {
log.warn("Token验证失败: userId={}, path={}", userId, requestPath);
handleUnauthorized(response, "认证令牌验证失败");
return;
}
// 将用户信息放入请求属性中,供后续使用
Claims claims = tokenParser.getAllClaimsFromToken(token);
request.setAttribute(AuthContants.USER_ID_ATTRIBUTE, userId);
request.setAttribute(AuthContants.USERNAME_ATTRIBUTE, claims.get(AuthContants.CLAIMS_USERNAME_KEY, String.class));
request.setAttribute(AuthContants.TOKEN_ATTRIBUTE, token);
// 从Redis加载 LoginDomain并将权限装配到 Spring Security 上下文
if (redisService != null) {
Object obj = redisService.get(REDIS_LOGIN_PREFIX + userId);
if (obj instanceof LoginDomain loginDomain) {
// 组装权限码 authorities已存在
List<SimpleGrantedAuthority> permAuthorities = null;
if (loginDomain.getUserPermissions() != null) {
permAuthorities = loginDomain.getUserPermissions().stream()
.filter(Objects::nonNull)
.map(TbSysPermissionDTO::getCode)
.filter(StringUtils::hasText)
.map(SimpleGrantedAuthority::new)
.toList();
}
// 组装角色 authorities关键ROLE_ 前缀)
List<SimpleGrantedAuthority> roleAuthorities = null;
if (loginDomain.getUserRoles() != null) {
roleAuthorities = loginDomain.getUserRoles().stream()
.filter(Objects::nonNull)
.map(r -> r.getRoleId()) // 若有角色code/名称,可替换为对应字段
.filter(StringUtils::hasText)
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.toList();
}
// 合并权限与角色
List<SimpleGrantedAuthority> authorities = Stream
.concat(
permAuthorities != null ? permAuthorities.stream() : Stream.empty(),
roleAuthorities != null ? roleAuthorities.stream() : Stream.empty()
)
.toList();
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginDomain, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
log.debug("Token验证成功: userId={}, path={}", userId, requestPath);
// 继续执行过滤器链
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Token解析或验证异常: path={}, error={}", requestPath, e.getMessage(), e);
handleUnauthorized(response, "认证令牌解析失败: " + e.getMessage());
}
}
/**
* 检查路径是否在白名单中
*/
private boolean isWhitelisted(@NonNull String path) {
// 1. 先检查认证相关接口login / logout / captcha / refresh
if (authProperties.getAuthPaths() != null) {
for (String pattern : authProperties.getAuthPaths()) {
if (pattern != null && pathMatcher.match(pattern, path)) {
return true;
}
}
}
// 2. 再检查通用白名单
if (authProperties.getWhitelist() != null) {
for (String pattern : authProperties.getWhitelist()) {
if (pattern != null && pathMatcher.match(pattern, path)) {
return true;
}
}
}
return false;
}
/**
* 从请求头中提取Token
*/
private String extractToken(HttpServletRequest request) {
String header = request.getHeader(authProperties.getTokenHeader());
if (!StringUtils.hasText(header)) {
return null;
}
// 支持 Bearer 前缀
String prefix = authProperties.getTokenPrefix();
if (StringUtils.hasText(prefix) && header.startsWith(prefix)) {
return header.substring(prefix.length()).trim();
}
// 也支持直接传递Token不带前缀
return header.trim();
}
/**
* 处理未授权响应
*/
private void handleUnauthorized(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
ResultDomain<Object> result = ResultDomain.failure(HttpStatus.UNAUTHORIZED.value(), message);
String json = JSON.toJSONString(result);
response.getWriter().write(json);
response.getWriter().flush();
}
}

View File

@@ -0,0 +1,46 @@
package org.xyzh.common.auth.token;
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import org.xyzh.common.auth.utils.JwtTokenUtil;
/**
* @description JwtTokenParser.java文件描述 JWT令牌解析器实现类
* @filename JwtTokenParser.java
* @author yslg
* @copyright yslg
* @since 2025-11-07
*/
@Component
public class JwtTokenParser implements TokenParser {
private final JwtTokenUtil jwtTokenUtil;
/**
* Spring会自动注入无需@Autowired注解
*/
public JwtTokenParser(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
public String getUserIdFromToken(String token) {
return jwtTokenUtil.getUserIdFromToken(token);
}
@Override
public Claims getAllClaimsFromToken(String token) {
return jwtTokenUtil.getAllClaimsFromToken(token);
}
@Override
public boolean validateToken(String token, String userId) {
return jwtTokenUtil.validateToken(token, userId);
}
@Override
public boolean isTokenExpired(String token) {
return jwtTokenUtil.isTokenExpired(token);
}
}

View File

@@ -0,0 +1,51 @@
package org.xyzh.common.auth.token;
import io.jsonwebtoken.Claims;
/**
* @description TokenParser.java文件描述 令牌解析器接口
* @filename TokenParser.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public interface TokenParser {
/**
* @description 从令牌中获取用户ID
* @param token 令牌
* @return String 用户ID
* @author yslg
* @since 2025-11-02
*/
String getUserIdFromToken(String token);
/**
* @description 从令牌中获取所有声明信息
* @param token 令牌
* @return Claims 所有声明信息
* @author yslg
* @since 2025-11-02
*/
Claims getAllClaimsFromToken(String token);
/**
* @description 验证令牌
* @param token 令牌
* @param userId 用户ID
* @return boolean 是否有效
* @author yslg
* @since 2025-11-02
*/
boolean validateToken(String token, String userId);
/**
* @description 检查令牌是否过期
* @param token 令牌
* @return boolean 是否过期
* @author yslg
* @since 2025-11-02
*/
boolean isTokenExpired(String token);
}

View File

@@ -0,0 +1,151 @@
package org.xyzh.common.auth.utils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.dto.sys.TbSysUserDTO;
import org.xyzh.common.utils.IDUtils;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @description JwtTokenUtil.java文件描述 JWT工具类
* @filename JwtTokenUtil.java
* @author yslg
* @copyright yslg
* @since 2025-11-07
*/
@Component
public class JwtTokenUtil {
@Value("${urban-lifeline.auth.jwt-secret:schoolNewsDefaultSecretKeyForJWT2025}")
private String secret;
@Value("${urban-lifeline.auth.jwt-expiration:86400}")
private Long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* @description 生成JWT令牌
* @param loginDomain 登录域对象
* @return String JWT令牌
* @author yslg
* @since 2025-11-07
*/
public String generateToken(LoginDomain loginDomain) {
Map<String, Object> claims = new HashMap<>();
TbSysUserDTO user = loginDomain.getUser();
claims.put("userId", user.getUserId());
claims.put("username", user.getUsername());
claims.put("email", user.getEmail());
claims.put("loginType", loginDomain.getLoginType());
claims.put("ipAddress", loginDomain.getIpAddress());
return Jwts.builder()
.setClaims(claims)
.setSubject(user.getUserId())
.setId(IDUtils.generateID()) // 使用IDUtils生成JWT ID
.setIssuedAt(new Date())
.setExpiration(generateExpirationDate())
.signWith(getSigningKey())
.compact();
}
/**
* @description 从令牌中获取用户ID
* @param token JWT令牌
* @return String 用户ID
* @author yslg
* @since 2025-11-07
*/
public String getUserIdFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* @description 从令牌中获取过期时间
* @param token JWT令牌
* @return Date 过期时间
* @author yslg
* @since 2025-11-07
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* @description 从令牌中获取指定信息
* @param token JWT令牌
* @param claimsResolver 信息解析器
* @return T 指定信息
* @author yslg
* @since 2025-11-07
*/
public <T> T getClaimFromToken(String token, java.util.function.Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* @description 从令牌中获取所有信息
* @param token JWT令牌
* @return Claims 所有信息
* @author yslg
* @since 2025-11-07
*/
public Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* @description 验证令牌
* @param token JWT令牌
* @param userId 用户ID
* @return boolean 是否有效
* @author yslg
* @since 2025-11-07
*/
public boolean validateToken(String token, String userId) {
try {
final String tokenUserId = getUserIdFromToken(token);
return (userId.equals(tokenUserId) && !isTokenExpired(token));
} catch (Exception e) {
return false;
}
}
/**
* @description 检查令牌是否过期
* @param token JWT令牌
* @return boolean 是否过期
* @author yslg
* @since 2025-11-07
*/
public boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* @description 生成过期时间
* @return Date 过期时间
* @author yslg
* @since 2025-11-07
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
}

View File

@@ -0,0 +1,33 @@
# 认证配置示例文件
# 将此配置添加到各服务的 application.yml 中
urban-lifeline:
auth:
enabled: true
# 认证接口:可以按服务自定义
login-path: /urban-lifeline/auth/login
logout-path: /urban-lifeline/auth/logout
captcha-path: /urban-lifeline/auth/captcha
refresh-path: /urban-lifeline/auth/refresh
# 通用白名单(非认证接口)
whitelist:
# Swagger/OpenAPI 文档相关(建议不带 context-path
- /swagger-ui/**
- /swagger-ui.html
- /v3/api-docs/**
- /webjars/**
# 静态资源
- /favicon.ico
- /error
# 健康检查
- /actuator/health
- /actuator/info
# 其他需要放行的路径
# - /public/**
# - /api/public/**

View File

@@ -0,0 +1,29 @@
<?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>org.xyzh</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh.common</groupId>
<artifactId>common-core</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-dto</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,28 @@
package org.xyzh.common.core.constant;
/**
* @description Constants.java文件描述 常量类
* @filename Constants.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class Constants {
/**
* @description 令牌前缀
* @filename Constants.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* @description JSON_WHITELIST_STR JSON白名单
* @author yslg
* @since 2025-11-02
*/
public static final String JSON_WHITELIST_STR = "org.xyzh";
}

View File

@@ -0,0 +1,48 @@
package org.xyzh.common.core.domain;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import org.xyzh.common.dto.sys.TbSysUserDTO;
import org.xyzh.common.dto.sys.TbSysUserRoleDTO;
import org.xyzh.common.dto.sys.TbSysUserInfoDTO;
import org.xyzh.common.dto.sys.TbSysDeptDTO;
import org.xyzh.common.dto.sys.TbSysPermissionDTO;
import org.xyzh.common.dto.sys.TbSysViewDTO;
import lombok.Data;
/**
* @description 登录域
* @filename 登录域
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Data
public class LoginDomain implements Serializable{
private static final long serialVersionUID = 1L;
private TbSysUserDTO user;
private TbSysUserInfoDTO userInfo;
private List<TbSysUserRoleDTO> userRoles;
private List<TbSysDeptDTO> userDepts;
private List<TbSysPermissionDTO> userPermissions;
private List<TbSysViewDTO> userViews;
private String token;
private Date tokenExpireTime;
private String loginTime;
private String ipAddress;
private String loginType;
}

View File

@@ -0,0 +1,84 @@
package org.xyzh.common.core.domain;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
* @description 登录参数
* @filename 登录参数
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Data
public class LoginParam implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名
* @author yslg
* @since 2025-11-02
*/
private String username;
/**
* 密码
* @author yslg
* @since 2025-11-02
*/
private String password;
/**
* 邮箱
* @author yslg
* @since 2025-11-02
*/
private String email;
/**
* 手机号
* @author yslg
* @since 2025-11-02
*/
private String phone;
/**
* 微信ID
* @author yslg
* @since 2025-11-02
*/
private String wechatId;
/**
* 验证码类型
* @author yslg
* @since 2025-11-02
*/
private String captchaType;
/**
* 验证码
* @author yslg
* @since 2025-11-02
*/
private String captcha;
/**
* 验证码ID
* @author yslg
* @since 2025-11-02
*/
private String captchaId;
/**
* 登录类型
* @author yslg
* @since 2025-11-02
*/
private String loginType;
/**
* 是否记住我
* @author yslg
* @since 2025-11-02
*/
private Boolean rememberMe;
}

View File

@@ -0,0 +1,110 @@
package org.xyzh.common.core.domain;
import java.io.Serializable;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.xyzh.common.core.page.PageDomain;
import lombok.Data;
/**
* @description 结果域 通用返回结果
* @filename 结果域
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Data
public class ResultDomain<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private boolean success;
private String message;
private T data;
private List<T> dataList;
private PageDomain<T> pageDomain;
public ResultDomain(){
}
public ResultDomain(int code, String message) {
this.code = code;
this.message = message;
this.success = false;
}
public ResultDomain(int code, String message, T data) {
this.code = code;
this.message = message;
this.success = false;
this.data = data;
}
public ResultDomain(int code, String message, List<T> dataList) {
this.code = code;
this.message = message;
this.success = false;
this.dataList = dataList;
}
public ResultDomain(int code, String message, PageDomain<T> pageDomain) {
this.code = code;
this.message = message;
this.success = false;
this.pageDomain = pageDomain;
}
// 静态工厂方法 - 推荐使用(简洁、清晰)
public static <R> ResultDomain<R> success(String message) {
ResultDomain<R> result = new ResultDomain<>();
result.success = true;
result.message = message;
result.code = HttpStatus.OK.value();
return result;
}
public static <R> ResultDomain<R> success(String message, R data) {
ResultDomain<R> result = new ResultDomain<>();
result.success = true;
result.message = message;
result.data = data;
result.code = HttpStatus.OK.value();
return result;
}
public static <R> ResultDomain<R> success(String message, List<R> dataList) {
ResultDomain<R> result = new ResultDomain<>();
result.success = true;
result.message = message;
result.dataList = dataList;
result.code = HttpStatus.OK.value();
return result;
}
public static <R> ResultDomain<R> success(String message, PageDomain<R> pageDomain) {
ResultDomain<R> result = new ResultDomain<>();
result.success = true;
result.message = message;
result.pageDomain = pageDomain;
result.code = HttpStatus.OK.value();
return result;
}
public static <R> ResultDomain<R> failure(String message) {
ResultDomain<R> result = new ResultDomain<>();
result.success = false;
result.message = message;
result.code = HttpStatus.INTERNAL_SERVER_ERROR.value();
return result;
}
public static <R> ResultDomain<R> failure(int code, String message) {
ResultDomain<R> result = new ResultDomain<>();
result.success = false;
result.message = message;
result.code = code;
return result;
}
}

View File

@@ -0,0 +1,65 @@
package org.xyzh.common.core.enums;
/**
* @description 验证码类型
* @filename CaptchaType.java
* @author yslg
* @copyright yslg
* @since 2025-11-03
*/
public enum CaptchaType {
EMAIL(1, "EMAIL", "邮箱验证码"),
SMS(2, "SMS", "短信验证码"),
IMAGE(3, "IMAGE", "图形验证码");
/**
* 验证码类型编码
*/
private int code;
/**
* 验证码类型名称
*/
private String name;
/**
* 验证码类型描述
*/
private String description;
private CaptchaType(int code, String name, String description) {
this.code = code;
this.name = name;
this.description = description;
}
public int getCode() {
return code;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
/**
* @description 根据名称获取验证码类型
* @param name 验证码类型名称
* @return 验证码类型
* @author yslg
* @since 2025-11-03
*/
public static CaptchaType fromName(String name) {
for (CaptchaType type : CaptchaType.values()) {
if (type.getName().equalsIgnoreCase(name)) {
return type;
}
}
return null;
}
}

View File

@@ -0,0 +1,34 @@
package org.xyzh.common.core.page;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
@Data
public class PageDomain<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分页参数
* @author yslg
* @since 2025-11-02
*/
private PageParam pageParam;
/**
* 数据列表
* @author yslg
* @since 2025-11-02
*/
private List<T> dataList;
public PageDomain(PageParam pageParam, List<T> dataList) {
if (pageParam == null) {
throw new IllegalArgumentException("分页参数不能为空");
}
this.pageParam = pageParam;
this.dataList = dataList;
}
}

View File

@@ -0,0 +1,76 @@
package org.xyzh.common.core.page;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @description 分页参数
* @filename 分页参数
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Data
@NoArgsConstructor
public class PageParam implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 页码
* @author yslg
* @since 2025-11-02
*/
private int page;
/**
* 每页条数
* @author yslg
* @since 2025-11-02
*/
private int pageSize;
private int total;
/**
* 总页数
* @author yslg
* @since 2025-11-02
*/
private int totalPages;
private int offset;
public PageParam(int page, int pageSize) {
if (page <= 0) {
throw new IllegalArgumentException("页码必须大于0");
}
if (pageSize <= 0) {
throw new IllegalArgumentException("每页条数必须大于0");
}
this.page = page;
this.pageSize = pageSize;
this.offset = (page - 1) * pageSize;
}
public void setPage(int page){
if (page <= 0) {
throw new IllegalArgumentException("页码必须大于0");
}
this.page = page;
if (this.pageSize <= 0) {
this.pageSize = 10;
}
this.offset = (page - 1) * this.pageSize;
}
public void setPageSize(int pageSize){
if (pageSize <= 0) {
throw new IllegalArgumentException("每页条数必须大于0");
}
this.pageSize = pageSize;
if (this.page <= 0) {
this.page = 1;
}
this.offset = (this.page - 1) * this.pageSize;
}
}

View File

@@ -0,0 +1,17 @@
package org.xyzh.common.core.page;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageRequest<T> implements Serializable {
private static final long serialVersionUID = 1L;
private PageParam pageParam;
private T filter;
}

View File

@@ -0,0 +1,35 @@
<?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>org.xyzh</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh.common</groupId>
<artifactId>common-dto</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- Existing dependencies -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,44 @@
package org.xyzh.common.dto;
import java.io.Serializable;
import java.util.Date;
import com.alibaba.fastjson2.annotation.JSONField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "基础数据传输对象")
public class BaseDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "操作流水号")
private String optsn;
@Schema(description = "创建人")
private String creator;
@Schema(description = "更新人")
private String updater;
@Schema(description = "部门路径")
private String deptPath;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建时间", format = "date-time")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@Schema(description = "更新时间", format = "date-time")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@Schema(description = "删除时间", format = "date-time")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date deleteTime;
@Schema(description = "是否已删除", defaultValue = "false")
private Boolean deleted = false;
}

View File

@@ -0,0 +1,45 @@
package org.xyzh.common.dto.sys;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
/**
* @description 系统访问控制列表DTO
* @filename TbSysAclDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统访问控制列表DTO")
public class TbSysAclDTO extends BaseDTO {
@Schema(description = "权限ID")
private String aclId;
@Schema(description = "对象类型article/file/course/...")
private String objectType;
@Schema(description = "对象ID")
private String objectId;
@Schema(description = "主体类型user/dept/role")
private String principalType;
@Schema(description = "主体ID")
private String principalId;
@Schema(description = "当主体为role且限定到某部门时的部门ID支持“某部门的某角色”")
private String principalDeptId;
@Schema(description = "权限位1读 2写 4执行")
private Integer permission;
@Schema(description = "允许或显式拒绝", defaultValue = "true")
private Boolean allow = true;
@Schema(description = "是否包含子级对dept/role生效", defaultValue = "false")
private Boolean includeDescendants = false;
}

View File

@@ -0,0 +1,43 @@
package org.xyzh.common.dto.sys;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
/**
* @description ACL 策略表:定义对象类型的层级可见/可编辑规则
* @filename 系统权限策略DTO
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统访问控制策略DTO")
public class TbSysAclPolicyDTO extends BaseDTO {
@Schema(description = "策略ID")
private String policyId;
@Schema(description = "策略名称")
private String name;
@Schema(description = "对象类型article/file/course/..")
private String objectType;
@Schema(description = "编辑层级规则parent_only/parent_or_same_admin/owner_only/none")
private String editHierarchyRule;
@Schema(description = "可见层级规则 children_all/children_specified/none")
private String viewHierarchyRule;
@Schema(description = "默认权限无显式ACL时应用", defaultValue = "0")
private Integer defaultPermission=0;
@Schema(description = "默认是否允许", defaultValue = "true")
private boolean defaultAllow=true;
@Schema(description = "是否默认应用到子级", defaultValue = "true")
private boolean applyToChildren=true;
}

View File

@@ -0,0 +1,61 @@
package org.xyzh.common.dto.sys;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import com.alibaba.fastjson2.JSONObject;
/**
* @description 系统配置DTO
* @filename TbSysConfigDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统配置DTO")
public class TbSysConfigDTO extends BaseDTO {
// optsn、creator、updater、deptPath、remark、createTime、updateTime、deleteTime、deleted 继承自BaseDTO
@Schema(description = "配置ID")
private String configId;
@Schema(description = "配置键")
private String key;
@Schema(description = "配置名称")
private String name;
@Schema(description = "配置值")
private String value;
@Schema(description = "数据类型(String, Integer, Boolean, Float, Double)")
private String configType;
@Schema(description = "配置渲染类型(select, input, textarea, checkbox, radio, switch)")
private String renderType;
@Schema(description = "配置描述")
private String description;
@Schema(description = "正则表达式校验规则(JSON)")
private JSONObject re;
@Schema(description = "可选项(JSON)render_type为select、checkbox、radio时使用")
private JSONObject options;
@Schema(description = "配置组")
private String group;
@Schema(description = "模块id")
private String moduleId;
@Schema(description = "配置顺序")
private Integer orderNum;
@Schema(description = "配置状态 0:启用 1:禁用", defaultValue = "0")
private Integer status = 0;
}

View File

@@ -0,0 +1,30 @@
package org.xyzh.common.dto.sys;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
/**
* @description 系统部门DTO
* @filename TbSysDeptDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统部门DTO")
public class TbSysDeptDTO extends BaseDTO {
@Schema(description = "部门ID")
private String deptId;
@Schema(description = "部门名称")
private String name;
@Schema(description = "父级部门ID")
private String parentId;
@Schema(description = "部门描述")
private String description;
}

View File

@@ -0,0 +1,25 @@
package org.xyzh.common.dto.sys;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
/**
* @description 系统部门角色关系DTO
* @filename TbSysDeptRoleDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统部门角色关系DTO")
public class TbSysDeptRoleDTO extends BaseDTO {
@Schema(description = "部门ID")
private String deptId;
@Schema(description = "角色ID")
private String roleId;
}

View File

@@ -0,0 +1,29 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统模块DTO
* @filename TbSysModuleDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统模块DTO")
public class TbSysModuleDTO extends BaseDTO {
@Schema(description = "模块ID")
private String moduleId;
@Schema(description = "模块名称")
private String name;
@Schema(description = "模块描述")
private String description;
}

View File

@@ -0,0 +1,37 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统权限DTO
* @filename TbSysPermissionDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统权限DTO")
public class TbSysPermissionDTO extends BaseDTO {
@Schema(description = "权限ID")
private String permissionId;
@Schema(description = "权限名称")
private String name;
@Schema(description = "权限代码")
private String code;
@Schema(description = "权限描述")
private String description;
@Schema(description = "模块ID")
private String moduleId;
@Schema(description = "状态")
private String status;
}

View File

@@ -0,0 +1,37 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统角色DTO
* @filename TbSysRoleDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统角色DTO")
public class TbSysRoleDTO extends BaseDTO {
@Schema(description = "角色ID")
private String roleId;
@Schema(description = "角色名称")
private String name;
@Schema(description = "角色描述")
private String description;
@Schema(description = "角色作用域 global 全局角色, dept 部门角色")
private String scope;
@Schema(description = "所属部门ID")
private String ownerDeptId;
@Schema(description = "角色状态 true 有效, false 无效")
private boolean status;
}

View File

@@ -0,0 +1,25 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统角色权限关系DTO
* @filename TbSysRolePermissionDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统角色权限关系DTO")
public class TbSysRolePermissionDTO extends BaseDTO {
@Schema(description = "角色ID")
private String roleId;
@Schema(description = "权限ID")
private String permissionId;
}

View File

@@ -0,0 +1,44 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统用户DTO
* @filename TbSysUserDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统用户DTO")
public class TbSysUserDTO extends BaseDTO {
@Schema(description = "用户ID")
private String userId;
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机")
private String phone;
@Schema(description = "微信ID")
private String wechatId;
@Schema(description = "用户状态")
private String status;
@Schema(description = "用户类型")
private String userType;
}

View File

@@ -0,0 +1,46 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统用户信息DTO
* @filename TbSysUserInfoDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统用户信息DTO")
public class TbSysUserInfoDTO extends BaseDTO {
@Schema(description = "用户ID")
private String userId;
@Schema(description = "头像")
private String avatar;
@Schema(description = "性别")
private Integer gender;
@Schema(description = "")
private String familyName;
@Schema(description = "")
private String givenName;
@Schema(description = "全名")
private String fullName;
@Schema(description = "等级")
private Integer level;
@Schema(description = "身份证号")
private String idCard;
@Schema(description = "地址")
private String address;
}

View File

@@ -0,0 +1,26 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统用户角色关系DTO
* @filename TbSysUserRoleDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统用户角色关系DTO")
public class TbSysUserRoleDTO extends BaseDTO {
@Schema(description = "用户ID")
private String userId;
@Schema(description = "角色ID")
private String roleId;
}

View File

@@ -0,0 +1,50 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统视图DTO
* @filename TbSysViewDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统视图DTO")
public class TbSysViewDTO extends BaseDTO {
@Schema(description = "视图ID")
private String viewId;
@Schema(description = "视图名称")
private String name;
@Schema(description = "父视图ID")
private String parentId;
@Schema(description = "URL")
private String url;
@Schema(description = "组件")
private String component;
@Schema(description = "图标")
private String icon;
@Schema(description = "类型")
private Integer type;
@Schema(description = "布局")
private String layout;
@Schema(description = "排序")
private Integer orderNum;
@Schema(description = "描述")
private String description;
}

View File

@@ -0,0 +1,26 @@
package org.xyzh.common.dto.sys;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.xyzh.common.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @description 系统视图权限关系DTO
* @filename TbSysViewPermissionDTO.java
* @author yslg
* @copyright yslg
* @since 2025-11-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "系统视图权限关系DTO")
public class TbSysViewPermissionDTO extends BaseDTO {
@Schema(description = "视图ID")
private String viewId;
@Schema(description = "权限ID")
private String permissionId;
}

View File

@@ -0,0 +1,52 @@
package org.xyzh.common.vo;
import java.io.Serializable;
import java.util.Date;
import com.alibaba.fastjson2.annotation.JSONField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* @description 基础视图对象
* @filename BaseVO.java
* @author yslg
* @copyright yslg
* @since 2025-11-05
*/
@Data
@Schema(description = "基础视图对象")
public class BaseVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "操作流水号")
private String optsn;
@Schema(description = "创建人")
private String creator;
@Schema(description = "更新人")
private String updater;
@Schema(description = "部门路径")
private String deptPath;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建时间", format = "date-time")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@Schema(description = "更新时间", format = "date-time")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@Schema(description = "删除时间", format = "date-time")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date deleteTime;
@Schema(description = "是否已删除", defaultValue = "false")
private Boolean deleted = false;
}

View File

@@ -0,0 +1,67 @@
<?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>org.xyzh</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh.common</groupId>
<artifactId>common-redis</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring Boot Redis Starter -->
<!-- Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- 排除默认的logback依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<!-- FastJSON2 for JSON serialization -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- Common Core dependency -->
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-core</artifactId>
</dependency>
<!-- Spring Context for @Component annotation -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- Spring Boot Auto Configuration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,56 @@
package org.xyzh.redis.config;
import org.xyzh.common.core.constant.Constants;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
/**
* @description FastJson2JsonRedisSerializer.java文件描述
* @filename FastJson2JsonRedisSerializer.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
private Class<T> clazz;
public FastJson2JsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
}
}

View File

@@ -0,0 +1,44 @@
package org.xyzh.redis.config;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @description RedisConfig.java文件描述 Redis配置
* @filename RedisConfig.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -0,0 +1,384 @@
package org.xyzh.redis.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.RedisCallback;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Component;
/**
* @description RedisService.java Redis工具服务类封装常用Redis操作
* @filename RedisService.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* @description 设置key-value
* @param key String 键
* @param value Object 值
* @author yslg
* @since 2025-11-02
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* @description 设置key-value并指定过期时间
* @param key String 键
* @param value Object 值
* @param timeout long 过期时间
* @param unit TimeUnit 时间单位
* @author yslg
* @since 2025-11-02
*/
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* @description 获取key对应的value
* @param key String 键
* @return Object 值
* @author yslg
* @since 2025-11-02
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* @description 删除key
* @param key String 键
* @author yslg
* @since 2025-11-02
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* @description 批量删除key
* @param keys Collection<String> 键集合
* @author yslg
* @since 2025-11-02
*/
public void delete(Collection<String> keys) {
redisTemplate.delete(keys);
}
/**
* @description 判断key是否存在
* @param key String 键
* @return boolean 是否存在
* @author yslg
* @since 2025-11-02
*/
public boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* @description 设置key过期时间
* @param key String 键
* @param timeout long 过期秒数
* @author yslg
* @since 2025-11-02
*/
public void setExpire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* @description 设置key过期时间自定义单位
* @param key String 键
* @param timeout long 过期时间
* @param unit TimeUnit 时间单位
* @author yslg
* @since 2025-11-02
*/
public void setExpire(String key, long timeout, TimeUnit unit) {
redisTemplate.expire(key, timeout, unit);
}
/**
* @description 获取key剩余过期时间
* @param key String 键
* @return long 剩余秒数
* @author yslg
* @since 2025-11-02
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* @description 原子递增
* @param key String 键
* @param delta long 增量
* @return long 递增后值
* @author yslg
* @since 2025-11-02
*/
public long incr(String key, long delta) {
Long result = redisTemplate.opsForValue().increment(key, delta);
return result != null ? result : 0L;
}
/**
* @description 原子递减
* @param key String 键
* @param delta long 减量
* @return long 递减后值
* @author yslg
* @since 2025-11-02
*/
public long decr(String key, long delta) {
Long result = redisTemplate.opsForValue().increment(key, -delta);
return result != null ? result : 0L;
}
/**
* @description Hash操作-put
* @param key String 键
* @param hashKey String 哈希键
* @param value Object 值
* @author yslg
* @since 2025-11-02
*/
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
/**
* @description Hash操作-get
* @param key String 键
* @param hashKey String 哈希键
* @return Object 值
* @author yslg
* @since 2025-11-02
*/
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
/**
* @description Hash操作-获取所有
* @param key String 键
* @return Map<Object, Object> 哈希所有键值对
* @author yslg
* @since 2025-11-02
*/
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* @description Hash操作-删除
* @param key String 键
* @param hashKeys String[] 哈希键数组
* @author yslg
* @since 2025-11-02
*/
public void hDelete(String key, String... hashKeys) {
redisTemplate.opsForHash().delete(key, (Object[]) hashKeys);
}
/**
* @description List操作-左入队
* @param key String 键
* @param value Object 值
* @author yslg
* @since 2025-11-02
*/
public void lPush(String key, Object value) {
redisTemplate.opsForList().leftPush(key, value);
}
/**
* @description List操作-右入队
* @param key String 键
* @param value Object 值
* @author yslg
* @since 2025-11-02
*/
public void rPush(String key, Object value) {
redisTemplate.opsForList().rightPush(key, value);
}
/**
* @description List操作-左出队
* @param key String 键
* @return Object 出队值
* @author yslg
* @since 2025-11-02
*/
public Object lPop(String key) {
return redisTemplate.opsForList().leftPop(key);
}
/**
* @description List操作-右出队
* @param key String 键
* @return Object 出队值
* @author yslg
* @since 2025-11-02
*/
public Object rPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
/**
* @description List操作-获取区间元素
* @param key String 键
* @param start long 起始索引
* @param end long 结束索引
* @return List<Object> 元素列表
* @author yslg
* @since 2025-11-02
*/
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* @description Set操作-添加元素
* @param key String 键
* @param values Object[] 元素数组
* @author yslg
* @since 2025-11-02
*/
public void sAdd(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
/**
* @description Set操作-移除元素
* @param key String 键
* @param values Object[] 元素数组
* @author yslg
* @since 2025-11-02
*/
public void sRemove(String key, Object... values) {
redisTemplate.opsForSet().remove(key, values);
}
/**
* @description Set操作-获取所有元素
* @param key String 键
* @return Set<Object> 元素集合
* @author yslg
* @since 2025-11-02
*/
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* @description ZSet操作-添加元素
* @param key String 键
* @param value Object 元素
* @param score double 分数
* @author yslg
* @since 2025-11-02
*/
public void zAdd(String key, Object value, double score) {
redisTemplate.opsForZSet().add(key, value, score);
}
/**
* @description ZSet操作-移除元素
* @param key String 键
* @param values Object[] 元素数组
* @author yslg
* @since 2025-11-02
*/
public void zRemove(String key, Object... values) {
redisTemplate.opsForZSet().remove(key, values);
}
/**
* @description ZSet操作-按分数区间获取元素
* @param key String 键
* @param min double 最小分数
* @param max double 最大分数
* @return Set<Object> 元素集合
* @author yslg
* @since 2025-11-02
*/
public Set<Object> zRangeByScore(String key, double min, double max) {
return redisTemplate.opsForZSet().rangeByScore(key, min, max);
}
/**
* @description ZSet操作-获取全部元素
* @param key String 键
* @param start long 起始索引
* @param end long 结束索引
* @return Set<Object> 元素集合
* @author yslg
* @since 2025-11-02
*/
public Set<Object> zRange(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
/**
* @description 发布消息Pub/Sub
* @param channel String 通道
* @param message Object 消息内容
* @author yslg
* @since 2025-11-02
*/
public void publish(String channel, Object message) {
redisTemplate.convertAndSend(channel, message);
}
/**
* @description 执行Redis原生命令
* @param action RedisCallback<?> 回调命令
* @return Object 执行结果
* @author yslg
* @since 2025-11-02
*/
public Object execute(RedisCallback<?> action) {
return redisTemplate.execute(action);
}
/**
* @description 获取所有key慎用
* @param pattern String 匹配模式
* @return Set<String> key集合
* @author yslg
* @since 2025-11-02
*/
public Set<String> keys(String pattern) {
Set<String> keys = redisTemplate.keys(pattern);
Set<String> stringKeys = new HashSet<>();
if (keys != null) {
for (String key : keys) {
stringKeys.add(key);
}
}
return stringKeys;
}
/**
* @description 清空当前数据库(慎用)
* @author yslg
* @since 2025-11-02
*/
public void flushDb() {
redisTemplate.execute((RedisCallback<Void>) connection -> { connection.flushDb(); return null; });
}
}

View File

@@ -0,0 +1,34 @@
<?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>org.xyzh</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh.common</groupId>
<artifactId>common-utils</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- Apache POI for Excel -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,23 @@
package org.xyzh.common.utils;
import java.util.UUID;
/**
* @description IDUtils.java文件描述
* @filename IDUtils.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class IDUtils {
/**
* @description 生成UUID
* @return UUID
* @author yslg
* @since 2025-11-02
*/
public static String generateID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}

View File

@@ -0,0 +1,603 @@
package org.xyzh.common.utils;
import java.lang.reflect.Array;
import java.util.*;
import java.util.function.Predicate;
/**
* @description NonUtils.java文件描述 空值判断工具类
* @filename NonUtils.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class NonUtils {
private NonUtils() {
throw new UnsupportedOperationException("工具类不能被实例化");
}
// ======================== 基础null判断 ========================
/**
* 判断对象是否为null
*
* @param obj 待判断的对象
* @return true-对象为nullfalse-对象不为null
*/
public static boolean isNull(Object obj) {
return obj == null;
}
/**
* 判断对象是否不为null
*
* @param obj 待判断的对象
* @return true-对象不为nullfalse-对象为null
*/
public static boolean isNotNull(Object obj) {
return obj != null;
}
/**
* 判断多个对象是否都为null
*
* @param objects 待判断的对象数组
* @return true-所有对象都为nullfalse-至少有一个对象不为null
*/
public static boolean isAllNull(Object... objects) {
if (objects == null || objects.length == 0) {
return true;
}
for (Object obj : objects) {
if (isNotNull(obj)) {
return false;
}
}
return true;
}
/**
* 判断多个对象是否都不为null
*
* @param objects 待判断的对象数组
* @return true-所有对象都不为nullfalse-至少有一个对象为null
*/
public static boolean isAllNotNull(Object... objects) {
if (objects == null || objects.length == 0) {
return false;
}
for (Object obj : objects) {
if (isNull(obj)) {
return false;
}
}
return true;
}
/**
* 判断多个对象中是否存在null
*
* @param objects 待判断的对象数组
* @return true-存在null对象false-不存在null对象
*/
public static boolean hasNull(Object... objects) {
if (objects == null || objects.length == 0) {
return true;
}
for (Object obj : objects) {
if (isNull(obj)) {
return true;
}
}
return false;
}
// ======================== 空值判断包含null、空字符串、空集合等 ========================
/**
* 判断对象是否为空
* - null -> true
* - "" -> true
* - " " -> true (仅包含空白字符)
* - 空集合 -> true
* - 空数组 -> true
*
* @param obj 待判断的对象
* @return true-对象为空false-对象不为空
*/
public static boolean isEmpty(Object obj) {
if (isNull(obj)) {
return true;
}
// 字符串判断
if (obj instanceof CharSequence) {
return ((CharSequence) obj).length() == 0 || obj.toString().trim().isEmpty();
}
// 集合判断
if (obj instanceof Collection) {
return ((Collection<?>) obj).isEmpty();
}
// Map判断
if (obj instanceof Map) {
return ((Map<?, ?>) obj).isEmpty();
}
// 数组判断
if (obj.getClass().isArray()) {
return Array.getLength(obj) == 0;
}
// Optional判断
if (obj instanceof Optional) {
return !((Optional<?>) obj).isPresent();
}
return false;
}
/**
* 判断对象是否不为空
*
* @param obj 待判断的对象
* @return true-对象不为空false-对象为空
*/
public static boolean isNotEmpty(Object obj) {
return !isEmpty(obj);
}
/**
* 判断多个对象是否都为空
*
* @param objects 待判断的对象数组
* @return true-所有对象都为空false-至少有一个对象不为空
*/
public static boolean isAllEmpty(Object... objects) {
if (objects == null || objects.length == 0) {
return true;
}
for (Object obj : objects) {
if (isNotEmpty(obj)) {
return false;
}
}
return true;
}
/**
* 判断多个对象是否都不为空
*
* @param objects 待判断的对象数组
* @return true-所有对象都不为空false-至少有一个对象为空
*/
public static boolean isAllNotEmpty(Object... objects) {
if (objects == null || objects.length == 0) {
return false;
}
for (Object obj : objects) {
if (isEmpty(obj)) {
return false;
}
}
return true;
}
/**
* 判断多个对象中是否存在空值
*
* @param objects 待判断的对象数组
* @return true-存在空值false-不存在空值
*/
public static boolean hasEmpty(Object... objects) {
if (objects == null || objects.length == 0) {
return true;
}
for (Object obj : objects) {
if (isEmpty(obj)) {
return true;
}
}
return false;
}
// ======================== 深度递归判断 ========================
/**
* 深度判断对象是否为空(递归检查)
* 对于集合、数组等容器类型,会递归检查其内部元素
*
* @param obj 待判断的对象
* @return true-对象为空包括递归检查false-对象不为空
*/
public static boolean isDeepEmpty(Object obj) {
return isDeepEmpty(obj, new HashSet<>());
}
/**
* 深度判断对象是否为空(递归检查,防止循环引用)
*
* @param obj 待判断的对象
* @param visited 已访问对象集合,用于防止循环引用
* @return true-对象为空包括递归检查false-对象不为空
*/
private static boolean isDeepEmpty(Object obj, Set<Object> visited) {
if (isEmpty(obj)) {
return true;
}
// 防止循环引用
if (visited.contains(obj)) {
return false;
}
visited.add(obj);
try {
// 集合类型递归检查
if (obj instanceof Collection) {
Collection<?> collection = (Collection<?>) obj;
for (Object item : collection) {
if (!isDeepEmpty(item, visited)) {
return false;
}
}
return true;
}
// Map类型递归检查
if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!isDeepEmpty(entry.getKey(), visited) || !isDeepEmpty(entry.getValue(), visited)) {
return false;
}
}
return true;
}
// 数组类型递归检查
if (obj.getClass().isArray()) {
int length = Array.getLength(obj);
for (int i = 0; i < length; i++) {
if (!isDeepEmpty(Array.get(obj, i), visited)) {
return false;
}
}
return true;
}
// Optional类型递归检查
if (obj instanceof Optional) {
Optional<?> optional = (Optional<?>) obj;
return !optional.isPresent() || isDeepEmpty(optional.get(), visited);
}
// 其他类型认为不为空
return false;
} finally {
visited.remove(obj);
}
}
/**
* 深度判断对象是否不为空(递归检查)
*
* @param obj 待判断的对象
* @return true-对象不为空包括递归检查false-对象为空
*/
public static boolean isDeepNotEmpty(Object obj) {
return !isDeepEmpty(obj);
}
// ======================== 集合专用方法 ========================
/**
* 判断集合是否为空或null
*
* @param collection 待判断的集合
* @return true-集合为null或空false-集合不为空
*/
public static boolean isEmptyCollection(Collection<?> collection) {
return collection == null || collection.isEmpty();
}
/**
* 判断集合是否不为空且不为null
*
* @param collection 待判断的集合
* @return true-集合不为null且不为空false-集合为null或空
*/
public static boolean isNotEmptyCollection(Collection<?> collection) {
return collection != null && !collection.isEmpty();
}
/**
* 判断集合是否包含有效元素非null且非空的元素
*
* @param collection 待判断的集合
* @return true-集合包含有效元素false-集合为空或只包含null/空元素
*/
public static boolean hasValidElements(Collection<?> collection) {
if (isEmptyCollection(collection)) {
return false;
}
for (Object item : collection) {
if (isNotEmpty(item)) {
return true;
}
}
return false;
}
/**
* 判断集合是否所有元素都有效非null且非空
*
* @param collection 待判断的集合
* @return true-集合所有元素都有效false-集合为空或包含null/空元素
*/
public static boolean allValidElements(Collection<?> collection) {
if (isEmptyCollection(collection)) {
return false;
}
for (Object item : collection) {
if (isEmpty(item)) {
return false;
}
}
return true;
}
/**
* 过滤集合中的空元素,返回新集合
*
* @param collection 原集合
* @param <T> 集合元素类型
* @return 过滤后的新集合
*/
public static <T> List<T> filterEmpty(Collection<T> collection) {
if (isEmptyCollection(collection)) {
return new ArrayList<>();
}
List<T> result = new ArrayList<>();
for (T item : collection) {
if (isNotEmpty(item)) {
result.add(item);
}
}
return result;
}
// ======================== Map专用方法 ========================
/**
* 判断Map是否为空或null
*
* @param map 待判断的Map
* @return true-Map为null或空false-Map不为空
*/
public static boolean isEmptyMap(Map<?, ?> map) {
return map == null || map.isEmpty();
}
/**
* 判断Map是否不为空且不为null
*
* @param map 待判断的Map
* @return true-Map不为null且不为空false-Map为null或空
*/
public static boolean isNotEmptyMap(Map<?, ?> map) {
return map != null && !map.isEmpty();
}
// ======================== 数组专用方法 ========================
/**
* 判断数组是否为空或null
*
* @param array 待判断的数组
* @return true-数组为null或空false-数组不为空
*/
public static boolean isEmptyArray(Object array) {
return array == null || !array.getClass().isArray() || Array.getLength(array) == 0;
}
/**
* 判断数组是否不为空且不为null
*
* @param array 待判断的数组
* @return true-数组不为null且不为空false-数组为null或空
*/
public static boolean isNotEmptyArray(Object array) {
return array != null && array.getClass().isArray() && Array.getLength(array) > 0;
}
// ======================== 字符串专用方法 ========================
/**
* 判断字符串是否为空或null包括空白字符串
*
* @param str 待判断的字符串
* @return true-字符串为null、空或只包含空白字符false-字符串有有效内容
*/
public static boolean isEmptyString(String str) {
return str == null || str.trim().isEmpty();
}
/**
* 判断字符串是否不为空且不为null
*
* @param str 待判断的字符串
* @return true-字符串不为null且有有效内容false-字符串为null、空或只包含空白字符
*/
public static boolean isNotEmptyString(String str) {
return str != null && !str.trim().isEmpty();
}
// ======================== 条件判断方法 ========================
/**
* 如果对象为空则返回默认值
*
* @param obj 待判断的对象
* @param defaultValue 默认值
* @param <T> 对象类型
* @return 如果obj为空则返回defaultValue否则返回obj
*/
@SuppressWarnings("unchecked")
public static <T> T defaultIfEmpty(T obj, T defaultValue) {
return isEmpty(obj) ? defaultValue : obj;
}
/**
* 如果对象为null则返回默认值
*
* @param obj 待判断的对象
* @param defaultValue 默认值
* @param <T> 对象类型
* @return 如果obj为null则返回defaultValue否则返回obj
*/
public static <T> T defaultIfNull(T obj, T defaultValue) {
return isNull(obj) ? defaultValue : obj;
}
/**
* 获取第一个非空的对象
*
* @param objects 对象数组
* @param <T> 对象类型
* @return 第一个非空的对象如果都为空则返回null
*/
@SafeVarargs
public static <T> T firstNotEmpty(T... objects) {
if (objects == null || objects.length == 0) {
return null;
}
for (T obj : objects) {
if (isNotEmpty(obj)) {
return obj;
}
}
return null;
}
/**
* 获取第一个非null的对象
*
* @param objects 对象数组
* @param <T> 对象类型
* @return 第一个非null的对象如果都为null则返回null
*/
@SafeVarargs
public static <T> T firstNotNull(T... objects) {
if (objects == null || objects.length == 0) {
return null;
}
for (T obj : objects) {
if (isNotNull(obj)) {
return obj;
}
}
return null;
}
// ======================== 统计方法 ========================
/**
* 统计数组中非空元素的个数
*
* @param objects 对象数组
* @return 非空元素个数
*/
public static int countNotEmpty(Object... objects) {
if (objects == null || objects.length == 0) {
return 0;
}
int count = 0;
for (Object obj : objects) {
if (isNotEmpty(obj)) {
count++;
}
}
return count;
}
/**
* 统计数组中非null元素的个数
*
* @param objects 对象数组
* @return 非null元素个数
*/
public static int countNotNull(Object... objects) {
if (objects == null || objects.length == 0) {
return 0;
}
int count = 0;
for (Object obj : objects) {
if (isNotNull(obj)) {
count++;
}
}
return count;
}
// ======================== 断言方法 ========================
/**
* 断言对象不为null如果为null则抛出异常
*
* @param obj 待断言的对象
* @param message 异常消息
* @throws IllegalArgumentException 如果对象为null
*/
public static void requireNotNull(Object obj, String message) {
if (isNull(obj)) {
throw new IllegalArgumentException(message != null ? message : "对象不能为null");
}
}
/**
* 断言对象不为空,如果为空则抛出异常
*
* @param obj 待断言的对象
* @param message 异常消息
* @throws IllegalArgumentException 如果对象为空
*/
public static void requireNotEmpty(Object obj, String message) {
if (isEmpty(obj)) {
throw new IllegalArgumentException(message != null ? message : "对象不能为空");
}
}
// ======================== 类型检查方法 ========================
/**
* 检查对象是否为指定类型且不为null
*
* @param obj 待检查的对象
* @param clazz 目标类型
* @param <T> 目标类型
* @return true-对象不为null且为指定类型false-否则
*/
public static <T> boolean isInstanceAndNotNull(Object obj, Class<T> clazz) {
return isNotNull(obj) && clazz.isInstance(obj);
}
/**
* 安全的类型转换如果对象为null或不是目标类型则返回null
*
* @param obj 待转换的对象
* @param clazz 目标类型
* @param <T> 目标类型
* @return 转换后的对象如果转换失败则返回null
*/
@SuppressWarnings("unchecked")
public static <T> T safeCast(Object obj, Class<T> clazz) {
if (isInstanceAndNotNull(obj, clazz)) {
return (T) obj;
}
return null;
}
}

View File

@@ -0,0 +1,404 @@
package org.xyzh.common.utils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.util.Enumeration;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
/**
* @description ServletUtils.java文件描述Servlet相关常用工具方法
* @filename ServletUtils.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ServletUtils {
/**
* @description 获取请求的真实IP地址
* @param request HTTP请求对象
* @return 客户端真实IP地址
* @author yslg
* @since 2025-11-02
*/
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
// 多级代理时取第一个
int idx = ip.indexOf(',');
if (idx > -1) {
ip = ip.substring(0, idx);
}
return ip.trim();
}
ip = request.getHeader("Proxy-Client-IP");
if (ip != null && ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
ip = request.getHeader("WL-Proxy-Client-IP");
if (ip != null && ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
ip = request.getRemoteAddr();
return ip;
}
/**
* @description 向响应写出JSON字符串
* @param response HTTP响应对象
* @param json 要写出的JSON字符串
* @return void
* @author yslg
* @since 2025-11-02
*/
public static void writeJson(HttpServletResponse response, String json) throws IOException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write(json);
writer.flush();
writer.close();
}
/**
* @description 获取请求参数(支持默认值)
* @param request HTTP请求对象
* @param name 参数名
* @param defaultValue 默认值
* @return 参数值或默认值
* @author yslg
* @since 2025-11-02
*/
public static String getParameter(HttpServletRequest request, String name, String defaultValue) {
String value = request.getParameter(name);
return value != null ? value : defaultValue;
}
/**
* @description 判断请求是否为Ajax
* @param request HTTP请求对象
* @return 是否为Ajax请求
* @author yslg
* @since 2025-11-02
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String header = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equalsIgnoreCase(header);
}
/**
* @description 获取请求体内容
* @param request HTTP请求对象
* @return 请求体内容
* @author yslg
* @since 2025-11-02
*/
public static String getRequestBody(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
/**
* @description 重定向到指定URL
* @param response HTTP响应对象
* @param url 目标URL
* @return void
* @author yslg
* @since 2025-11-02
*/
public static void redirect(HttpServletResponse response, String url) throws IOException {
response.sendRedirect(url);
}
/**
* @description 获取完整请求URL
* @param request HTTP请求对象
* @return 完整URL
* @author yslg
* @since 2025-11-02
*/
public static String getFullUrl(HttpServletRequest request) {
StringBuilder url = new StringBuilder();
url.append(request.getScheme()).append("://");
url.append(request.getServerName());
int port = request.getServerPort();
if (("http".equals(request.getScheme()) && port != 80) ||
("https".equals(request.getScheme()) && port != 443)) {
url.append(":").append(port);
}
url.append(request.getRequestURI());
String queryString = request.getQueryString();
if (queryString != null) {
url.append("?").append(queryString);
}
return url.toString();
}
/**
* @description 判断请求是否为GET方法
* @param request HTTP请求对象
* @return 是否为GET请求
* @author yslg
* @since 2025-11-02
*/
public static boolean isGet(HttpServletRequest request) {
return "GET".equalsIgnoreCase(request.getMethod());
}
/**
* @description 判断请求是否为POST方法
* @param request HTTP请求对象
* @return 是否为POST请求
* @author yslg
* @since 2025-11-02
*/
public static boolean isPost(HttpServletRequest request) {
return "POST".equalsIgnoreCase(request.getMethod());
}
/**
* @description 判断请求是否为PUT方法
* @param request HTTP请求对象
* @return 是否为PUT请求
* @author yslg
* @since 2025-11-02
*/
public static boolean isPut(HttpServletRequest request) {
return "PUT".equalsIgnoreCase(request.getMethod());
}
/**
* @description 判断请求是否为DELETE方法
* @param request HTTP请求对象
* @return 是否为DELETE请求
* @author yslg
* @since 2025-11-02
*/
public static boolean isDelete(HttpServletRequest request) {
return "DELETE".equalsIgnoreCase(request.getMethod());
}
/**
* @description 获取所有请求参数
* @param request HTTP请求对象
* @return 参数Map
* @author yslg
* @since 2025-11-02
*/
public static Map<String, String> getParameterMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
String paramValue = request.getParameter(paramName);
paramMap.put(paramName, paramValue);
}
return paramMap;
}
/**
* @description 获取请求头信息
* @param request HTTP请求对象
* @return 头信息Map
* @author yslg
* @since 2025-11-02
*/
public static Map<String, String> getHeaderMap(HttpServletRequest request) {
Map<String, String> headerMap = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
headerMap.put(headerName, headerValue);
}
return headerMap;
}
/**
* @description 获取Cookie值
* @param request HTTP请求对象
* @param name Cookie名
* @return Cookie值
* @author yslg
* @since 2025-11-02
*/
public static String getCookieValue(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
/**
* @description 设置Cookie
* @param response HTTP响应对象
* @param name Cookie名
* @param value Cookie值
* @param maxAge 过期时间(秒)
* @return void
* @author yslg
* @since 2025-11-02
*/
public static void setCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setMaxAge(maxAge);
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
/**
* @description 删除Cookie
* @param response HTTP响应对象
* @param name Cookie名
* @return void
* @author yslg
* @since 2025-11-02
*/
public static void removeCookie(HttpServletResponse response, String name) {
Cookie cookie = new Cookie(name, null);
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
}
/**
* @description 判断是否为HTTPS请求
* @param request HTTP请求对象
* @return 是否为HTTPS请求
* @author yslg
* @since 2025-11-02
*/
public static boolean isHttps(HttpServletRequest request) {
return "https".equals(request.getScheme()) || request.isSecure() ||
"443".equals(request.getHeader("X-Forwarded-Port")) ||
"https".equals(request.getHeader("X-Forwarded-Proto"));
}
/**
* @description 获取上下文路径
* @param request HTTP请求对象
* @return 上下文路径
* @author yslg
* @since 2025-11-02
*/
public static String getContextPath(HttpServletRequest request) {
return request.getContextPath();
}
/**
* @description 检测请求是否包含指定参数
* @param request HTTP请求对象
* @param paramName 参数名
* @return 是否包含参数
* @author yslg
* @since 2025-11-02
*/
public static boolean hasParameter(HttpServletRequest request, String paramName) {
return request.getParameter(paramName) != null;
}
/**
* @description 获取Session对象
* @param request HTTP请求对象
* @return HttpSession对象
* @author yslg
* @since 2025-11-02
*/
public static HttpSession getSession(HttpServletRequest request) {
return request.getSession();
}
/**
* @description 获取Session属性
* @param request HTTP请求对象
* @param attributeName 属性名
* @return 属性值
* @author yslg
* @since 2025-11-02
*/
public static Object getSessionAttribute(HttpServletRequest request, String attributeName) {
HttpSession session = request.getSession(false);
return session != null ? session.getAttribute(attributeName) : null;
}
/**
* @description 设置Session属性
* @param request HTTP请求对象
* @param attributeName 属性名
* @param attributeValue 属性值
* @return void
* @author yslg
* @since 2025-11-02
*/
public static void setSessionAttribute(HttpServletRequest request, String attributeName, Object attributeValue) {
request.getSession().setAttribute(attributeName, attributeValue);
}
/**
* @description 移除Session属性
* @param request HTTP请求对象
* @param attributeName 属性名
* @return void
* @author yslg
* @since 2025-11-02
*/
public static void removeSessionAttribute(HttpServletRequest request, String attributeName) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute(attributeName);
}
}
/**
* @description 防止XSS攻击的字符串过滤
* @param input 输入字符串
* @return 过滤后的字符串
* @author yslg
* @since 2025-11-02
*/
public static String escapeXss(String input) {
if (input == null) {
return null;
}
return input
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("'", "&#39;")
.replace("\"", "&quot;")
.replace("&", "&amp;");
}
/**
* @description 获取所有请求参数名
* @param request HTTP请求对象
* @return 参数名集合
* @author yslg
* @since 2025-11-02
*/
public static Set<String> getParameterNames(HttpServletRequest request) {
Set<String> paramNames = new HashSet<>();
Enumeration<String> names = request.getParameterNames();
while (names.hasMoreElements()) {
paramNames.add(names.nextElement());
}
return paramNames;
}
}

View File

@@ -0,0 +1,235 @@
package org.xyzh.common.utils;
/**
* @description StringUtils.java文件描述 字符串工具类
* @filename StringUtils.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class StringUtils {
/**
* @description 字符串是否为空
* @param str String 字符串
* @return 是否为空
* @author yslg
* @since 2025-11-02
*/
public static boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
/**
* @description 字符串是否不为空
* @param str String 字符串
* @return 是否不为空
* @author yslg
* @since 2025-11-02
*/
public static boolean isNotEmpty(String str) {
return !isEmpty(str);
}
public static String format(String template, Object... args) {
if (template == null || args == null) return template;
return String.format(template, args);
}
/**
* @description 去除字符串首尾空格
* @param str String 字符串
* @return 去除空格后的字符串
* @author yslg
* @since 2025-11-02
*/
public static String trim(String str) {
return str == null ? null : str.trim();
}
/**
* @description 判断两个字符串是否相等支持null
* @param a String 字符串A
* @param b String 字符串B
* @return 是否相等
* @author yslg
* @since 2025-11-02
*/
public static boolean equals(String a, String b) {
return a == null ? b == null : a.equals(b);
}
/**
* @description 判断字符串是否包含子串
* @param str String 原字符串
* @param sub String 子串
* @return 是否包含
* @author yslg
* @since 2025-11-02
*/
public static boolean contains(String str, String sub) {
return str != null && sub != null && str.contains(sub);
}
/**
* @description 字符串拼接(用分隔符)
* @param delimiter String 分隔符
* @param elements String[] 待拼接字符串数组
* @return 拼接后的字符串
* @author yslg
* @since 2025-11-02
*/
public static String join(String delimiter, String... elements) {
if (elements == null) return null;
return String.join(delimiter, elements);
}
/**
* @description 字符串分割
* @param str String 原字符串
* @param regex String 分割正则表达式
* @return 分割后的字符串数组
* @author yslg
* @since 2025-11-02
*/
public static String[] split(String str, String regex) {
return str == null ? null : str.split(regex);
}
/**
* @description 字符串替换
* @param str String 原字符串
* @param target String 替换目标
* @param replacement String 替换内容
* @return 替换后的字符串
* @author yslg
* @since 2025-11-02
*/
public static String replace(String str, String target, String replacement) {
return str == null ? null : str.replace(target, replacement);
}
/**
* @description 是否以指定前缀开头
* @param str String 原字符串
* @param prefix String 前缀
* @return 是否以前缀开头
* @author yslg
* @since 2025-11-02
*/
public static boolean startsWith(String str, String prefix) {
return str != null && prefix != null && str.startsWith(prefix);
}
/**
* @description 是否以指定后缀结尾
* @param str String 原字符串
* @param suffix String 后缀
* @return 是否以后缀结尾
* @author yslg
* @since 2025-11-02
*/
public static boolean endsWith(String str, String suffix) {
return str != null && suffix != null && str.endsWith(suffix);
}
/**
* @description 转为大写
* @param str String 原字符串
* @return 大写字符串
* @author yslg
* @since 2025-11-02
*/
public static String toUpperCase(String str) {
return str == null ? null : str.toUpperCase();
}
/**
* @description 转为小写
* @param str String 原字符串
* @return 小写字符串
* @author yslg
* @since 2025-11-02
*/
public static String toLowerCase(String str) {
return str == null ? null : str.toLowerCase();
}
/**
* @description 反转字符串
* @param str String 原字符串
* @return 反转后的字符串
* @author yslg
* @since 2025-11-02
*/
public static String reverse(String str) {
if (str == null) return null;
return new StringBuilder(str).reverse().toString();
}
/**
* @description 重复字符串n次
* @param str String 原字符串
* @param n int 重复次数
* @return 重复后的字符串
* @author yslg
* @since 2025-11-02
*/
public static String repeat(String str, int n) {
if (str == null || n <= 0) return "";
return str.repeat(n);
}
/**
* @description 截取字符串
* @param str String 原字符串
* @param beginIndex int 起始索引
* @param endIndex int 结束索引
* @return 截取后的字符串
* @author yslg
* @since 2025-11-02
*/
public static String substring(String str, int beginIndex, int endIndex) {
if (str == null) return null;
if (beginIndex < 0) beginIndex = 0;
if (endIndex > str.length()) endIndex = str.length();
if (beginIndex > endIndex) return "";
return str.substring(beginIndex, endIndex);
}
/**
* @description 判断字符串是否为数字
* @param str String 原字符串
* @return 是否为数字
* @author yslg
* @since 2025-11-02
*/
public static boolean isNumeric(String str) {
if (isEmpty(str)) return false;
for (int i = 0; i < str.length(); i++) {
if (!Character.isDigit(str.charAt(i))) return false;
}
return true;
}
/**
* @description 字符串是否为空白
* @param str String 原字符串
* @return 是否为空白
* @author yslg
* @since 2025-11-02
*/
public static boolean isBlank(String str) {
return str == null || str.isBlank();
}
/**
* @description 字符串是否不为空白
* @param str String 原字符串
* @return 是否不为空白
* @author yslg
* @since 2025-11-02
*/
public static boolean isNotBlank(String str) {
return !isBlank(str);
}
}

View File

@@ -0,0 +1,297 @@
package org.xyzh.common.utils;
import java.text.DateFormat;
import java.time.Duration;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Instant;
import java.time.ZoneId;
import java.text.ParseException;
/**
* @description TimeUtils.java文件描述时间相关的常用工具方法
* @filename TimeUtils.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class TimeUtils {
/**
* @description 将时间戳字符串转为指定格式的日期时间字符串
* @param time 毫秒时间戳字符串
* @param format 日期格式化对象
* @return 格式化后的日期时间字符串
* @author yslg
* @since 2025-11-02
*/
public static String timeFormat(String time, DateFormat format){
try {
Date date = format.parse(time);
return format.format(date);
} catch (ParseException e) {
return null;
}
}
/**
* @description 格式化Date为指定格式字符串
* @param date Date对象
* @param pattern 格式化模式,如"yyyy-MM-dd HH:mm:ss"
* @return String 格式化后的字符串
*/
public static String format(Date date, String pattern) {
if (date == null || pattern == null) return null;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
LocalDateTime ldt = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
return ldt.format(formatter);
}
/**
* @description 格式化LocalDate为指定格式字符串
* @param localDate LocalDate对象
* @param pattern 格式化模式
* @return 格式化后的字符串
*/
public static String format(LocalDate localDate, String pattern) {
if (localDate == null || pattern == null) return null;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return localDate.format(formatter);
}
/**
* @description 格式化LocalDateTime为指定格式字符串
* @param localDateTime LocalDateTime对象
* @param pattern 格式化模式
* @return 格式化后的字符串
*/
public static String format(LocalDateTime localDateTime, String pattern) {
if (localDateTime == null || pattern == null) return null;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return localDateTime.format(formatter);
}
/**
* @description 格式化时间戳为指定格式字符串
* @param timestampMillis 毫秒时间戳
* @param pattern 格式化模式
* @return 格式化后的字符串
*/
public static String format(long timestampMillis, String pattern) {
if (pattern == null) return null;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
LocalDateTime ldt = Instant.ofEpochMilli(timestampMillis).atZone(ZoneId.systemDefault()).toLocalDateTime();
return ldt.format(formatter);
}
/**
* @description 将字符串按指定格式解析为LocalDateTime
* @param dateTimeStr 日期时间字符串
* @param pattern 格式化模式
* @return LocalDateTime对象解析失败返回null
*/
public static LocalDateTime parseToLocalDateTime(String dateTimeStr, String pattern) {
if (dateTimeStr == null || pattern == null) return null;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
try {
return LocalDateTime.parse(dateTimeStr, formatter);
} catch (Exception e) {
return null;
}
}
/**
* @description 将字符串按指定格式解析为LocalDate
* @param dateStr 日期字符串
* @param pattern 格式化模式
* @return LocalDate对象解析失败返回null
*/
public static LocalDate parseToLocalDate(String dateStr, String pattern) {
if (dateStr == null || pattern == null) return null;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
try {
return LocalDate.parse(dateStr, formatter);
} catch (Exception e) {
return null;
}
}
/**
* @description 将字符串或Date对象按指定DateTimeFormatter格式化为标准时间字符串yyyy-MM-dd HH:mm:ss
* @param input 可以为String类型的日期、Date对象或LocalDateTime对象
* @param formatter 指定的DateTimeFormatter
* @return 标准化时间字符串无法解析时返回null
*/
public static String normalizeToDateTimeString(Object input, DateTimeFormatter formatter) {
if (input == null || formatter == null) return null;
try {
if (input instanceof String str) {
LocalDateTime ldt;
try {
ldt = LocalDateTime.parse(str, formatter);
} catch (Exception e) {
LocalDate ld = LocalDate.parse(str, formatter);
ldt = ld.atStartOfDay();
}
return ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
if (input instanceof Date date) {
LocalDateTime ldt = dateToLocalDateTime(date);
return ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
if (input instanceof LocalDateTime ldt) {
return ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
if (input instanceof LocalDate ld) {
return ld.atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
} catch (Exception e) {
return null;
}
return null;
}
/**
* @description 获取当前时间戳,单位毫秒
* @return 当前时间戳字符串
* @author yslg
* @since 2025-11-02
*/
public static String getCurrentTimestamp() {
return String.valueOf(System.currentTimeMillis());
}
/**
* @description 获取当前时间戳,单位秒
* @return 当前时间戳(秒)字符串
* @author yslg
* @since 2025-11-02
*/
public static String getCurrentTimestampSeconds() {
return String.valueOf(System.currentTimeMillis() / 1000);
}
/**
* @description 获取当前日期时间格式yyyy-MM-dd HH:mm:ss
* @return 当前日期时间字符串
* @author yslg
* @since 2025-11-02
*/
public static String getCurrentDateTime() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* @description 获取当前日期格式yyyy-MM-dd
* @return 当前日期字符串
* @author yslg
* @since 2025-11-02
*/
public static String getCurrentDate() {
return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
/**
* @description 获取当前时间格式HH:mm:ss
* @return 当前时间字符串
* @author yslg
* @since 2025-11-02
*/
public static String getCurrentTime() {
return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
}
/**
* @description 将时间戳毫秒转为日期时间字符串格式yyyy-MM-dd HH:mm:ss
* @param timestampMillis 毫秒时间戳
* @return 日期时间字符串
* @author yslg
* @since 2025-11-02
*/
public static String timestampToDateTime(long timestampMillis) {
return Instant.ofEpochMilli(timestampMillis)
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* @description 将日期时间字符串yyyy-MM-dd HH:mm:ss转为时间戳毫秒
* @param dateTimeStr 日期时间字符串
* @return 毫秒时间戳
* @author yslg
* @since 2025-11-02
*/
public static long dateTimeToTimestamp(String dateTimeStr) {
LocalDateTime ldt = LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return ldt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
/**
* @description 获取指定日期加减天数后的日期字符串yyyy-MM-dd
* @param dateStr 原始日期字符串
* @param days 增加或减少的天数(可为负数)
* @return 计算后的日期字符串
* @author yslg
* @since 2025-11-02
*/
public static String plusDays(String dateStr, int days) {
LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
return date.plusDays(days).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
/**
* @description 获取两个日期之间的天数差
* @param startDate 开始日期字符串yyyy-MM-dd
* @param endDate 结束日期字符串yyyy-MM-dd
* @return 天数差
* @author yslg
* @since 2025-11-02
*/
public static long daysBetween(String startDate, String endDate) {
LocalDate start = LocalDate.parse(startDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
LocalDate end = LocalDate.parse(endDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
return Duration.between(start.atStartOfDay(), end.atStartOfDay()).toDays();
}
/**
* @description 判断当前时间是否在指定时间段内
* @param startTime 开始时间字符串HH:mm:ss
* @param endTime 结束时间字符串HH:mm:ss
* @return 是否在时间段内
* @author yslg
* @since 2025-11-02
*/
public static boolean isNowBetween(String startTime, String endTime) {
LocalTime now = LocalTime.now();
LocalTime start = LocalTime.parse(startTime, DateTimeFormatter.ofPattern("HH:mm:ss"));
LocalTime end = LocalTime.parse(endTime, DateTimeFormatter.ofPattern("HH:mm:ss"));
return !now.isBefore(start) && !now.isAfter(end);
}
/**
* @description Date转LocalDateTime
* @param date java.util.Date对象
* @return LocalDateTime对象
* @author yslg
* @since 2025-11-02
*/
public static LocalDateTime dateToLocalDateTime(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
/**
* @description LocalDateTime转Date
* @param localDateTime LocalDateTime对象
* @return java.util.Date对象
* @author yslg
* @since 2025-11-02
*/
public static Date localDateTimeToDate(LocalDateTime localDateTime) {
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
}

View File

@@ -0,0 +1,208 @@
package org.xyzh.common.utils.excel;
import org.xyzh.common.utils.validation.ValidationParam;
import java.util.ArrayList;
import java.util.List;
/**
* @description Excel列映射配置
* @filename ExcelColumnMapping.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ExcelColumnMapping {
/**
* @description Excel列名表头名称
*/
private String columnName;
/**
* @description Excel列索引从0开始优先级高于列名
*/
private Integer columnIndex;
/**
* @description 对象字段名
*/
private String fieldName;
/**
* @description 字段类型
*/
private Class<?> fieldType;
/**
* @description 是否必填
*/
private boolean required = false;
/**
* @description 默认值
*/
private String defaultValue;
/**
* @description 日期格式当字段类型为Date时使用
*/
private String dateFormat = "yyyy-MM-dd";
/**
* @description 校验参数列表
*/
private List<ValidationParam> validationParams;
private ExcelColumnMapping() {
this.validationParams = new ArrayList<>();
}
public String getColumnName() {
return columnName;
}
public Integer getColumnIndex() {
return columnIndex;
}
public String getFieldName() {
return fieldName;
}
public Class<?> getFieldType() {
return fieldType;
}
public boolean isRequired() {
return required;
}
public String getDefaultValue() {
return defaultValue;
}
public String getDateFormat() {
return dateFormat;
}
public List<ValidationParam> getValidationParams() {
return validationParams;
}
/**
* @description Builder类
*/
public static class Builder {
private ExcelColumnMapping mapping = new ExcelColumnMapping();
/**
* 设置Excel列名
*/
public Builder columnName(String columnName) {
mapping.columnName = columnName;
return this;
}
/**
* 设置Excel列索引从0开始
*/
public Builder columnIndex(int columnIndex) {
mapping.columnIndex = columnIndex;
return this;
}
/**
* 设置对象字段名
*/
public Builder fieldName(String fieldName) {
mapping.fieldName = fieldName;
return this;
}
/**
* 设置字段类型
*/
public Builder fieldType(Class<?> fieldType) {
mapping.fieldType = fieldType;
return this;
}
/**
* 设置是否必填
*/
public Builder required(boolean required) {
mapping.required = required;
return this;
}
/**
* 设置为必填
*/
public Builder required() {
mapping.required = true;
return this;
}
/**
* 设置默认值
*/
public Builder defaultValue(String defaultValue) {
mapping.defaultValue = defaultValue;
return this;
}
/**
* 设置日期格式
*/
public Builder dateFormat(String dateFormat) {
mapping.dateFormat = dateFormat;
return this;
}
/**
* 添加校验参数
*/
public Builder addValidation(ValidationParam param) {
mapping.validationParams.add(param);
return this;
}
/**
* 设置校验参数列表
*/
public Builder validations(List<ValidationParam> params) {
mapping.validationParams = params;
return this;
}
public ExcelColumnMapping build() {
if (mapping.fieldName == null || mapping.fieldName.isEmpty()) {
throw new IllegalArgumentException("字段名不能为空");
}
if (mapping.columnName == null && mapping.columnIndex == null) {
throw new IllegalArgumentException("必须指定列名或列索引");
}
if (mapping.fieldType == null) {
mapping.fieldType = String.class;
}
return mapping;
}
}
public static Builder builder() {
return new Builder();
}
@Override
public String toString() {
return "ExcelColumnMapping{" +
"columnName='" + columnName + '\'' +
", columnIndex=" + columnIndex +
", fieldName='" + fieldName + '\'' +
", fieldType=" + (fieldType != null ? fieldType.getSimpleName() : "null") +
", required=" + required +
'}';
}
}

View File

@@ -0,0 +1,142 @@
package org.xyzh.common.utils.excel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @description Excel读取结果
* @filename ExcelReadResult.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ExcelReadResult<T> {
/**
* @description 是否成功
*/
private boolean success;
/**
* @description 成功读取的数据列表
*/
private List<T> dataList;
/**
* @description 失败的行数据(行号 -> 错误信息)
*/
private Map<Integer, String> errorRowsMap;
/**
* @description 总行数(不包括表头)
*/
private int totalRows;
/**
* @description 成功行数
*/
private int successRows;
/**
* @description 失败行数
*/
private int errorRowsCount;
/**
* @description 错误信息
*/
private String errorMessage;
public ExcelReadResult() {
this.success = true;
this.dataList = new ArrayList<>();
this.errorRowsMap = new HashMap<>();
this.totalRows = 0;
this.successRows = 0;
this.errorRowsCount = 0;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public List<T> getDataList() {
return dataList;
}
public void setDataList(List<T> dataList) {
this.dataList = dataList;
}
public Map<Integer, String> getErrorRows() {
return errorRowsMap;
}
public void setErrorRows(Map<Integer, String> errorRowsMap) {
this.errorRowsMap = errorRowsMap;
}
public int getTotalRows() {
return totalRows;
}
public void setTotalRows(int totalRows) {
this.totalRows = totalRows;
}
public int getSuccessRows() {
return successRows;
}
public void setSuccessRows(int successRows) {
this.successRows = successRows;
}
public int getErrorRowsCount() {
return errorRowsCount;
}
public void setErrorRowsCount(int errorRowsCount) {
this.errorRowsCount = errorRowsCount;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public void addData(T data) {
this.dataList.add(data);
this.successRows++;
}
public void addError(int rowNum, String error) {
this.errorRowsMap.put(rowNum, error);
this.errorRowsCount++;
}
public boolean hasErrors() {
return this.errorRowsCount > 0;
}
@Override
public String toString() {
return "ExcelReadResult{" +
"success=" + success +
", totalRows=" + totalRows +
", successRows=" + successRows +
", errorRows=" + errorRowsCount +
", errorMessage='" + errorMessage + '\'' +
'}';
}
}

View File

@@ -0,0 +1,426 @@
package org.xyzh.common.utils.excel;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.xyzh.common.utils.validation.ValidationParam;
import org.xyzh.common.utils.validation.ValidationResult;
import org.xyzh.common.utils.validation.ValidationUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @description Excel读取工具类非泛型版本
* @filename ExcelReaderUtils.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ExcelReaderUtils {
/**
* @description 从文件读取Excel
* @param file Excel文件
* @param targetClass 目标对象Class
* @param columnMappings Excel列映射配置列表
* @return ExcelReadResult
*/
public static ExcelReadResult<Object> readExcel(File file, Class<?> targetClass, List<ExcelColumnMapping> columnMappings) {
try (FileInputStream fis = new FileInputStream(file)) {
return readExcel(fis, file.getName(), targetClass, columnMappings, new HashMap<>());
} catch (Exception e) {
ExcelReadResult<Object> result = new ExcelReadResult<>();
result.setSuccess(false);
result.setErrorMessage("读取Excel文件失败: " + e.getMessage());
return result;
}
}
/**
* @description 从文件读取Excel带配置
* @param file Excel文件
* @param targetClass 目标对象Class
* @param columnMappings Excel列映射配置列表
* @param options 配置选项
* @return ExcelReadResult
*/
public static ExcelReadResult<Object> readExcel(File file, Class<?> targetClass,
List<ExcelColumnMapping> columnMappings,
Map<String, Object> options) {
try (FileInputStream fis = new FileInputStream(file)) {
return readExcel(fis, file.getName(), targetClass, columnMappings, options);
} catch (Exception e) {
ExcelReadResult<Object> result = new ExcelReadResult<>();
result.setSuccess(false);
result.setErrorMessage("读取Excel文件失败: " + e.getMessage());
return result;
}
}
/**
* @description 从输入流读取Excel
* @param inputStream 输入流
* @param fileName 文件名
* @param targetClass 目标对象Class
* @param columnMappings Excel列映射配置列表
* @return ExcelReadResult
*/
public static ExcelReadResult<Object> readExcel(InputStream inputStream, String fileName,
Class<?> targetClass,
List<ExcelColumnMapping> columnMappings) {
return readExcel(inputStream, fileName, targetClass, columnMappings, new HashMap<>());
}
/**
* @description 从输入流读取Excel带配置
* @param inputStream 输入流
* @param fileName 文件名
* @param targetClass 目标对象Class
* @param columnMappings Excel列映射配置列表
* @param options 配置选项headerRowIndex, dataStartRowIndex, sheetIndex, sheetName, skipEmptyRow, maxRows, continueOnError
* @return ExcelReadResult
*/
public static ExcelReadResult<Object> readExcel(InputStream inputStream, String fileName,
Class<?> targetClass,
List<ExcelColumnMapping> columnMappings,
Map<String, Object> options) {
ExcelReadResult<Object> result = new ExcelReadResult<>();
try {
// 获取配置
int headerRowIndex = (int) options.getOrDefault("headerRowIndex", 0);
int dataStartRowIndex = (int) options.getOrDefault("dataStartRowIndex", 1);
int sheetIndex = (int) options.getOrDefault("sheetIndex", 0);
String sheetName = (String) options.get("sheetName");
boolean skipEmptyRow = (boolean) options.getOrDefault("skipEmptyRow", true);
int maxRows = (int) options.getOrDefault("maxRows", 0);
boolean continueOnError = (boolean) options.getOrDefault("continueOnError", true);
// 创建Workbook
Workbook workbook = createWorkbook(inputStream, fileName);
// 获取Sheet
Sheet sheet = getSheet(workbook, sheetIndex, sheetName);
if (sheet == null) {
result.setSuccess(false);
result.setErrorMessage("未找到指定的Sheet");
return result;
}
// 解析表头
Map<String, Integer> headerMap = parseHeader(sheet, headerRowIndex);
// 读取数据
readData(sheet, headerMap, targetClass, columnMappings, dataStartRowIndex,
skipEmptyRow, maxRows, continueOnError, result);
workbook.close();
} catch (Exception e) {
result.setSuccess(false);
result.setErrorMessage("读取Excel失败: " + e.getMessage());
}
return result;
}
/**
* @description 创建Workbook对象
*/
private static Workbook createWorkbook(InputStream inputStream, String fileName) throws Exception {
if (fileName.endsWith(".xlsx")) {
return new XSSFWorkbook(inputStream);
} else if (fileName.endsWith(".xls")) {
return new HSSFWorkbook(inputStream);
} else {
throw new IllegalArgumentException("不支持的文件格式,仅支持.xls和.xlsx");
}
}
/**
* @description 获取Sheet
*/
private static Sheet getSheet(Workbook workbook, int sheetIndex, String sheetName) {
if (sheetName != null && !sheetName.isEmpty()) {
return workbook.getSheet(sheetName);
} else {
return workbook.getSheetAt(sheetIndex);
}
}
/**
* @description 解析表头
*/
private static Map<String, Integer> parseHeader(Sheet sheet, int headerRowIndex) {
Map<String, Integer> headerMap = new HashMap<>();
Row headerRow = sheet.getRow(headerRowIndex);
if (headerRow != null) {
for (Cell cell : headerRow) {
String headerName = getCellValue(cell).toString().trim();
if (!headerName.isEmpty()) {
headerMap.put(headerName, cell.getColumnIndex());
}
}
}
return headerMap;
}
/**
* @description 读取数据
*/
private static void readData(Sheet sheet, Map<String, Integer> headerMap,
Class<?> targetClass, List<ExcelColumnMapping> columnMappings,
int startRow, boolean skipEmptyRow, int maxRows,
boolean continueOnError, ExcelReadResult<Object> result) {
int lastRowNum = sheet.getLastRowNum();
int endRow = maxRows > 0 ? Math.min(startRow + maxRows, lastRowNum + 1) : lastRowNum + 1;
for (int rowNum = startRow; rowNum < endRow; rowNum++) {
Row row = sheet.getRow(rowNum);
// 跳过空行
if (skipEmptyRow && isEmptyRow(row)) {
continue;
}
result.setTotalRows(result.getTotalRows() + 1);
try {
// 将行数据转换为对象
Object data = convertRowToObject(row, headerMap, targetClass, columnMappings, rowNum + 1);
// 数据校验
List<ValidationParam> allValidations = new ArrayList<>();
for (ExcelColumnMapping mapping : columnMappings) {
if (!mapping.getValidationParams().isEmpty()) {
allValidations.addAll(mapping.getValidationParams());
}
}
if (!allValidations.isEmpty()) {
ValidationResult validationResult = ValidationUtils.validate(data, allValidations);
if (!validationResult.isValid()) {
result.addError(rowNum + 1, validationResult.getFirstError());
if (!continueOnError) {
break;
}
continue;
}
}
result.addData(data);
} catch (Exception e) {
result.addError(rowNum + 1, e.getMessage());
if (!continueOnError) {
break;
}
}
}
}
/**
* @description 判断是否为空行
*/
private static boolean isEmptyRow(Row row) {
if (row == null) {
return true;
}
for (Cell cell : row) {
if (cell != null && cell.getCellType() != CellType.BLANK) {
String value = getCellValue(cell).toString().trim();
if (!value.isEmpty()) {
return false;
}
}
}
return true;
}
/**
* @description 将行数据转换为对象
*/
private static Object convertRowToObject(Row row, Map<String, Integer> headerMap,
Class<?> targetClass, List<ExcelColumnMapping> columnMappings,
int rowNum) throws Exception {
Object obj = targetClass.getDeclaredConstructor().newInstance();
for (ExcelColumnMapping mapping : columnMappings) {
// 获取单元格
Cell cell = getCell(row, mapping, headerMap);
// 获取单元格值
Object cellValue = getCellValue(cell);
// 处理空值
if (cellValue == null || cellValue.toString().trim().isEmpty()) {
if (mapping.isRequired()) {
throw new IllegalArgumentException("" + rowNum + "行,字段[" +
(mapping.getColumnName() != null ? mapping.getColumnName() : "索引" + mapping.getColumnIndex())
+ "]不能为空");
}
// 使用默认值
if (mapping.getDefaultValue() != null && !mapping.getDefaultValue().isEmpty()) {
cellValue = mapping.getDefaultValue();
} else {
continue;
}
}
// 类型转换
Object fieldValue = convertValue(cellValue, mapping.getFieldType(), mapping.getDateFormat());
// 设置字段值
Field field = getField(targetClass, mapping.getFieldName());
field.setAccessible(true);
field.set(obj, fieldValue);
}
return obj;
}
/**
* @description 获取字段(支持父类)
*/
private static Field getField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
// 尝试从父类获取
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
return getField(superClass, fieldName);
}
throw e;
}
}
/**
* @description 获取单元格
*/
private static Cell getCell(Row row, ExcelColumnMapping mapping, Map<String, Integer> headerMap) {
if (row == null) {
return null;
}
// 优先使用索引
if (mapping.getColumnIndex() != null) {
return row.getCell(mapping.getColumnIndex());
}
// 使用列名
Integer columnIndex = headerMap.get(mapping.getColumnName());
if (columnIndex != null) {
return row.getCell(columnIndex);
}
return null;
}
/**
* @description 获取单元格值
*/
private static Object getCellValue(Cell cell) {
if (cell == null) {
return null;
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue();
} else {
return cell.getNumericCellValue();
}
case BOOLEAN:
return cell.getBooleanCellValue();
case FORMULA:
return cell.getCellFormula();
case BLANK:
return "";
default:
return cell.toString();
}
}
/**
* @description 类型转换
*/
private static Object convertValue(Object value, Class<?> targetType, String dateFormat) throws Exception {
if (value == null) {
return null;
}
String strValue = value.toString().trim();
// String类型
if (targetType == String.class) {
return strValue;
}
// Integer类型
if (targetType == Integer.class || targetType == int.class) {
if (value instanceof Number) {
return ((Number) value).intValue();
}
return Integer.parseInt(strValue);
}
// Long类型
if (targetType == Long.class || targetType == long.class) {
if (value instanceof Number) {
return ((Number) value).longValue();
}
return Long.parseLong(strValue);
}
// Double类型
if (targetType == Double.class || targetType == double.class) {
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return Double.parseDouble(strValue);
}
// Float类型
if (targetType == Float.class || targetType == float.class) {
if (value instanceof Number) {
return ((Number) value).floatValue();
}
return Float.parseFloat(strValue);
}
// Boolean类型
if (targetType == Boolean.class || targetType == boolean.class) {
if (value instanceof Boolean) {
return value;
}
return Boolean.parseBoolean(strValue) || "1".equals(strValue) ||
"".equals(strValue) || "true".equalsIgnoreCase(strValue);
}
// Date类型
if (targetType == Date.class) {
if (value instanceof Date) {
return value;
}
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
return sdf.parse(strValue);
}
// 其他类型
return value;
}
}

View File

@@ -0,0 +1,429 @@
package org.xyzh.common.utils.excel;
import org.xyzh.common.utils.validation.ValidationParam;
import org.xyzh.common.utils.validation.method.ValidateMethodType;
import java.io.File;
import java.util.*;
/**
* @description Excel工具使用示例
* @filename ExcelUtilsExample.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ExcelUtilsExample {
/**
* 示例实体类:用户信息
*/
public static class UserInfo {
private String name;
private Integer age;
private String phone;
private String email;
private String idCard;
private Date joinDate;
private Boolean active;
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getIdCard() { return idCard; }
public void setIdCard(String idCard) { this.idCard = idCard; }
public Date getJoinDate() { return joinDate; }
public void setJoinDate(Date joinDate) { this.joinDate = joinDate; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
", idCard='" + idCard + '\'' +
", joinDate=" + joinDate +
", active=" + active +
'}';
}
}
/**
* 示例1基本使用
*/
public static void example1_BasicUsage() {
System.out.println("========== 示例1: 基本使用 ==========");
// 1. 定义列映射关系
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnName("姓名")
.fieldName("name")
.fieldType(String.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnName("年龄")
.fieldName("age")
.fieldType(Integer.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnName("手机号")
.fieldName("phone")
.fieldType(String.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnName("邮箱")
.fieldName("email")
.fieldType(String.class)
.validations(Arrays.asList(
ValidationParam.builder()
.fieldName("email")
.fieldLabel("邮箱")
.required()
.validateMethod(ValidateMethodType.EMAIL)
.build()
))
.build(),
ExcelColumnMapping.builder()
.columnName("入职日期")
.fieldName("joinDate")
.fieldType(Date.class)
.dateFormat("yyyy-MM-dd")
.build(),
ExcelColumnMapping.builder()
.columnName("是否在职")
.fieldName("active")
.fieldType(Boolean.class)
.defaultValue("true")
.build()
);
// 2. 读取Excel
File file = new File("users.xlsx");
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file,
UserInfo.class,
columnMappings
);
// 3. 处理结果
if (result.isSuccess()) {
System.out.println("读取成功!");
System.out.println("总行数: " + result.getTotalRows());
System.out.println("成功行数: " + result.getSuccessRows());
for (Object obj : result.getDataList()) {
UserInfo user = (UserInfo) obj;
System.out.println(user);
}
} else {
System.out.println("读取失败: " + result.getErrorMessage());
}
}
/**
* 示例2带数据校验
*/
public static void example2_WithValidation() {
System.out.println("\n========== 示例2: 带数据校验 ==========");
// 定义列映射关系(带校验)
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnName("姓名")
.fieldName("name")
.fieldType(String.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("name")
.fieldLabel("姓名")
.required()
.minLength(2)
.maxLength(20)
.validateMethod(ValidateMethodType.CHINESE)
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("年龄")
.fieldName("age")
.fieldType(Integer.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("age")
.fieldLabel("年龄")
.required()
.customValidator(value -> {
Integer age = (Integer) value;
return age >= 18 && age <= 65;
})
.customErrorMessage("年龄必须在18-65岁之间")
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("手机号")
.fieldName("phone")
.fieldType(String.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("phone")
.fieldLabel("手机号")
.required()
.validateMethod(ValidateMethodType.PHONE)
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("邮箱")
.fieldName("email")
.fieldType(String.class)
.addValidation(
ValidationParam.builder()
.fieldName("email")
.fieldLabel("邮箱")
.required(false)
.validateMethod(ValidateMethodType.EMAIL)
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("身份证号")
.fieldName("idCard")
.fieldType(String.class)
.addValidation(
ValidationParam.builder()
.fieldName("idCard")
.fieldLabel("身份证号")
.required(false)
.validateMethod(ValidateMethodType.ID_CARD)
.build()
)
.build()
);
// 读取配置
Map<String, Object> options = new HashMap<>();
options.put("continueOnError", true); // 遇到错误继续读取
File file = new File("users.xlsx");
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file,
UserInfo.class,
columnMappings,
options
);
// 处理结果
System.out.println("读取完成!");
System.out.println("总行数: " + result.getTotalRows());
System.out.println("成功行数: " + result.getSuccessRows());
System.out.println("失败行数: " + result.getErrorRowsCount());
// 显示错误信息
if (result.hasErrors()) {
System.out.println("\n错误信息:");
result.getErrorRows().forEach((rowNum, error) -> {
System.out.println("" + rowNum + "行: " + error);
});
}
}
/**
* 示例3使用列索引
*/
public static void example3_UseColumnIndex() {
System.out.println("\n========== 示例3: 使用列索引 ==========");
// 使用列索引而非列名
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnIndex(0) // 第1列
.fieldName("name")
.fieldType(String.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnIndex(1) // 第2列
.fieldName("age")
.fieldType(Integer.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnIndex(2) // 第3列
.fieldName("phone")
.fieldType(String.class)
.required()
.build()
);
File file = new File("users.xlsx");
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file,
UserInfo.class,
columnMappings
);
System.out.println("成功读取: " + result.getSuccessRows() + "");
}
/**
* 示例4自定义配置
*/
public static void example4_CustomConfig() {
System.out.println("\n========== 示例4: 自定义配置 ==========");
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnName("姓名")
.fieldName("name")
.fieldType(String.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnName("年龄")
.fieldName("age")
.fieldType(Integer.class)
.required()
.build()
);
// 自定义配置
Map<String, Object> options = new HashMap<>();
options.put("sheetName", "员工信息"); // 指定Sheet名称
options.put("headerRowIndex", 0); // 表头在第1行
options.put("dataStartRowIndex", 1); // 数据从第2行开始
options.put("skipEmptyRow", true); // 跳过空行
options.put("maxRows", 1000); // 最多读取1000行
options.put("continueOnError", true); // 遇到错误继续
File file = new File("users.xlsx");
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file,
UserInfo.class,
columnMappings,
options
);
System.out.println("读取结果: " + result);
}
/**
* 示例5在Controller中使用
*/
public static void example5_InController() {
System.out.println("\n========== 示例5: 在Controller中使用代码示例==========");
System.out.println("""
@PostMapping("/import")
public ResultDomain<String> importUsers(@RequestParam("file") MultipartFile file) {
try {
// 1. 定义列映射关系
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnName("姓名")
.fieldName("name")
.fieldType(String.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("name")
.fieldLabel("姓名")
.required()
.validateMethod(ValidateMethodType.CHINESE)
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("手机号")
.fieldName("phone")
.fieldType(String.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("phone")
.fieldLabel("手机号")
.required()
.validateMethod(ValidateMethodType.PHONE)
.build()
)
.build()
);
// 2. 读取Excel
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file.getInputStream(),
file.getOriginalFilename(),
UserInfo.class,
columnMappings
);
// 3. 处理结果
if (result.hasErrors()) {
StringBuilder errorMsg = new StringBuilder();
errorMsg.append("导入失败,共").append(result.getErrorRowsCount()).append("行数据有误:\\n");
result.getErrorRows().forEach((rowNum, error) -> {
errorMsg.append("").append(rowNum).append("行: ").append(error).append("\\n");
});
return ResultDomain.fail(errorMsg.toString());
}
// 4. 保存数据
List<UserInfo> users = new ArrayList<>();
for (Object obj : result.getDataList()) {
users.add((UserInfo) obj);
}
userService.batchSave(users);
return ResultDomain.success("导入成功,共导入" + result.getSuccessRows() + "条数据");
} catch (Exception e) {
return ResultDomain.fail("导入失败: " + e.getMessage());
}
}
""");
}
public static void main(String[] args) {
// 运行示例
// example1_BasicUsage();
// example2_WithValidation();
// example3_UseColumnIndex();
// example4_CustomConfig();
example5_InController();
}
}

View File

@@ -0,0 +1,542 @@
# Excel读取工具使用说明
## 概述
这是一个功能强大的Excel读取工具通过配置 **Excel列对象** 来定义Excel列与Java对象字段的映射关系支持自动构建对象并集成数据校验功能。
## 核心组件
### 1. ExcelColumnMapping - Excel列映射配置
定义Excel列与对象字段的映射关系
- **columnName** - Excel列名表头名称
- **columnIndex** - Excel列索引从0开始优先级高于列名
- **fieldName** - 对象字段名
- **fieldType** - 字段类型
- **required** - 是否必填
- **defaultValue** - 默认值
- **dateFormat** - 日期格式
- **validationParams** - 校验参数列表
### 2. ExcelReaderUtils - Excel读取工具类
提供静态方法读取Excel
- `readExcel(File, Class, List<ExcelColumnMapping>)` - 从文件读取
- `readExcel(File, Class, List<ExcelColumnMapping>, Map<options>)` - 从文件读取(带配置)
- `readExcel(InputStream, fileName, Class, List<ExcelColumnMapping>)` - 从输入流读取
- `readExcel(InputStream, fileName, Class, List<ExcelColumnMapping>, Map<options>)` - 从输入流读取(带配置)
### 3. ExcelReadResult - 读取结果
包含读取结果的详细信息:
- 成功数据列表
- 错误行信息(行号 -> 错误消息)
- 统计信息(总行数、成功数、失败数)
## 基本使用
### 1. 定义实体类
```java
public class UserInfo {
private String name;
private Integer age;
private String phone;
private String email;
private Date joinDate;
private Boolean active;
// Getters and Setters...
}
```
### 2. 定义列映射关系
```java
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnName("姓名") // Excel列名
.fieldName("name") // 对象字段名
.fieldType(String.class) // 字段类型
.required() // 必填
.build(),
ExcelColumnMapping.builder()
.columnName("年龄")
.fieldName("age")
.fieldType(Integer.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnName("手机号")
.fieldName("phone")
.fieldType(String.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnName("邮箱")
.fieldName("email")
.fieldType(String.class)
.build(),
ExcelColumnMapping.builder()
.columnName("入职日期")
.fieldName("joinDate")
.fieldType(Date.class)
.dateFormat("yyyy-MM-dd")
.build(),
ExcelColumnMapping.builder()
.columnName("是否在职")
.fieldName("active")
.fieldType(Boolean.class)
.defaultValue("true")
.build()
);
```
### 3. 读取Excel
```java
// 读取文件
File file = new File("users.xlsx");
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file,
UserInfo.class,
columnMappings
);
// 处理结果
if (result.isSuccess()) {
for (Object obj : result.getDataList()) {
UserInfo user = (UserInfo) obj;
System.out.println(user);
}
System.out.println("成功读取: " + result.getSuccessRows() + " 行");
}
```
### 4. 带数据校验的读取
```java
// 定义列映射关系(带校验)
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnName("姓名")
.fieldName("name")
.fieldType(String.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("name")
.fieldLabel("姓名")
.required()
.minLength(2)
.maxLength(20)
.validateMethod(ValidateMethodType.CHINESE)
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("年龄")
.fieldName("age")
.fieldType(Integer.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("age")
.fieldLabel("年龄")
.required()
.customValidator(value -> {
Integer age = (Integer) value;
return age >= 18 && age <= 65;
})
.customErrorMessage("年龄必须在18-65岁之间")
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("手机号")
.fieldName("phone")
.fieldType(String.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("phone")
.fieldLabel("手机号")
.required()
.validateMethod(ValidateMethodType.PHONE)
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("邮箱")
.fieldName("email")
.fieldType(String.class)
.addValidation(
ValidationParam.builder()
.fieldName("email")
.fieldLabel("邮箱")
.required(false)
.validateMethod(ValidateMethodType.EMAIL)
.build()
)
.build()
);
// 配置选项
Map<String, Object> options = new HashMap<>();
options.put("continueOnError", true); // 遇到错误继续读取
// 读取Excel
File file = new File("users.xlsx");
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file,
UserInfo.class,
columnMappings,
options
);
// 处理结果
System.out.println("总行数: " + result.getTotalRows());
System.out.println("成功: " + result.getSuccessRows());
System.out.println("失败: " + result.getErrorRowsCount());
// 显示错误
if (result.hasErrors()) {
result.getErrorRows().forEach((rowNum, error) -> {
System.out.println("第" + rowNum + "行: " + error);
});
}
```
### 5. 使用列索引而非列名
```java
// 使用列索引从0开始
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnIndex(0) // 第1列
.fieldName("name")
.fieldType(String.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnIndex(1) // 第2列
.fieldName("age")
.fieldType(Integer.class)
.required()
.build(),
ExcelColumnMapping.builder()
.columnIndex(2) // 第3列
.fieldName("phone")
.fieldType(String.class)
.required()
.build()
);
File file = new File("users.xlsx");
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file,
UserInfo.class,
columnMappings
);
```
### 6. 自定义配置选项
以下是旧版本的校验代码现在已经整合到ExcelColumnMapping中
```java
// 旧版本(已弃用)
List<ValidationParam> validationParams = Arrays.asList(
ValidationParam.builder()
.fieldName("name")
.fieldLabel("姓名")
.required()
.minLength(2)
.maxLength(20)
.validateMethod(ValidateMethodType.CHINESE)
.build(),
ValidationParam.builder()
.fieldName("phone")
.fieldLabel("手机号")
.required()
.validateMethod(ValidateMethodType.PHONE)
.build(),
ValidationParam.builder()
.fieldName("email")
.fieldLabel("邮箱")
.required(false)
.validateMethod(ValidateMethodType.EMAIL)
.build(),
ValidationParam.builder()
.fieldName("age")
.fieldLabel("年龄")
.required()
.customValidator(value -> {
Integer age = (Integer) value;
return age >= 18 && age <= 65;
})
.customErrorMessage("年龄必须在18-65岁之间")
.build()
);
// 创建配置
ExcelReaderConfig<UserInfo> config = new ExcelReaderConfig<>(UserInfo.class)
.setValidationParams(validationParams)
.setContinueOnError(true); // 遇到错误继续读取
// 读取
ExcelReader<UserInfo> reader = ExcelReader.create(config);
ExcelReadResult<UserInfo> result = reader.read(file);
// 处理结果
System.out.println("总行数: " + result.getTotalRows());
System.out.println("成功: " + result.getSuccessRows());
System.out.println("失败: " + result.getErrorRowsCount());
// 显示错误
if (result.hasErrors()) {
result.getErrorRows().forEach((rowNum, error) -> {
System.out.println("第" + rowNum + "行: " + error);
});
}
```
```java
List<ExcelColumnMapping> columnMappings = Arrays.asList(
// 列映射配置...
);
// 自定义配置选项
Map<String, Object> options = new HashMap<>();
options.put("sheetName", "员工信息"); // 指定Sheet名称
options.put("headerRowIndex", 0); // 表头在第1行
options.put("dataStartRowIndex", 1); // 数据从第2行开始
options.put("skipEmptyRow", true); // 跳过空行
options.put("maxRows", 1000); // 最多读取1000行
options.put("continueOnError", true); // 遇到错误继续
File file = new File("users.xlsx");
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file,
UserInfo.class,
columnMappings,
options
);
```
## 在Controller中使用
```java
@PostMapping("/import")
public ResultDomain<String> importUsers(@RequestParam("file") MultipartFile file) {
try {
// 1. 定义列映射关系(带校验)
List<ExcelColumnMapping> columnMappings = Arrays.asList(
ExcelColumnMapping.builder()
.columnName("姓名")
.fieldName("name")
.fieldType(String.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("name")
.fieldLabel("姓名")
.required()
.validateMethod(ValidateMethodType.CHINESE)
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("手机号")
.fieldName("phone")
.fieldType(String.class)
.required()
.addValidation(
ValidationParam.builder()
.fieldName("phone")
.fieldLabel("手机号")
.required()
.validateMethod(ValidateMethodType.PHONE)
.build()
)
.build(),
ExcelColumnMapping.builder()
.columnName("邮箱")
.fieldName("email")
.fieldType(String.class)
.addValidation(
ValidationParam.builder()
.fieldName("email")
.fieldLabel("邮箱")
.validateMethod(ValidateMethodType.EMAIL)
.build()
)
.build()
);
// 2. 读取Excel
ExcelReadResult<Object> result = ExcelReaderUtils.readExcel(
file.getInputStream(),
file.getOriginalFilename(),
UserInfo.class,
columnMappings
);
// 3. 处理结果
if (result.hasErrors()) {
StringBuilder errorMsg = new StringBuilder();
errorMsg.append("导入失败,共").append(result.getErrorRowsCount()).append("行数据有误:\n");
result.getErrorRows().forEach((rowNum, error) -> {
errorMsg.append("第").append(rowNum).append("行: ").append(error).append("\n");
});
return ResultDomain.fail(errorMsg.toString());
}
// 4. 保存数据
List<UserInfo> users = new ArrayList<>();
for (Object obj : result.getDataList()) {
users.add((UserInfo) obj);
}
userService.batchSave(users);
return ResultDomain.success("导入成功,共导入" + result.getSuccessRows() + "条数据");
} catch (Exception e) {
return ResultDomain.fail("导入失败: " + e.getMessage());
}
}
```
## 支持的数据类型
- **String** - 字符串
- **Integer/int** - 整数
- **Long/long** - 长整数
- **Double/double** - 双精度浮点数
- **Float/float** - 单精度浮点数
- **Boolean/boolean** - 布尔值支持true/false、1/0、是/否)
- **Date** - 日期需指定dateFormat
## 配置选项
### ExcelColumnMapping Builder方法
| 方法 | 说明 | 必填 |
|------|------|------|
| columnName(String) | 设置Excel列名 | columnName和columnIndex至少一个 |
| columnIndex(int) | 设置Excel列索引从0开始优先级高于columnName | columnName和columnIndex至少一个 |
| fieldName(String) | 设置对象字段名 | 是 |
| fieldType(Class<?>) | 设置字段类型 | 否默认String.class |
| required() / required(boolean) | 设置是否必填 | 否默认false |
| defaultValue(String) | 设置默认值 | 否 |
| dateFormat(String) | 设置日期格式 | 否(默认"yyyy-MM-dd" |
| addValidation(ValidationParam) | 添加校验参数 | 否 |
| validations(List<ValidationParam>) | 设置校验参数列表 | 否 |
### Options配置项
传递给 `readExcel` 方法的 `Map<String, Object> options` 支持以下选项:
| 键名 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| headerRowIndex | int | 表头所在行从0开始 | 0 |
| dataStartRowIndex | int | 数据起始行从0开始 | 1 |
| sheetIndex | int | Sheet索引从0开始 | 0 |
| sheetName | String | Sheet名称优先级高于sheetIndex | null |
| skipEmptyRow | boolean | 跳过空行 | true |
| maxRows | int | 最大读取行数0=不限制) | 0 |
| continueOnError | boolean | 遇到错误继续读取 | true |
## 核心特性
1. **配置驱动**通过ExcelColumnMapping配置Excel列与对象字段的映射关系
2. **无需注解**:不需要在实体类上添加注解,更加灵活
3. **数据校验**集成ValidationUtils和ValidateMethodType进行专业数据校验
4. **灵活配置**支持多种配置选项Sheet选择、行范围、错误处理等
5. **错误处理**:详细的错误信息和错误行记录
6. **类型转换**:自动进行类型转换
7. **空值处理**:支持默认值和空值校验
8. **多Sheet支持**可指定Sheet名称或索引
9. **两种映射方式**:支持列名和列索引两种方式
## 注意事项
1. **实体类要求**:必须有无参构造函数
2. **字段访问**字段必须有setter方法或可访问支持父类字段
3. **列映射优先级**columnIndex优先级高于columnName
4. **日期类型**必须指定dateFormat
5. **布尔类型**支持多种格式true/false、1/0、是/否)
6. **错误处理**根据continueOnError选项决定遇到错误时是否继续
7. **文件格式**:支持.xls和.xlsx两种格式
8. **类型转换**自动转换支持String、Integer、Long、Double、Float、Boolean、Date
## 依赖
```xml
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
```
## API说明
### ExcelReaderUtils 静态方法
```java
// 从文件读取(基本)
public static ExcelReadResult<Object> readExcel(
File file,
Class<?> targetClass,
List<ExcelColumnMapping> columnMappings
)
// 从文件读取(带配置)
public static ExcelReadResult<Object> readExcel(
File file,
Class<?> targetClass,
List<ExcelColumnMapping> columnMappings,
Map<String, Object> options
)
// 从输入流读取(基本)
public static ExcelReadResult<Object> readExcel(
InputStream inputStream,
String fileName,
Class<?> targetClass,
List<ExcelColumnMapping> columnMappings
)
// 从输入流读取(带配置)
public static ExcelReadResult<Object> readExcel(
InputStream inputStream,
String fileName,
Class<?> targetClass,
List<ExcelColumnMapping> columnMappings,
Map<String, Object> options
)
```
## 完整示例
参考 `ExcelUtilsExample.java` 查看完整使用示例。

View File

@@ -0,0 +1,380 @@
# 数据校验工具使用说明
## 概述
这是一个灵活、可扩展的Java数据校验工具支持对对象和Map进行多种类型的校验。
## 核心组件
### 1. ValidationParam - 校验参数对象
定义字段的校验规则,支持:
- 字段名称和中文标签
- 是否必传
- 字段类型校验
- 字符串长度限制
- 数字范围限制
- 正则表达式校验
- 自定义校验函数
- 预定义的校验方法ValidateMethod
### 2. ValidationResult - 校验结果对象
保存校验结果,包含:
- 是否校验通过
- 错误信息列表
- 第一个错误信息
- 错误数量统计
### 3. ValidationUtils - 校验工具类
执行校验逻辑,提供:
- 校验Java对象
- 校验Map对象
- 快捷方法:`requiredString()`, `requiredNumber()`, `email()`, `phone()`
### 4. ValidateMethod - 校验方法接口
预定义的专业校验方法,已实现:
- **PasswordValidateMethod** - 密码校验
- **IdCardValidateMethod** - 身份证号校验
- **PhoneValidateMethod** - 手机号码校验
- **EmailValidateMethod** - 邮箱地址校验
- **UrlValidateMethod** - URL链接校验
- **BankCardValidateMethod** - 银行卡号校验
- **ChineseValidateMethod** - 中文字符校验
### 5. ValidateMethodType - 校验方法类型枚举 ⭐推荐使用
枚举类型指向预定义的校验方法,使用更简洁:
- **PASSWORD** - 密码校验6-20位字母+数字)
- **STRONG_PASSWORD** - 强密码校验8-20位大小写+数字+特殊字符)
- **ID_CARD** - 身份证号校验
- **PHONE** - 手机号码校验(中国大陆)
- **PHONE_LOOSE** - 手机号码校验(支持大陆/香港/台湾)
- **EMAIL** - 邮箱地址校验
- **URL** - URL链接校验
- **HTTPS_URL** - HTTPS链接校验
- **BANK_CARD** - 银行卡号校验
- **CHINESE** - 中文字符校验(纯中文)
- **CHINESE_WITH_PUNCTUATION** - 中文字符校验(允许标点)
## 基本使用示例
### 1. 使用枚举类型校验(⭐推荐)
```java
List<ValidationParam> params = Arrays.asList(
ValidationParam.builder()
.fieldName("password")
.fieldLabel("密码")
.required()
.validateMethod(ValidateMethodType.PASSWORD) // 使用枚举
.build(),
ValidationParam.builder()
.fieldName("email")
.fieldLabel("邮箱")
.required()
.validateMethod(ValidateMethodType.EMAIL) // 使用枚举
.build(),
ValidationParam.builder()
.fieldName("phone")
.fieldLabel("手机号")
.required()
.validateMethod(ValidateMethodType.PHONE) // 使用枚举
.build(),
ValidationParam.builder()
.fieldName("idCard")
.fieldLabel("身份证号")
.required()
.validateMethod(ValidateMethodType.ID_CARD) // 使用枚举
.build()
);
ValidationResult result = ValidationUtils.validateMap(data, params);
if (!result.isValid()) {
System.out.println(result.getFirstError());
}
```
### 2. 简单字段校验
```java
List<ValidationParam> params = Arrays.asList(
ValidationUtils.requiredString("username", "用户名", 3, 20),
ValidationUtils.email("email", "邮箱", true),
ValidationUtils.phone("phone", "手机号", false)
);
// 校验对象
ValidationResult result = ValidationUtils.validate(userObject, params);
// 校验Map
ValidationResult result = ValidationUtils.validateMap(userMap, params);
// 检查结果
if (result.isValid()) {
// 校验通过
} else {
// 获取错误信息
String firstError = result.getFirstError();
String allErrors = result.getAllErrors();
List<String> errors = result.getErrors();
}
```
### 3. 使用ValidateMethod进行专业校验兼容旧方式
```java
List<ValidationParam> params = Arrays.asList(
// 方式1使用枚举推荐
ValidationParam.builder()
.fieldName("password")
.fieldLabel("密码")
.required()
.validateMethod(ValidateMethodType.STRONG_PASSWORD) // 使用预定义的强密码
.build(),
// 方式2直接实例化如需自定义参数
ValidationParam.builder()
.fieldName("password2")
.fieldLabel("自定义密码")
.required()
.validateMethod(new PasswordValidateMethod(8, 20, true, true, true, true))
.build(),
// 身份证号校验
ValidationParam.builder()
.fieldName("idCard")
.fieldLabel("身份证号")
.required()
.validateMethod(new IdCardValidateMethod())
.build(),
// 手机号校验
ValidationParam.builder()
.fieldName("phone")
.fieldLabel("手机号")
.required()
.validateMethod(new PhoneValidateMethod())
.build(),
// 限制域名的邮箱校验
ValidationParam.builder()
.fieldName("email")
.fieldLabel("邮箱")
.required()
.validateMethod(new EmailValidateMethod(new String[]{"company.com"}))
.build()
);
ValidationResult result = ValidationUtils.validateMap(data, params);
```
### 4. 自定义校验
```java
ValidationParam param = ValidationParam.builder()
.fieldName("age")
.fieldLabel("年龄")
.required()
.customValidator(value -> {
Integer age = (Integer) value;
return age >= 18 && age <= 60;
})
.customErrorMessage("年龄必须在18-60岁之间")
.build();
```
### 5. 复合校验
```java
List<ValidationParam> params = Arrays.asList(
ValidationParam.builder()
.fieldName("username")
.fieldLabel("用户名")
.required()
.fieldType(String.class)
.minLength(3)
.maxLength(20)
.pattern("^[a-zA-Z0-9_]+$")
.patternDesc("只能包含字母、数字和下划线")
.build(),
ValidationParam.builder()
.fieldName("password")
.fieldLabel("密码")
.required()
.minLength(6)
.validateMethod(new PasswordValidateMethod())
.build()
);
```
## 预定义校验方法详解
### PasswordValidateMethod - 密码校验
```java
// 默认规则6-20位必须包含字母和数字
new PasswordValidateMethod()
// 自定义规则
new PasswordValidateMethod(
8, // 最小长度
20, // 最大长度
true, // 需要大写字母
true, // 需要小写字母
true, // 需要数字
true // 需要特殊字符
)
```
### IdCardValidateMethod - 身份证号校验
```java
// 支持15位和18位身份证号
// 自动校验:格式、省份代码、出生日期、校验码
new IdCardValidateMethod()
```
### PhoneValidateMethod - 手机号码校验
```java
// 严格模式:仅中国大陆手机号
new PhoneValidateMethod()
// 宽松模式:支持大陆、香港、台湾
new PhoneValidateMethod(false)
```
### EmailValidateMethod - 邮箱地址校验
```java
// 允许所有域名
new EmailValidateMethod()
// 限制特定域名
new EmailValidateMethod(new String[]{"company.com", "example.com"})
```
### UrlValidateMethod - URL链接校验
```java
// 允许HTTP和HTTPS
new UrlValidateMethod()
// 仅允许HTTPS
new UrlValidateMethod(true)
```
### BankCardValidateMethod - 银行卡号校验
```java
// 使用Luhn算法校验银行卡号
new BankCardValidateMethod()
```
### ChineseValidateMethod - 中文字符校验
```java
// 仅纯中文字符
new ChineseValidateMethod()
// 允许中文标点符号
new ChineseValidateMethod(true)
```
## 在Controller中使用
```java
@PostMapping("/register")
public ResultDomain<User> register(@RequestBody Map<String, Object> params) {
// 定义校验规则(使用枚举,更简洁)
List<ValidationParam> validationParams = Arrays.asList(
ValidationParam.builder()
.fieldName("username")
.fieldLabel("用户名")
.required()
.minLength(3)
.maxLength(20)
.build(),
ValidationParam.builder()
.fieldName("password")
.fieldLabel("密码")
.required()
.validateMethod(ValidateMethodType.PASSWORD) // 使用枚举!
.build(),
ValidationParam.builder()
.fieldName("email")
.fieldLabel("邮箱")
.required()
.validateMethod(ValidateMethodType.EMAIL) // 使用枚举!
.build(),
ValidationParam.builder()
.fieldName("phone")
.fieldLabel("手机号")
.required()
.validateMethod(ValidateMethodType.PHONE) // 使用枚举!
.build()
);
// 执行校验
ValidationResult validationResult = ValidationUtils.validateMap(params, validationParams);
if (!validationResult.isValid()) {
ResultDomain<User> result = new ResultDomain<>();
result.fail(validationResult.getFirstError());
return result;
}
// 校验通过,继续业务逻辑
// ...
}
```
## 自定义ValidateMethod
如需添加新的校验方法,只需实现`ValidateMethod`接口:
```java
public class CustomValidateMethod implements ValidateMethod {
@Override
public boolean validate(Object value) {
// 实现校验逻辑
return true;
}
@Override
public String getErrorMessage() {
return "自定义错误信息";
}
@Override
public String getName() {
return "自定义校验";
}
}
```
## 优势
1. **简洁性**使用枚举类型无需每次new对象 ⭐
2. **灵活性**:支持多种校验方式组合使用
3. **可扩展性**:易于添加新的校验方法
4. **可读性**Builder模式让代码清晰易懂
5. **可复用性**:预定义的校验方法可在项目中重复使用
6. **专业性**:内置多种常用的专业校验算法(身份证、银行卡等)
7. **类型安全**:枚举类型提供编译时类型检查
## 注意事项
1. **推荐使用枚举类型**`ValidateMethodType` 比直接 `new` 对象更简洁
2. 校验顺序:必填 -> 类型 -> 长度/范围 -> 正则 -> 自定义 -> ValidateMethodType -> ValidateMethod
3. ValidateMethod和customValidator可以同时使用都会执行
4. 当值为null且非必填时会跳过后续所有校验
5. 错误信息会累积,可以获取所有错误或只获取第一个错误
6. 枚举方式和实例方式可以并存,但推荐统一使用枚举方式

View File

@@ -0,0 +1,276 @@
package org.xyzh.common.utils.validation;
import org.xyzh.common.utils.validation.method.ValidateMethod;
import org.xyzh.common.utils.validation.method.ValidateMethodType;
import java.util.function.Predicate;
/**
* @description 校验参数对象,定义字段的校验规则
* @filename ValidationParam.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ValidationParam {
/**
* @description 字段名称
*/
private String fieldName;
/**
* @description 字段中文名称(用于错误提示)
*/
private String fieldLabel;
/**
* @description 是否必传
*/
private boolean required;
/**
* @description 字段类型
*/
private Class<?> fieldType;
/**
* @description 最小长度(字符串)
*/
private Integer minLength;
/**
* @description 最大长度(字符串)
*/
private Integer maxLength;
/**
* @description 最小值(数字)
*/
private Number minValue;
/**
* @description 最大值(数字)
*/
private Number maxValue;
/**
* @description 正则表达式
*/
private String pattern;
/**
* @description 正则表达式描述(用于错误提示)
*/
private String patternDesc;
/**
* @description 自定义校验函数
*/
private Predicate<Object> customValidator;
/**
* @description 自定义校验失败消息
*/
private String customErrorMessage;
/**
* @description 是否允许为空字符串(默认不允许)
*/
private boolean allowEmpty = false;
/**
* @description 校验方法(使用预定义的校验方法)
*/
private ValidateMethod validateMethod;
/**
* @description 校验方法类型枚举
*/
private ValidateMethodType validateMethodType;
/**
* @description 校验方法配置参数(用于需要自定义参数的校验方法)
*/
private Object[] methodParams;
// 私有构造函数使用Builder模式
private ValidationParam() {
}
public String getFieldName() {
return fieldName;
}
public String getFieldLabel() {
return fieldLabel;
}
public boolean isRequired() {
return required;
}
public Class<?> getFieldType() {
return fieldType;
}
public Integer getMinLength() {
return minLength;
}
public Integer getMaxLength() {
return maxLength;
}
public Number getMinValue() {
return minValue;
}
public Number getMaxValue() {
return maxValue;
}
public String getPattern() {
return pattern;
}
public String getPatternDesc() {
return patternDesc;
}
public Predicate<Object> getCustomValidator() {
return customValidator;
}
public String getCustomErrorMessage() {
return customErrorMessage;
}
public boolean isAllowEmpty() {
return allowEmpty;
}
public ValidateMethod getValidateMethod() {
return validateMethod;
}
public ValidateMethodType getValidateMethodType() {
return validateMethodType;
}
public Object[] getMethodParams() {
return methodParams;
}
/**
* @description Builder类用于构建ValidationParam对象
*/
public static class Builder {
private ValidationParam param = new ValidationParam();
public Builder fieldName(String fieldName) {
param.fieldName = fieldName;
return this;
}
public Builder fieldLabel(String fieldLabel) {
param.fieldLabel = fieldLabel;
return this;
}
public Builder required(boolean required) {
param.required = required;
return this;
}
public Builder required() {
param.required = true;
return this;
}
public Builder fieldType(Class<?> fieldType) {
param.fieldType = fieldType;
return this;
}
public Builder minLength(Integer minLength) {
param.minLength = minLength;
return this;
}
public Builder maxLength(Integer maxLength) {
param.maxLength = maxLength;
return this;
}
public Builder minValue(Number minValue) {
param.minValue = minValue;
return this;
}
public Builder maxValue(Number maxValue) {
param.maxValue = maxValue;
return this;
}
public Builder pattern(String pattern) {
param.pattern = pattern;
return this;
}
public Builder patternDesc(String patternDesc) {
param.patternDesc = patternDesc;
return this;
}
public Builder customValidator(Predicate<Object> customValidator) {
param.customValidator = customValidator;
return this;
}
public Builder customErrorMessage(String customErrorMessage) {
param.customErrorMessage = customErrorMessage;
return this;
}
public Builder allowEmpty(boolean allowEmpty) {
param.allowEmpty = allowEmpty;
return this;
}
public Builder validateMethod(ValidateMethod validateMethod) {
param.validateMethod = validateMethod;
return this;
}
public Builder validateMethod(ValidateMethodType methodType) {
param.validateMethodType = methodType;
return this;
}
public Builder validateMethod(ValidateMethodType methodType, Object... params) {
param.validateMethodType = methodType;
param.methodParams = params;
return this;
}
public ValidationParam build() {
if (param.fieldName == null || param.fieldName.isEmpty()) {
throw new IllegalArgumentException("fieldName不能为空");
}
if (param.fieldLabel == null || param.fieldLabel.isEmpty()) {
param.fieldLabel = param.fieldName;
}
return param;
}
}
/**
* @description 创建Builder对象
* @return Builder
*/
public static Builder builder() {
return new Builder();
}
}

View File

@@ -0,0 +1,96 @@
package org.xyzh.common.utils.validation;
import java.util.ArrayList;
import java.util.List;
/**
* @description 校验结果类
* @filename ValidationResult.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ValidationResult {
/**
* @description 是否校验通过
*/
private boolean valid;
/**
* @description 错误信息列表
*/
private List<String> errors;
/**
* @description 第一个错误信息
*/
private String firstError;
public ValidationResult() {
this.valid = true;
this.errors = new ArrayList<>();
}
public boolean isValid() {
return valid;
}
public void setValid(boolean valid) {
this.valid = valid;
}
public List<String> getErrors() {
return errors;
}
public String getFirstError() {
return firstError;
}
/**
* @description 添加错误信息
* @param error 错误信息
*/
public void addError(String error) {
this.valid = false;
this.errors.add(error);
if (this.firstError == null) {
this.firstError = error;
}
}
/**
* @description 获取所有错误信息的字符串
* @return 错误信息字符串
*/
public String getAllErrors() {
return String.join("; ", errors);
}
/**
* @description 是否有错误
* @return boolean
*/
public boolean hasErrors() {
return !valid;
}
/**
* @description 获取错误数量
* @return 错误数量
*/
public int getErrorCount() {
return errors.size();
}
@Override
public String toString() {
return "ValidationResult{" +
"valid=" + valid +
", errorCount=" + errors.size() +
", errors=" + errors +
'}';
}
}

View File

@@ -0,0 +1,321 @@
package org.xyzh.common.utils.validation;
import org.xyzh.common.utils.validation.method.ValidateMethod;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* @description 校验工具类
* @filename ValidationUtils.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ValidationUtils {
/**
* @description 校验对象
* @param obj 待校验的对象
* @param validationParams 校验参数列表
* @return ValidationResult 校验结果
*/
public static ValidationResult validate(Object obj, List<ValidationParam> validationParams) {
ValidationResult result = new ValidationResult();
if (obj == null) {
result.addError("待校验对象不能为null");
return result;
}
if (validationParams == null || validationParams.isEmpty()) {
return result;
}
for (ValidationParam param : validationParams) {
try {
Object fieldValue = getFieldValue(obj, param.getFieldName());
validateField(param, fieldValue, result);
} catch (Exception e) {
result.addError(param.getFieldLabel() + "字段获取失败: " + e.getMessage());
}
}
return result;
}
/**
* @description 校验Map对象
* @param map 待校验的Map
* @param validationParams 校验参数列表
* @return ValidationResult 校验结果
*/
public static ValidationResult validateMap(Map<String, Object> map, List<ValidationParam> validationParams) {
ValidationResult result = new ValidationResult();
if (map == null) {
result.addError("待校验Map不能为null");
return result;
}
if (validationParams == null || validationParams.isEmpty()) {
return result;
}
for (ValidationParam param : validationParams) {
Object fieldValue = map.get(param.getFieldName());
validateField(param, fieldValue, result);
}
return result;
}
/**
* @description 校验单个字段
* @param param 校验参数
* @param fieldValue 字段值
* @param result 校验结果
*/
private static void validateField(ValidationParam param, Object fieldValue, ValidationResult result) {
String fieldLabel = param.getFieldLabel();
// 1. 必填校验
if (param.isRequired()) {
if (fieldValue == null) {
result.addError(fieldLabel + "不能为空");
return;
}
if (fieldValue instanceof String) {
String strValue = (String) fieldValue;
if (!param.isAllowEmpty() && strValue.trim().isEmpty()) {
result.addError(fieldLabel + "不能为空字符串");
return;
}
}
}
// 如果值为null且非必填跳过后续校验
if (fieldValue == null) {
return;
}
// 2. 类型校验
if (param.getFieldType() != null) {
if (!param.getFieldType().isAssignableFrom(fieldValue.getClass())) {
result.addError(fieldLabel + "类型错误,期望类型: " + param.getFieldType().getSimpleName() +
", 实际类型: " + fieldValue.getClass().getSimpleName());
return;
}
}
// 3. 字符串长度校验
if (fieldValue instanceof String) {
String strValue = (String) fieldValue;
if (param.getMinLength() != null && strValue.length() < param.getMinLength()) {
result.addError(fieldLabel + "长度不能少于" + param.getMinLength() + "个字符");
}
if (param.getMaxLength() != null && strValue.length() > param.getMaxLength()) {
result.addError(fieldLabel + "长度不能超过" + param.getMaxLength() + "个字符");
}
}
// 4. 数字范围校验
if (fieldValue instanceof Number) {
double numValue = ((Number) fieldValue).doubleValue();
if (param.getMinValue() != null && numValue < param.getMinValue().doubleValue()) {
result.addError(fieldLabel + "不能小于" + param.getMinValue());
}
if (param.getMaxValue() != null && numValue > param.getMaxValue().doubleValue()) {
result.addError(fieldLabel + "不能大于" + param.getMaxValue());
}
}
// 5. 正则表达式校验
if (param.getPattern() != null && fieldValue instanceof String) {
String strValue = (String) fieldValue;
if (!Pattern.matches(param.getPattern(), strValue)) {
String errorMsg = fieldLabel + "格式不正确";
if (param.getPatternDesc() != null) {
errorMsg += "" + param.getPatternDesc();
}
result.addError(errorMsg);
}
}
// 6. 自定义校验
if (param.getCustomValidator() != null) {
try {
if (!param.getCustomValidator().test(fieldValue)) {
String errorMsg = param.getCustomErrorMessage();
if (errorMsg == null || errorMsg.isEmpty()) {
errorMsg = fieldLabel + "校验失败";
}
result.addError(errorMsg);
}
} catch (Exception e) {
result.addError(fieldLabel + "自定义校验异常: " + e.getMessage());
}
}
// 7. 使用ValidateMethod校验枚举类型
if (param.getValidateMethodType() != null) {
try {
ValidateMethod method = param.getValidateMethodType().createInstance();
if (!method.validate(fieldValue)) {
String errorMsg = method.getErrorMessage();
if (errorMsg != null && !errorMsg.isEmpty()) {
result.addError(errorMsg);
} else {
result.addError(fieldLabel + "校验失败");
}
}
} catch (Exception e) {
result.addError(fieldLabel + "校验异常: " + e.getMessage());
}
}
// 8. 使用ValidateMethod校验直接传入实例保留兼容性
if (param.getValidateMethod() != null) {
try {
if (!param.getValidateMethod().validate(fieldValue)) {
String errorMsg = param.getValidateMethod().getErrorMessage();
if (errorMsg != null && !errorMsg.isEmpty()) {
result.addError(errorMsg);
} else {
result.addError(fieldLabel + "校验失败");
}
}
} catch (Exception e) {
result.addError(fieldLabel + "校验异常: " + e.getMessage());
}
}
}
/**
* @description 获取对象字段值支持getter方法和直接访问
* @param obj 对象
* @param fieldName 字段名
* @return 字段值
* @throws Exception 异常
*/
private static Object getFieldValue(Object obj, String fieldName) throws Exception {
if (obj instanceof Map) {
return ((Map<?, ?>) obj).get(fieldName);
}
Class<?> clazz = obj.getClass();
// 首先尝试getter方法
try {
String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
return clazz.getMethod(getterName).invoke(obj);
} catch (NoSuchMethodException e) {
// getter方法不存在尝试直接访问字段
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException ex) {
// 尝试父类
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
Field field = superClass.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
throw ex;
}
}
}
/**
* @description 快速创建必填字符串校验参数
* @param fieldName 字段名
* @param fieldLabel 字段标签
* @return ValidationParam
*/
public static ValidationParam requiredString(String fieldName, String fieldLabel) {
return ValidationParam.builder()
.fieldName(fieldName)
.fieldLabel(fieldLabel)
.required()
.fieldType(String.class)
.build();
}
/**
* @description 快速创建必填字符串校验参数(带长度限制)
* @param fieldName 字段名
* @param fieldLabel 字段标签
* @param minLength 最小长度
* @param maxLength 最大长度
* @return ValidationParam
*/
public static ValidationParam requiredString(String fieldName, String fieldLabel, int minLength, int maxLength) {
return ValidationParam.builder()
.fieldName(fieldName)
.fieldLabel(fieldLabel)
.required()
.fieldType(String.class)
.minLength(minLength)
.maxLength(maxLength)
.build();
}
/**
* @description 快速创建必填数字校验参数
* @param fieldName 字段名
* @param fieldLabel 字段标签
* @param minValue 最小值
* @param maxValue 最大值
* @return ValidationParam
*/
public static ValidationParam requiredNumber(String fieldName, String fieldLabel, Number minValue, Number maxValue) {
return ValidationParam.builder()
.fieldName(fieldName)
.fieldLabel(fieldLabel)
.required()
.minValue(minValue)
.maxValue(maxValue)
.build();
}
/**
* @description 快速创建邮箱校验参数
* @param fieldName 字段名
* @param fieldLabel 字段标签
* @param required 是否必填
* @return ValidationParam
*/
public static ValidationParam email(String fieldName, String fieldLabel, boolean required) {
return ValidationParam.builder()
.fieldName(fieldName)
.fieldLabel(fieldLabel)
.required(required)
.fieldType(String.class)
.pattern("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
.patternDesc("请输入有效的邮箱地址")
.build();
}
/**
* @description 快速创建手机号校验参数
* @param fieldName 字段名
* @param fieldLabel 字段标签
* @param required 是否必填
* @return ValidationParam
*/
public static ValidationParam phone(String fieldName, String fieldLabel, boolean required) {
return ValidationParam.builder()
.fieldName(fieldName)
.fieldLabel(fieldLabel)
.required(required)
.fieldType(String.class)
.pattern("^1[3-9]\\d{9}$")
.patternDesc("请输入有效的手机号码")
.build();
}
}

View File

@@ -0,0 +1,71 @@
package org.xyzh.common.utils.validation.method;
/**
* @description 银行卡号校验方法Luhn算法
* @filename BankCardValidateMethod.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class BankCardValidateMethod implements ValidateMethod {
@Override
public boolean validate(Object value) {
if (value == null || !(value instanceof String)) {
return false;
}
String cardNumber = ((String) value).replaceAll("\\s", "");
// 长度校验银行卡号通常为16-19位
if (cardNumber.length() < 16 || cardNumber.length() > 19) {
return false;
}
// 数字校验
if (!cardNumber.matches("^\\d+$")) {
return false;
}
// Luhn算法校验
return luhnCheck(cardNumber);
}
/**
* @description Luhn算法校验银行卡校验算法
* @param cardNumber 银行卡号
* @return boolean 是否通过校验
*/
private boolean luhnCheck(String cardNumber) {
int sum = 0;
boolean alternate = false;
// 从右向左遍历
for (int i = cardNumber.length() - 1; i >= 0; i--) {
int digit = Character.getNumericValue(cardNumber.charAt(i));
if (alternate) {
digit *= 2;
if (digit > 9) {
digit = digit - 9;
}
}
sum += digit;
alternate = !alternate;
}
return sum % 10 == 0;
}
@Override
public String getErrorMessage() {
return "请输入有效的银行卡号";
}
@Override
public String getName() {
return "银行卡号校验";
}
}

View File

@@ -0,0 +1,54 @@
package org.xyzh.common.utils.validation.method;
import java.util.regex.Pattern;
/**
* @description 中文字符校验方法
* @filename ChineseValidateMethod.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class ChineseValidateMethod implements ValidateMethod {
// 中文字符正则(包括中文标点符号)
private static final Pattern CHINESE_PATTERN = Pattern.compile("^[\u4e00-\u9fa5]+$");
private final boolean allowPunctuation; // 是否允许中文标点符号
public ChineseValidateMethod() {
this.allowPunctuation = false;
}
public ChineseValidateMethod(boolean allowPunctuation) {
this.allowPunctuation = allowPunctuation;
}
@Override
public boolean validate(Object value) {
if (value == null || !(value instanceof String)) {
return false;
}
String str = (String) value;
if (allowPunctuation) {
// 允许中文字符和中文标点符号
return Pattern.matches("^[\u4e00-\u9fa5\\u3000-\\u303f]+$", str);
} else {
// 仅允许纯中文字符
return CHINESE_PATTERN.matcher(str).matches();
}
}
@Override
public String getErrorMessage() {
return allowPunctuation ? "请输入中文字符" : "请输入纯中文字符(不含标点符号)";
}
@Override
public String getName() {
return "中文字符校验";
}
}

View File

@@ -0,0 +1,77 @@
package org.xyzh.common.utils.validation.method;
import java.util.regex.Pattern;
/**
* @description 邮箱校验方法
* @filename EmailValidateMethod.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class EmailValidateMethod implements ValidateMethod {
// 邮箱正则表达式
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
);
private final String[] allowedDomains; // 允许的域名列表
/**
* @description 默认构造函数,允许所有域名
*/
public EmailValidateMethod() {
this.allowedDomains = null;
}
/**
* @description 限制域名的构造函数
* @param allowedDomains 允许的域名列表,例如:["company.com", "example.com"]
*/
public EmailValidateMethod(String[] allowedDomains) {
this.allowedDomains = allowedDomains;
}
@Override
public boolean validate(Object value) {
if (value == null || !(value instanceof String)) {
return false;
}
String email = ((String) value).trim().toLowerCase();
// 基本格式校验
if (!EMAIL_PATTERN.matcher(email).matches()) {
return false;
}
// 域名限制校验
if (allowedDomains != null && allowedDomains.length > 0) {
boolean domainMatched = false;
for (String domain : allowedDomains) {
if (email.endsWith("@" + domain.toLowerCase())) {
domainMatched = true;
break;
}
}
return domainMatched;
}
return true;
}
@Override
public String getErrorMessage() {
if (allowedDomains != null && allowedDomains.length > 0) {
return "请输入有效的邮箱地址(仅支持: " + String.join(", ", allowedDomains) + "";
}
return "请输入有效的邮箱地址";
}
@Override
public String getName() {
return "邮箱校验";
}
}

View File

@@ -0,0 +1,198 @@
package org.xyzh.common.utils.validation.method;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
* @description 身份证号码校验方法支持15位和18位身份证
* @filename IdCardValidateMethod.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class IdCardValidateMethod implements ValidateMethod {
// 加权因子
private static final int[] WEIGHT = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
// 校验码对应值
private static final char[] VALIDATE_CODE = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};
// 省份代码
private static final Map<String, String> PROVINCE_CODES = new HashMap<>();
static {
PROVINCE_CODES.put("11", "北京");
PROVINCE_CODES.put("12", "天津");
PROVINCE_CODES.put("13", "河北");
PROVINCE_CODES.put("14", "山西");
PROVINCE_CODES.put("15", "内蒙古");
PROVINCE_CODES.put("21", "辽宁");
PROVINCE_CODES.put("22", "吉林");
PROVINCE_CODES.put("23", "黑龙江");
PROVINCE_CODES.put("31", "上海");
PROVINCE_CODES.put("32", "江苏");
PROVINCE_CODES.put("33", "浙江");
PROVINCE_CODES.put("34", "安徽");
PROVINCE_CODES.put("35", "福建");
PROVINCE_CODES.put("36", "江西");
PROVINCE_CODES.put("37", "山东");
PROVINCE_CODES.put("41", "河南");
PROVINCE_CODES.put("42", "湖北");
PROVINCE_CODES.put("43", "湖南");
PROVINCE_CODES.put("44", "广东");
PROVINCE_CODES.put("45", "广西");
PROVINCE_CODES.put("46", "海南");
PROVINCE_CODES.put("50", "重庆");
PROVINCE_CODES.put("51", "四川");
PROVINCE_CODES.put("52", "贵州");
PROVINCE_CODES.put("53", "云南");
PROVINCE_CODES.put("54", "西藏");
PROVINCE_CODES.put("61", "陕西");
PROVINCE_CODES.put("62", "甘肃");
PROVINCE_CODES.put("63", "青海");
PROVINCE_CODES.put("64", "宁夏");
PROVINCE_CODES.put("65", "新疆");
PROVINCE_CODES.put("71", "台湾");
PROVINCE_CODES.put("81", "香港");
PROVINCE_CODES.put("82", "澳门");
}
@Override
public boolean validate(Object value) {
if (value == null || !(value instanceof String)) {
return false;
}
String idCard = ((String) value).toUpperCase();
// 长度校验
if (idCard.length() != 15 && idCard.length() != 18) {
return false;
}
// 格式校验
if (idCard.length() == 15) {
return validate15IdCard(idCard);
} else {
return validate18IdCard(idCard);
}
}
/**
* @description 校验15位身份证
*/
private boolean validate15IdCard(String idCard) {
// 15位身份证格式省(2位)市(2位)县(2位)年(2位)月(2位)日(2位)顺序号(3位)
if (!Pattern.matches("^\\d{15}$", idCard)) {
return false;
}
// 省份代码校验
String provinceCode = idCard.substring(0, 2);
if (!PROVINCE_CODES.containsKey(provinceCode)) {
return false;
}
// 出生日期校验
String year = "19" + idCard.substring(6, 8);
String month = idCard.substring(8, 10);
String day = idCard.substring(10, 12);
return validateDate(year, month, day);
}
/**
* @description 校验18位身份证
*/
private boolean validate18IdCard(String idCard) {
// 18位身份证格式省(2位)市(2位)县(2位)年(4位)月(2位)日(2位)顺序号(3位)校验码(1位)
if (!Pattern.matches("^\\d{17}[0-9Xx]$", idCard)) {
return false;
}
// 省份代码校验
String provinceCode = idCard.substring(0, 2);
if (!PROVINCE_CODES.containsKey(provinceCode)) {
return false;
}
// 出生日期校验
String year = idCard.substring(6, 10);
String month = idCard.substring(10, 12);
String day = idCard.substring(12, 14);
if (!validateDate(year, month, day)) {
return false;
}
// 校验码校验
return validateCheckCode(idCard);
}
/**
* @description 校验日期是否合法
*/
private boolean validateDate(String year, String month, String day) {
try {
int y = Integer.parseInt(year);
int m = Integer.parseInt(month);
int d = Integer.parseInt(day);
// 年份范围1900-当前年份
int currentYear = java.time.Year.now().getValue();
if (y < 1900 || y > currentYear) {
return false;
}
// 月份范围1-12
if (m < 1 || m > 12) {
return false;
}
// 日期范围
int[] daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 闰年2月29天
if (isLeapYear(y)) {
daysInMonth[1] = 29;
}
return d >= 1 && d <= daysInMonth[m - 1];
} catch (NumberFormatException e) {
return false;
}
}
/**
* @description 判断是否为闰年
*/
private boolean isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
/**
* @description 校验18位身份证的校验码
*/
private boolean validateCheckCode(String idCard) {
int sum = 0;
for (int i = 0; i < 17; i++) {
sum += (idCard.charAt(i) - '0') * WEIGHT[i];
}
char checkCode = VALIDATE_CODE[sum % 11];
return checkCode == idCard.charAt(17);
}
@Override
public String getErrorMessage() {
return "请输入有效的身份证号码15位或18位";
}
@Override
public String getName() {
return "身份证号码校验";
}
}

View File

@@ -0,0 +1,121 @@
package org.xyzh.common.utils.validation.method;
import java.util.regex.Pattern;
/**
* @description 密码校验方法
* @filename PasswordValidateMethod.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class PasswordValidateMethod implements ValidateMethod {
private final int minLength;
private final int maxLength;
private final boolean requireUpperCase;
private final boolean requireLowerCase;
private final boolean requireDigit;
private final boolean requireSpecialChar;
/**
* @description 默认密码规则6-20位必须包含字母和数字
*/
public PasswordValidateMethod() {
this(6, 20, false, false, true, false);
}
/**
* @description 自定义密码规则
* @param minLength 最小长度
* @param maxLength 最大长度
* @param requireUpperCase 是否需要大写字母
* @param requireLowerCase 是否需要小写字母
* @param requireDigit 是否需要数字
* @param requireSpecialChar 是否需要特殊字符
*/
public PasswordValidateMethod(int minLength, int maxLength,
boolean requireUpperCase, boolean requireLowerCase,
boolean requireDigit, boolean requireSpecialChar) {
this.minLength = minLength;
this.maxLength = maxLength;
this.requireUpperCase = requireUpperCase;
this.requireLowerCase = requireLowerCase;
this.requireDigit = requireDigit;
this.requireSpecialChar = requireSpecialChar;
}
@Override
public boolean validate(Object value) {
if (value == null || !(value instanceof String)) {
return false;
}
String password = (String) value;
// 长度校验
if (password.length() < minLength || password.length() > maxLength) {
return false;
}
// 大写字母校验
if (requireUpperCase && !Pattern.compile("[A-Z]").matcher(password).find()) {
return false;
}
// 小写字母校验
if (requireLowerCase && !Pattern.compile("[a-z]").matcher(password).find()) {
return false;
}
// 数字校验
if (requireDigit && !Pattern.compile("[0-9]").matcher(password).find()) {
return false;
}
// 特殊字符校验
if (requireSpecialChar && !Pattern.compile("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?]").matcher(password).find()) {
return false;
}
return true;
}
@Override
public String getErrorMessage() {
StringBuilder msg = new StringBuilder("密码必须是");
msg.append(minLength).append("-").append(maxLength).append("");
if (requireUpperCase || requireLowerCase || requireDigit || requireSpecialChar) {
msg.append(",且包含");
boolean first = true;
if (requireUpperCase) {
msg.append("大写字母");
first = false;
}
if (requireLowerCase) {
if (!first) msg.append("");
msg.append("小写字母");
first = false;
}
if (requireDigit) {
if (!first) msg.append("");
msg.append("数字");
first = false;
}
if (requireSpecialChar) {
if (!first) msg.append("");
msg.append("特殊字符");
}
}
return msg.toString();
}
@Override
public String getName() {
return "密码校验";
}
}

View File

@@ -0,0 +1,65 @@
package org.xyzh.common.utils.validation.method;
import java.util.regex.Pattern;
/**
* @description 手机号码校验方法
* @filename PhoneValidateMethod.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class PhoneValidateMethod implements ValidateMethod {
// 中国大陆手机号正则
private static final Pattern CHINA_PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
// 香港手机号正则
private static final Pattern HK_PHONE_PATTERN = Pattern.compile("^[5-9]\\d{7}$");
// 台湾手机号正则
private static final Pattern TW_PHONE_PATTERN = Pattern.compile("^09\\d{8}$");
private final boolean strictMode; // 严格模式,只验证中国大陆手机号
public PhoneValidateMethod() {
this.strictMode = true;
}
public PhoneValidateMethod(boolean strictMode) {
this.strictMode = strictMode;
}
@Override
public boolean validate(Object value) {
if (value == null || !(value instanceof String)) {
return false;
}
String phone = (String) value;
// 去除空格和横线
phone = phone.replaceAll("[\\s-]", "");
if (strictMode) {
// 严格模式:只验证中国大陆手机号
return CHINA_PHONE_PATTERN.matcher(phone).matches();
} else {
// 宽松模式:支持大陆、香港、台湾手机号
return CHINA_PHONE_PATTERN.matcher(phone).matches()
|| HK_PHONE_PATTERN.matcher(phone).matches()
|| TW_PHONE_PATTERN.matcher(phone).matches();
}
}
@Override
public String getErrorMessage() {
return strictMode ? "请输入有效的手机号码" : "请输入有效的手机号码(支持大陆、香港、台湾)";
}
@Override
public String getName() {
return "手机号码校验";
}
}

View File

@@ -0,0 +1,60 @@
package org.xyzh.common.utils.validation.method;
import java.util.regex.Pattern;
/**
* @description URL校验方法
* @filename UrlValidateMethod.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public class UrlValidateMethod implements ValidateMethod {
// URL正则表达式
private static final Pattern URL_PATTERN = Pattern.compile(
"^(https?|ftp)://[a-zA-Z0-9+&@#/%?=~_|!:,.;-]*[a-zA-Z0-9+&@#/%=~_|-]$"
);
private final boolean requireHttps; // 是否要求HTTPS
public UrlValidateMethod() {
this.requireHttps = false;
}
public UrlValidateMethod(boolean requireHttps) {
this.requireHttps = requireHttps;
}
@Override
public boolean validate(Object value) {
if (value == null || !(value instanceof String)) {
return false;
}
String url = ((String) value).trim();
// 基本格式校验
if (!URL_PATTERN.matcher(url).matches()) {
return false;
}
// HTTPS校验
if (requireHttps && !url.startsWith("https://")) {
return false;
}
return true;
}
@Override
public String getErrorMessage() {
return requireHttps ? "请输入有效的HTTPS链接" : "请输入有效的URL";
}
@Override
public String getName() {
return "URL校验";
}
}

View File

@@ -0,0 +1,31 @@
package org.xyzh.common.utils.validation.method;
/**
* @description 校验方法接口,定义不同类型的校验方式
* @filename ValidateMethod.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public interface ValidateMethod {
/**
* @description 校验方法
* @param value 待校验的值
* @return boolean 是否校验通过
*/
boolean validate(Object value);
/**
* @description 获取校验失败的错误提示信息
* @return String 错误提示信息
*/
String getErrorMessage();
/**
* @description 获取校验方法的名称
* @return String 校验方法名称
*/
String getName();
}

View File

@@ -0,0 +1,133 @@
package org.xyzh.common.utils.validation.method;
import java.util.function.Supplier;
/**
* @description 校验方法类型枚举
* @filename ValidateMethodType.java
* @author yslg
* @copyright yslg
* @since 2025-11-02
*/
public enum ValidateMethodType {
/**
* 密码校验默认6-20位必须包含字母和数字
*/
PASSWORD("密码校验", PasswordValidateMethod.class, PasswordValidateMethod::new),
/**
* 强密码校验8-20位必须包含大小写字母、数字和特殊字符
*/
STRONG_PASSWORD("强密码校验", PasswordValidateMethod.class,
() -> new PasswordValidateMethod(8, 20, true, true, true, true)),
/**
* 身份证号校验支持15位和18位
*/
ID_CARD("身份证号校验", IdCardValidateMethod.class, IdCardValidateMethod::new),
/**
* 手机号码校验(中国大陆)
*/
PHONE("手机号码校验", PhoneValidateMethod.class, PhoneValidateMethod::new),
/**
* 手机号码校验(宽松模式,支持大陆、香港、台湾)
*/
PHONE_LOOSE("手机号码校验(宽松)", PhoneValidateMethod.class,
() -> new PhoneValidateMethod(false)),
/**
* 邮箱地址校验
*/
EMAIL("邮箱地址校验", EmailValidateMethod.class, EmailValidateMethod::new),
/**
* URL链接校验
*/
URL("URL链接校验", UrlValidateMethod.class, UrlValidateMethod::new),
/**
* HTTPS链接校验
*/
HTTPS_URL("HTTPS链接校验", UrlValidateMethod.class,
() -> new UrlValidateMethod(true)),
/**
* 银行卡号校验
*/
BANK_CARD("银行卡号校验", BankCardValidateMethod.class, BankCardValidateMethod::new),
/**
* 中文字符校验(纯中文)
*/
CHINESE("中文字符校验", ChineseValidateMethod.class, ChineseValidateMethod::new),
/**
* 中文字符校验(允许标点符号)
*/
CHINESE_WITH_PUNCTUATION("中文字符校验(含标点)", ChineseValidateMethod.class,
() -> new ChineseValidateMethod(true));
/**
* 校验方法名称
*/
private final String name;
/**
* 校验方法实现类
*/
private final Class<? extends ValidateMethod> methodClass;
/**
* 校验方法实例提供者
*/
private final Supplier<ValidateMethod> methodSupplier;
ValidateMethodType(String name, Class<? extends ValidateMethod> methodClass,
Supplier<ValidateMethod> methodSupplier) {
this.name = name;
this.methodClass = methodClass;
this.methodSupplier = methodSupplier;
}
/**
* @description 获取校验方法名称
* @return String
*/
public String getName() {
return name;
}
/**
* @description 获取校验方法实现类
* @return Class
*/
public Class<? extends ValidateMethod> getMethodClass() {
return methodClass;
}
/**
* @description 创建校验方法实例
* @return ValidateMethod
*/
public ValidateMethod createInstance() {
return methodSupplier.get();
}
/**
* @description 根据名称获取枚举
* @param name 名称
* @return ValidateMethodType
*/
public static ValidateMethodType fromName(String name) {
for (ValidateMethodType type : values()) {
if (type.getName().equals(name)) {
return type;
}
}
return null;
}
}

View File

@@ -0,0 +1,64 @@
<?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>org.xyzh</groupId>
<artifactId>urban-lifeline</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>common-dto</module>
<module>common-core</module>
<module>common-auth</module>
<module>common-redis</module>
<module>common-utils</module>
<module>common-all</module>
</modules>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-all</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-auth</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-core</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-dto</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-redis</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-utils</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>