chore: initial commit
Some checks failed
Synchronize to Gitee / repo-sync (push) Has been cancelled
Typos Checking / Spell Check with Typos (push) Has been cancelled

This commit is contained in:
2026-06-23 11:56:23 +08:00
commit 72e2110987
2883 changed files with 367388 additions and 0 deletions

180
backend/framework/README.md Executable file
View File

@@ -0,0 +1,180 @@
# 框架通用模块使用说明
## 目录
- [概述](#概述)
- [模块结构](#模块结构)
- [使用指南](#使用指南)
- [AOP 支持](#aop-支持)
- [公共功能](#公共功能)
- [MyBatis 配置](#mybatis-配置)
- [文件处理](#文件处理)
- [安全性管理](#安全性管理)
## 概述
本框架是一个企业级应用开发基础设施,提供了多种常用功能模块,包括 AOP 支持、公共工具类、MyBatis 增强、文件处理、安全性管理等。框架旨在简化开发流程,提高代码质量和开发效率。
## 模块结构
```
framework/
├── aop/ # AOP 相关功能
├── common/ # 公共功能
├── config/ # 配置相关
├── file/ # 文件处理
├── mybatis/ # MyBatis 增强
└── security/ # 安全性管理
```
## 使用指南
### AOP 支持
AOP 模块提供了面向切面编程的基础设施,包含以下子模块:
| 子模块 | 说明 |
|--------|------|
| annotation | 定义 AOP 功能的注解 |
| aspect | 实现切面逻辑 |
| builder | 提供构建器模式工具 |
| constants | 定义常量,避免硬编码 |
| dto | 数据传输对象 |
| event | 定义 AOP 相关事件 |
#### 使用示例
##### 1. 基础日志记录
```java
@OperationLog(
module = LogModule.SYSTEM,
type = LogType.ADD,
operator = "{{#user.name}}",
resourceId = "{{#user.id}}",
success = "添加用户成功",
extra = "{{#newUser}}"
)
public void addUser(User user) {
// 业务代码
// 添加日志上下文
OperationLogContext.putVariable("newUser", LogExtraDTO.builder()
.originalValue(null)
.modifiedValue(user)
.build());
}
```
说明:
- `resourceId`:业务资源的 ID
- `success`:方法调用成功后记录的日志内容
- 双大括号 `{{}}` 中的内容是 SpEL 表达式,支持调用静态方法、三目表达式等
##### 2. 成功/失败日志记录
```java
@OperationLog(
fail = "业务操作失败,失败原因:「{{#_errorMsg}}」",
success = "业务操作成功",
operator = "{{#user.name}}",
type = LogType.ADD,
resourceId = "{{#biz.id}}"
)
public boolean create(BizObj obj) {
OperationLogContext.putVariable("innerOrder", LogExtraDTO.builder()
.originalValue(obj)
.modifiedValue(obj)
.build());
return true;
}
```
- `#_errorMsg` 是方法抛出异常后自动获取的错误信息
##### 3. 多条日志记录
```java
@OperationLog(
module = LogModule.SYSTEM,
type = LogType.UPDATE,
operator = "{{#user.name}}",
resourceId = "{{#user.id}}",
success = "更新用户基本信息成功",
extra = "{{#upUser}}"
)
@OperationLog(
module = LogModule.SYSTEM,
type = LogType.UPDATE,
operator = "{{#user.name}}",
resourceId = "{{#user.id}}",
success = "更新用户权限成功",
extra = "{{#upPermission}}"
)
public void updateUser(User user) {
// 更新用户
User preUser = userMapper.selectByPrimaryKey(user.getId());
// 添加日志上下文
OperationLogContext.putVariable("upUser", LogExtraDTO.builder()
.originalValue(preUser)
.modifiedValue(user)
.build());
}
```
### 公共功能
公共功能模块提供了常用工具和基础功能:
- **constants**:常量定义,避免魔法值
- **exception**:自定义异常体系
- **groups**:分组管理
- **pager**:分页查询功能
- **response**:统一响应格式
- **uid**:唯一标识符生成
- **util**:工具类(包含加密、编码等)
### MyBatis 配置
MyBatis 配置模块增强了 MyBatis 的功能:
- **interceptor**:自定义拦截器
- **lambda**Lambda 表达式支持,简化查询构建
#### 使用示例
```java
@Resource
private BaseMapper<User> userMapper;
// Lambda 查询示例
List<User> userList = userMapper.selectListByLambda(
new LambdaQueryWrapper<User>()
.eq(User::getId, "admin")
);
```
自动生成的 SQL
```sql
SELECT * FROM user WHERE id = 'admin'
```
### 文件处理
文件处理模块提供了文件操作相关功能:
- **engine**:文件引擎,支持文件上传、下载、存储等操作
### 安全性管理
安全性管理模块提供了身份验证、授权和加密等功能:
- 身份验证
- 权限控制
- 数据加密
- 安全审计
## 更多信息
更多详细信息和高级用法,请参阅各模块的详细文档或源代码注释。

14
backend/framework/pom.xml Normal file
View File

@@ -0,0 +1,14 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.cordys</groupId>
<artifactId>backend</artifactId>
<version>${revision}</version>
</parent>
<artifactId>framework</artifactId>
<version>${revision}</version>
</project>

View File

@@ -0,0 +1,48 @@
package cn.cordys.aspectj.annotation;
import cn.cordys.aspectj.constants.LogModule;
import cn.cordys.aspectj.constants.LogType;
import java.lang.annotation.*;
/**
* @author mr.zhao
*/
@Repeatable(OperationLogs.class)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface OperationLog {
/**
* @return 日志的操作人
* 没填,默认从 session 中获取
*/
String operator() default "";
/**
* @return 操作日志的类型,如:新增、修改、删除
* {@link LogType}
*/
String type();
/**
* @return 业务模块名
* {@link LogModule}
*/
String module() default "";
/**
* @return 日志绑定的业务标识
* 可以在注解中设置,也可以在 LogContextInfo 中设置
* 优先级低于 LogContextInfo
*/
String resourceId() default "";
/**
* @return 操作对象的名称
* 可以在注解中设置,也可以在 LogContextInfo 中设置
* 优先级低于 LogContextInfo
*/
String resourceName() default "";
}

View File

@@ -0,0 +1,12 @@
package cn.cordys.aspectj.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface OperationLogs {
OperationLog[] value();
}

View File

@@ -0,0 +1,78 @@
package cn.cordys.aspectj.aop;
import lombok.Setter;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.core.Ordered;
import org.springframework.util.CollectionUtils;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
/**
* BeanFactoryLogRecordAdvisor with LogRecordPointcut
* <p>
* 结合了日志记录切点与通知的定义,提供一个完整的日志记录拦截功能。
* 通过配置 `LogRecordOperationSource` 和自定义的 `LogRecordPointcut` 来判断
* 哪些方法需要进行日志记录操作。
* </p>
*/
@Setter
public class OperationLogAopAdvisor extends AbstractBeanFactoryPointcutAdvisor implements Serializable, Ordered {
/**
* 日志记录操作源,用于获取方法的日志记录操作配置。
*/
private OperationLogSource operationLogSource;
/**
* 获取切面优先级,值越小优先级越高
* 设置较高优先级,确保 @OperationLog 切面在其他切面之前执行
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 10;
}
/**
* 获取日志记录的切点,用于定义拦截哪些方法。
*
* @return 返回定义的切点对象
*/
@Override
public Pointcut getPointcut() {
return new LogRecordPointcut(operationLogSource);
}
/**
* 自定义日志记录切点,用于判断哪些方法需要进行日志记录操作。
*/
private static class LogRecordPointcut extends StaticMethodMatcherPointcut implements Serializable {
private final OperationLogSource operationLogSource;
public LogRecordPointcut(OperationLogSource operationLogSource) {
this.operationLogSource = operationLogSource;
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
// 定义支持通配符的包名模式
// todo: 后续需要修改
String packagePattern = "cn.cordys\\..*\\.service";
// 获取目标类的包名
String targetPackage = targetClass.getPackage().getName();
// 使用正则表达式匹配包名
if (!Pattern.matches(packagePattern, targetPackage)) {
return false;
}
// 检查方法是否有日志操作配置
return !CollectionUtils.isEmpty(operationLogSource.computeLogRecordOperations(method, targetClass));
}
}
}

View File

@@ -0,0 +1,151 @@
package cn.cordys.aspectj.aop;
import cn.cordys.aspectj.annotation.OperationLog;
import cn.cordys.aspectj.annotation.OperationLogs;
import cn.cordys.aspectj.builder.OperationLogBuilder;
import cn.cordys.aspectj.context.OperationLogContext;
import cn.cordys.aspectj.dto.LogContextInfo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;
/**
* 该类负责解析和计算基于注解的日志记录操作。
*/
public class OperationLogSource {
/**
* 缓存方法与其对应接口方法的映射关系。
*/
private static final Map<Method, Method> INTERFACE_METHOD_CACHE = new ConcurrentReferenceHashMap<>(256);
/**
* 确定给定方法对应的接口方法(如果可能)。
*
* @param method 要处理的方法
*
* @return 对应的接口方法,如果不存在则返回原方法
*/
public static Method getInterfaceMethodIfPossible(Method method) {
if (!Modifier.isPublic(method.getModifiers()) || method.getDeclaringClass().isInterface()) {
return method;
}
return INTERFACE_METHOD_CACHE.computeIfAbsent(method, key -> {
Class<?> current = key.getDeclaringClass();
while (current != null && current != Object.class) {
for (Class<?> ifc : current.getInterfaces()) {
try {
return ifc.getMethod(key.getName(), key.getParameterTypes());
} catch (NoSuchMethodException ex) {
// 忽略异常
}
}
current = current.getSuperclass();
}
return key;
});
}
/**
* 根据指定的方法和目标类计算日志记录操作。
*
* @param method 要分析的方法
* @param targetClass 目标类
*
* @return 日志记录构建器的集合
*/
public Collection<OperationLogBuilder> computeLogRecordOperations(Method method, Class<?> targetClass) {
if (!Modifier.isPublic(method.getModifiers())) {
return Collections.emptyList();
}
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
Collection<OperationLogBuilder> logRecordOps = parseLogRecordAnnotations(specificMethod);
Collection<OperationLogBuilder> logRecordsOps = parseLogRecordsAnnotations(specificMethod);
Collection<OperationLogBuilder> abstractLogRecordOps = parseLogRecordAnnotations(getInterfaceMethodIfPossible(method));
Collection<OperationLogBuilder> abstractLogRecordsOps = parseLogRecordsAnnotations(getInterfaceMethodIfPossible(method));
Set<OperationLogBuilder> result = new HashSet<>();
result.addAll(logRecordOps);
result.addAll(abstractLogRecordOps);
result.addAll(logRecordsOps);
result.addAll(abstractLogRecordsOps);
return result;
}
/**
* 解析方法上的 {@code LogRecords} 注解。
*
* @param ae 被注解的元素
*
* @return 日志记录构建器的集合
*/
private Collection<OperationLogBuilder> parseLogRecordsAnnotations(AnnotatedElement ae) {
return AnnotatedElementUtils.findAllMergedAnnotations(ae, OperationLogs.class).stream()
.flatMap(operationLogs -> Arrays.stream(operationLogs.value())
.map(logRecord -> parseLogRecordAnnotation(ae, logRecord)))
.collect(Collectors.toList());
}
/**
* 解析方法上的 {@code LogRecord} 注解。
*
* @param ae 被注解的元素
*
* @return 日志记录构建器的集合
*/
private Collection<OperationLogBuilder> parseLogRecordAnnotations(AnnotatedElement ae) {
return AnnotatedElementUtils.findAllMergedAnnotations(ae, OperationLog.class).stream()
.map(recordAnnotation -> parseLogRecordAnnotation(ae, recordAnnotation))
.collect(Collectors.toList());
}
/**
* 将 {@code LogRecord} 注解转换为日志记录构建器。
*
* @param ae 被注解的元素
* @param recordAnnotation {@code LogRecord} 注解
*
* @return 日志记录构建器
*/
private OperationLogBuilder parseLogRecordAnnotation(AnnotatedElement ae, OperationLog recordAnnotation) {
return OperationLogBuilder.builder()
.resourceName(recordAnnotation.resourceName())
.type(recordAnnotation.type())
.resourceId(recordAnnotation.resourceId())
.operatorId(recordAnnotation.operator())
.subType(recordAnnotation.module())
.build();
}
/**
* 验证日志记录操作配置是否合法。
*
* @param recordOps 日志记录构建器
*
* @throws IllegalStateException 如果配置不合法
*/
public boolean isLogRecordOperationValidated(OperationLogBuilder recordOps) {
LogContextInfo extra = OperationLogContext.getContext();
String resourceName = recordOps.getResourceName();
String resourceId = recordOps.getResourceId();
if (extra != null && StringUtils.isNotBlank(extra.getResourceName())) {
resourceName = extra.getResourceName();
}
if (extra != null && StringUtils.isNotBlank(extra.getResourceId())) {
resourceId = extra.getResourceId();
}
return !StringUtils.isBlank(resourceName) && !StringUtils.isBlank(resourceId);
}
}

View File

@@ -0,0 +1,201 @@
package cn.cordys.aspectj.aop;
import cn.cordys.aspectj.builder.MethodExecuteResult;
import cn.cordys.aspectj.builder.OperationLog;
import cn.cordys.aspectj.builder.OperationLogBuilder;
import cn.cordys.aspectj.builder.parse.OperationLogValueParser;
import cn.cordys.aspectj.constants.CodeVariableType;
import cn.cordys.aspectj.context.OperationLogContext;
import cn.cordys.aspectj.handler.OperationLogService;
import cn.cordys.common.util.CommonBeanFactory;
import cn.cordys.security.SessionUtils;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.util.CollectionUtils;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
/**
* 日志记录拦截器,拦截方法执行并生成日志记录。
* <p>
* 该类支持基于注解的日志模板解析,能够在方法执行的前后记录业务操作日志。
* </p>
*/
@Slf4j
public class OperationOperationLogInterceptor extends OperationLogValueParser implements MethodInterceptor, Serializable, SmartInitializingSingleton {
@Setter
private OperationLogSource operationLogSource;
private OperationLogService operationLogService;
/**
* 拦截方法执行,进行日志记录逻辑的处理。
*
* @param invocation 方法调用上下文
*
* @return 方法执行结果
*
* @throws Throwable 执行过程中的异常
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
return execute(invocation, invocation.getThis(), method, invocation.getArguments());
}
/**
* 核心执行逻辑:完成方法调用并处理日志记录。
*
* @param invoker 方法调用上下文
* @param target 目标对象
* @param method 目标方法
* @param args 方法参数
*
* @return 方法的返回结果
*
* @throws Throwable 执行过程中的异常
*/
private Object execute(MethodInvocation invoker, Object target, Method method, Object[] args) throws Throwable {
if (AopUtils.isAopProxy(target)) {
return invoker.proceed();
}
Class<?> targetClass = getTargetClass(target);
Object ret = null;
MethodExecuteResult methodExecuteResult = new MethodExecuteResult(method, args, targetClass);
OperationLogContext.putEmptySpan();
Collection<OperationLogBuilder> operations = new ArrayList<>();
try {
operations = operationLogSource.computeLogRecordOperations(method, targetClass);
} catch (Exception e) {
log.error("日志解析异常", e);
}
try {
ret = invoker.proceed();
methodExecuteResult.setResult(ret);
methodExecuteResult.setSuccess(true);
} catch (Exception e) {
methodExecuteResult.setSuccess(false);
methodExecuteResult.setThrowable(e);
methodExecuteResult.setErrorMsg(e.getMessage());
}
processLogRecords(methodExecuteResult, operations);
if (methodExecuteResult.getThrowable() != null) {
throw methodExecuteResult.getThrowable();
}
return ret;
}
/**
* 处理日志记录逻辑,根据方法执行结果生成操作日志。
*
* @param methodExecuteResult 方法执行结果
* @param operations 日志操作集合
*/
private void processLogRecords(MethodExecuteResult methodExecuteResult, Collection<OperationLogBuilder> operations) {
if (CollectionUtils.isEmpty(operations)) {
return;
}
for (OperationLogBuilder operation : operations) {
try {
if (!operationLogSource.isLogRecordOperationValidated(operation)) {
continue;
}
if (methodExecuteResult.isSuccess()) {
handleSuccessLog(methodExecuteResult, operation);
}
} catch (Exception e) {
log.error("日志执行异常", e);
}
}
}
/**
* 处理成功的日志记录。
*
* @param methodExecuteResult 方法执行结果
* @param operation 日志操作信息
*/
private void handleSuccessLog(MethodExecuteResult methodExecuteResult, OperationLogBuilder operation) {
List<String> templates = getSpElTemplates(operation);
String operatorId = resolveOperatorId(operation, templates);
Map<String, String> expressionValues = processTemplate(templates, methodExecuteResult);
saveLogRecord(methodExecuteResult.getMethod(), operation, operatorId, expressionValues);
}
/**
* 保存日志记录。
*
* @param method 方法对象
* @param operation 日志操作信息
* @param operatorId 操作人 ID
* @param expressionValues 模板解析后的值
*/
private void saveLogRecord(Method method, OperationLogBuilder operation, String operatorId, Map<String, String> expressionValues) {
OperationLog operationLog = OperationLog.builder()
.type(expressionValues.get(operation.getType()))
.resourceId(expressionValues.get(operation.getResourceId()))
.resourceName(expressionValues.get(operation.getResourceName()))
.operator(expressionValues.get(operatorId))
.subType(expressionValues.get(operation.getSubType()))
.codeVariable(resolveCodeVariable(method))
.createTime(new Date())
.build();
operationLogService.record(operationLog);
}
private Map<CodeVariableType, Object> resolveCodeVariable(Method method) {
return Map.of(
CodeVariableType.ClassName, method.getDeclaringClass(),
CodeVariableType.MethodName, method.getName()
);
}
private String resolveOperatorId(OperationLogBuilder operation, List<String> templates) {
if (StringUtils.isEmpty(operation.getOperatorId())) {
// 如果没有标注操作人,则获取登入用户
operation.setOperatorId(SessionUtils.getUserId());
}
templates.add(operation.getOperatorId());
return operation.getOperatorId();
}
private Class<?> getTargetClass(Object target) {
return AopProxyUtils.ultimateTargetClass(target);
}
private List<String> getSpElTemplates(OperationLogBuilder operation) {
List<String> spElTemplates = new ArrayList<>();
spElTemplates.add(operation.getType());
spElTemplates.add(operation.getResourceId());
spElTemplates.add(operation.getResourceName());
spElTemplates.add(operation.getSubType());
return spElTemplates.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@Override
public void afterSingletonsInstantiated() {
operationLogService = CommonBeanFactory.getBean(OperationLogService.class);
}
}

View File

@@ -0,0 +1,68 @@
package cn.cordys.aspectj.builder;
import cn.cordys.aspectj.dto.LogDTO;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* <p>用于构建 {@link LogDTO} 的建造者类。</p>
* <p>通过 {@link Builder} 模式构建 {@link LogDTO} 对象,确保日志数据的完整性和一致性。</p>
*/
@Getter
@Setter
@Builder
public class LogDTOBuilder {
/**
* 组织ID
*/
private String organizationId;
/**
* 来源ID
*/
private String sourceId;
/**
* 创建用户
*/
private String createUser;
/**
* 日志类型
*/
private String type;
/**
* 方法名
*/
private String method;
/**
* 模块名
*/
private String module;
/**
* 日志内容
*/
private String content;
/**
* 请求路径
*/
private String path;
/**
* 构建 {@link LogDTO} 对象
*
* @return {@link LogDTO} 对象
*/
public LogDTO getLogDTO() {
LogDTO logDTO = new LogDTO(organizationId, sourceId, createUser, type, module, content);
logDTO.setPath(path);
logDTO.setMethod(method);
return logDTO;
}
}

View File

@@ -0,0 +1,28 @@
package cn.cordys.aspectj.builder;
import lombok.Getter;
import lombok.Setter;
import java.lang.reflect.Method;
@Getter
public class MethodExecuteResult {
private final Method method;
private final Object[] args;
private final Class<?> targetClass;
@Setter
private boolean success;
@Setter
private Throwable throwable;
@Setter
private String errorMsg;
@Setter
private Object result;
public MethodExecuteResult(Method method, Object[] args, Class<?> targetClass) {
this.method = method;
this.args = args;
this.targetClass = targetClass;
}
}

View File

@@ -0,0 +1,79 @@
package cn.cordys.aspectj.builder;
import cn.cordys.aspectj.constants.CodeVariableType;
import jakarta.validation.constraints.NotBlank;
import lombok.*;
import org.hibernate.validator.constraints.Length;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class OperationLog {
/**
* id
*/
private Serializable id;
/**
* 保存的操作日志的类型,比如:订单类型、商品类型
**/
@NotBlank(message = "type required")
@Length(max = 200, message = "type max length is 200")
private String type;
/**
* 日志的子类型比如订单的C端日志和订单的B端日志type都是订单类型但是子类型不一样
*/
private String subType;
/**
* 日志绑定的业务标识
*/
@NotBlank(message = "resourceId required")
@Length(max = 100, message = "resourceId max length is 100")
private String resourceId;
/**
* 操作对象的名称
*/
@NotBlank(message = "resourceName name")
private String resourceName;
/**
* 日志详情
*/
@NotBlank(message = "日志详情(非对比的日志详情)")
private String detail;
/**
* 操作人
*/
@NotBlank(message = "operator required")
@Length(max = 50, message = "operator max length 50")
private String operator;
/**
* 日志内容
*/
@NotBlank(message = "opAction required")
@Length(max = 500, message = "operator max length 500")
private String action;
/**
* 日志的创建时间
*/
private Date createTime;
/**
* 打印日志的代码信息
* CodeVariableType 日志记录的ClassName、MethodName
*/
private Map<CodeVariableType, Object> codeVariable;
}

View File

@@ -0,0 +1,15 @@
package cn.cordys.aspectj.builder;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class OperationLogBuilder {
private String resourceId;
private String resourceName;
private String operatorId;
private String type;
private String subType;
private String extra;
}

View File

@@ -0,0 +1,52 @@
package cn.cordys.aspectj.builder.parse;
import cn.cordys.aspectj.context.OperationLogContext;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.ParameterNameDiscoverer;
import java.lang.reflect.Method;
import java.util.Map;
/**
* 自定义的日志记录表达式上下文类,继承自 {@link MethodBasedEvaluationContext},用于处理方法日志记录时的上下文。
* 该类在构造时会将方法参数、返回值、错误信息以及全局变量和当前上下文的变量注入到表达式上下文中,
* 以便在日志记录时进行动态解析。
*/
public class OperationLogEvaluationContext extends MethodBasedEvaluationContext {
/**
* 构造方法,初始化日志记录表达式上下文。
*
* @param method 被调用的方法
* @param arguments 方法参数
* @param parameterNameDiscoverer 参数名称发现器
* @param ret 方法返回值
* @param errorMsg 错误信息
*/
public OperationLogEvaluationContext(Object target, Method method, Object[] arguments,
ParameterNameDiscoverer parameterNameDiscoverer, Object ret, String errorMsg) {
// 调用父类构造方法初始化
super(target, method, arguments, parameterNameDiscoverer);
// 获取日志记录上下文中的变量
Map<String, Object> variables = OperationLogContext.getVariables();
Map<String, Object> globalVariables = OperationLogContext.getGlobalVariableMap();
// 设置当前上下文中的变量
setVariables(variables);
// 如果全局变量不为空,将其添加到上下文中
if (!globalVariables.isEmpty()) {
for (Map.Entry<String, Object> entry : globalVariables.entrySet()) {
// 如果当前上下文中没有该变量,则设置该变量
if (lookupVariable(entry.getKey()) == null) {
setVariable(entry.getKey(), entry.getValue());
}
}
}
// 设置返回值和错误信息变量
setVariable("_ret", ret);
setVariable("_errorMsg", errorMsg);
}
}

View File

@@ -0,0 +1,50 @@
package cn.cordys.aspectj.builder.parse;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class OperationLogExpressionEvaluator extends CachedExpressionEvaluator {
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
private final Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);
public Object parseExpression(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
return getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, Object.class);
}
/**
* Create an {@link EvaluationContext}.
*
* @param method the method
* @param args the method arguments
* @param targetClass the target class
* @param result the return value (can be {@code null}) or
* @param errorMsg errorMsg
* @param beanFactory Spring beanFactory
*
* @return the evaluation context
*/
public EvaluationContext createEvaluationContext(Method method, Object[] args, Class<?> targetClass,
Object result, String errorMsg, BeanFactory beanFactory) {
Method targetMethod = getTargetMethod(targetClass, method);
OperationLogEvaluationContext evaluationContext = new OperationLogEvaluationContext(targetClass, targetMethod, args, getParameterNameDiscoverer(), result, errorMsg);
if (beanFactory != null) {
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
return evaluationContext;
}
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
return targetMethodCache.computeIfAbsent(methodKey, k -> AopUtils.getMostSpecificMethod(method, targetClass));
}
}

View File

@@ -0,0 +1,57 @@
package cn.cordys.aspectj.builder.parse;
import cn.cordys.aspectj.builder.MethodExecuteResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 解析需要存储的日志里面的SpeEL表达式
*/
public class OperationLogValueParser implements BeanFactoryAware {
private static final Pattern pattern = Pattern.compile("\\{\\s*(.*?)\\s*\\}");
private final OperationLogExpressionEvaluator expressionEvaluator = new OperationLogExpressionEvaluator();
protected BeanFactory beanFactory;
public Map<String, String> processTemplate(Collection<String> templates, MethodExecuteResult methodExecuteResult) {
Map<String, String> expressionValues = new HashMap<>();
EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(methodExecuteResult.getMethod(),
methodExecuteResult.getArgs(), methodExecuteResult.getTargetClass(), methodExecuteResult.getResult(),
methodExecuteResult.getErrorMsg(), beanFactory);
for (String expressionTemplate : templates) {
if (expressionTemplate.contains("{")) {
Matcher matcher = pattern.matcher(expressionTemplate);
StringBuilder parsedStr = new StringBuilder();
AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(methodExecuteResult.getMethod(), methodExecuteResult.getTargetClass());
while (matcher.find()) {
String expression = matcher.group(1);
Object value = expressionEvaluator.parseExpression(expression, annotatedElementKey, evaluationContext);
matcher.appendReplacement(parsedStr, Matcher.quoteReplacement(value == null ? StringUtils.EMPTY : value.toString()));
}
matcher.appendTail(parsedStr);
expressionValues.put(expressionTemplate, parsedStr.toString());
} else {
expressionValues.put(expressionTemplate, expressionTemplate);
}
}
return expressionValues;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}

View File

@@ -0,0 +1,66 @@
package cn.cordys.aspectj.config;
import cn.cordys.aspectj.aop.OperationLogAopAdvisor;
import cn.cordys.aspectj.aop.OperationLogSource;
import cn.cordys.aspectj.aop.OperationOperationLogInterceptor;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.*;
import org.springframework.core.type.AnnotationMetadata;
/**
* 日志记录配置类,默认启用日志记录功能。
* 不依赖 @EnableLogRecord 注解,直接配置日志记录相关的切面、拦截器等。
*/
@Configuration
public class OperationLogConfig implements ImportAware {
/**
* 创建 LogRecordOperationSource 实例,提供日志记录操作的元数据。
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public OperationLogSource logRecordOperationSource() {
return new OperationLogSource();
}
/**
* 创建 LogRecordAopAdvisor 实例,配置日志记录的切面和拦截器。
*
* @param operationLogInterceptor LogRecordInterceptor 实例
*
* @return LogRecordAopAdvisor 实例
*/
@DependsOn("logRecordInterceptor")
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public OperationLogAopAdvisor logRecordAdvisor(OperationOperationLogInterceptor operationLogInterceptor) {
OperationLogAopAdvisor advisor = new OperationLogAopAdvisor();
advisor.setOperationLogSource(logRecordOperationSource());
advisor.setAdvice(operationLogInterceptor);
return advisor;
}
/**
* 创建 LogRecordInterceptor 实例,配置日志记录的拦截器。
*
* @return LogRecordInterceptor 实例
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public OperationOperationLogInterceptor logRecordInterceptor() {
OperationOperationLogInterceptor interceptor = new OperationOperationLogInterceptor();
interceptor.setOperationLogSource(logRecordOperationSource());
return interceptor;
}
/**
* 设置导入的元数据,在此处读取日志记录相关的配置。
*
* @param importMetadata 导入的元数据
*/
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
// 如果需要在导入时读取其他的配置信息,可以在这里处理
// log.info("Log record configuration is enabled by default");
}
}

View File

@@ -0,0 +1,6 @@
package cn.cordys.aspectj.constants;
public enum CodeVariableType {
ClassName,
MethodName
}

View File

@@ -0,0 +1,39 @@
package cn.cordys.aspectj.constants;
/**
* 全局搜索
*/
public class GlobalSearchModule {
/**
* 客户
*/
public static final String CUSTOMER = "CUSTOMER";
/**
* 线索-线索
*/
public static final String CLUE = "CLUE";
/**
* 线索-线索池
*/
public static final String CLUE_POOL = "CLUE_MANAGEMENT";
/**
* 客户联系人
*/
public static final String CUSTOMER_CONTACT = "CUSTOMER_CONTACT";
/**
* 商机
*/
public static final String OPPORTUNITY = "OPPORTUNITY";
/**
* 客户公海
*/
public static final String CUSTOMER_POOL = "CUSTOMER_POOL";
}

View File

@@ -0,0 +1,10 @@
package cn.cordys.aspectj.constants;
public class LogConstants {
public static final String SYSTEM = "SYSTEM";
public static final String ORGANIZATION = "ORGANIZATION";
public static final String PRE_VALIDATION = "PRE_VALIDATION";
public static final String POST_VALIDATION = "POST_VALIDATION";
}

View File

@@ -0,0 +1,161 @@
package cn.cordys.aspectj.constants;
/**
* 系统日志模块常量类。
* 用于定义不同模块的名称,便于日志记录时进行分类。
*/
public class LogModule {
/**
* 系统管理模块
*/
public static final String SYSTEM = "SYSTEM";
/**
* 消息通知
*/
public static final String SYSTEM_MESSAGE_MESSAGE = "SYSTEM_MESSAGE_MESSAGE";
/**
* 邮件设置
*/
public static final String SYSTEM_BUSINESS_MAIL = "SYSTEM_BUSINESS_MAIL";
/**
* 认证设置
*/
public static final String SYSTEM_BUSINESS_AUTH = "SYSTEM_BUSINESS_AUTH";
/**
* 三方设置
*/
public static final String SYSTEM_BUSINESS_THIRD = "SYSTEM_BUSINESS_THIRD";
/**
* 界面设置
*/
public static final String SYSTEM_BUSINESS_UI = "SYSTEM_BUSINESS_UI";
/**
* 用户
*/
public static final String SYSTEM_USER = "SYSTEM_USER";
/**
* 公告
*/
public static final String SYSTEM_MESSAGE_ANNOUNCEMENT = "SYSTEM_MESSAGE_ANNOUNCEMENT";
/**
* 组织架构
*/
public static final String SYSTEM_ORGANIZATION = "SYSTEM_ORGANIZATION";
/**
* 角色权限
*/
public static final String SYSTEM_ROLE = "SYSTEM_ROLE";
/**
* 个人信息模块API密钥
*/
public static final String PERSONAL_INFORMATION_APIKEY = "PERSONAL_INFORMATION_APIKEY";
/**
* 模块配置
*/
public static final String SYSTEM_MODULE = "SYSTEM_MODULE";
/**
* 客户
*/
public static final String CUSTOMER_INDEX = "CUSTOMER_INDEX";
/**
* 线索-线索
*/
public static final String CLUE_INDEX = "CLUE_MANAGEMENT_CLUE";
/**
* 线索-线索池
*/
public static final String CLUE_POOL_INDEX = "CLUE_MANAGEMENT_POOL";
/**
* 客户联系人
*/
public static final String CUSTOMER_CONTACT = "CUSTOMER_CONTACT";
//TODO start 暂定跟进记录模块常量
/**
* 跟进记录
*/
public static final String FOLLOW_UP_RECORD = "FOLLOW_UP_RECORD";
/**
* 跟进计划
*/
public static final String FOLLOW_UP_PLAN = "FOLLOW_UP_PLAN";
/**
* 商机
*/
public static final String OPPORTUNITY_INDEX = "OPPORTUNITY_INDEX";
/**
* 商机报价
*/
public static final String OPPORTUNITY_QUOTATION = "OPPORTUNITY_QUOTATION";
//todo end
// 可以根据需要扩展其他模块常量
/**
* 产品
*/
public static final String PRODUCT_MANAGEMENT = "PRODUCT_MANAGEMENT_PRO";
/**
* 产品价格表
*/
public static final String PRODUCT_PRICE_MANAGEMENT = "PRODUCT_MANAGEMENT_PRICE";
/**
* 客户公海
*/
public static final String CUSTOMER_POOL = "CUSTOMER_POOL";
/**
* 仪表板
*/
public static final String DASHBOARD = "DASHBOARD";
public static final String AGENT = "AGENT";
public static final String CONTRACT_INDEX = "CONTRACT_INDEX";
/**
* 合同回款计划
*/
public static final String CONTRACT_PAYMENT = "CONTRACT_PAYMENT";
/**
* 合同回款记录
*/
public static final String CONTRACT_PAYMENT_RECORD = "CONTRACT_PAYMENT_RECORD";
/**
* 发票
*/
public static final String CONTRACT_INVOICE = "CONTRACT_INVOICE";
/**
* 工商抬头
*/
public static final String CONTRACT_BUSINESS_TITLE = "CONTRACT_BUSINESS_TITLE";
/**
* 订单
*/
public static final String ORDER_INDEX = "ORDER_INDEX";
public static final String APPROVAL_FLOW = "SYSTEM_PROCESS_APPROVAL";
/**
* 自定义表单
*/
public static final String CUSTOM_FORM = "CUSTOM_FORM_INDEX";
/**
* 自定义表单数据
*/
public static final String CUSTOM_FORM_DATA = "CUSTOM_FORM_DATA";
}

View File

@@ -0,0 +1,118 @@
package cn.cordys.aspectj.constants;
/**
* 操作日志类型枚举类。
* 用于定义不同操作的日志类型,便于日志分类和处理。
*/
public final class LogType {
public static final String SELECT = "SELECT";
/**
* 添加操作
*/
public static final String ADD = "ADD";
/**
* 删除操作
*/
public static final String DELETE = "DELETE";
/**
* 更新操作
*/
public static final String UPDATE = "UPDATE";
/**
* 移入公海
*/
public static final String MOVE_TO_CUSTOMER_POOL = "MOVE_TO_CUSTOMER_POOL";
/**
* 审核操作
*/
public static final String REVIEW = "REVIEW";
/**
* 登出操作
*/
public static final String LOGOUT = "LOGOUT";
/**
* 登录操作
*/
public static final String LOGIN = "LOGIN";
/**
* 复制操作
*/
public static final String COPY = "COPY";
/**
* 同步
*/
public static final String SYNC = "SYNC";
/**
* 领取
*/
public static final String PICK = "PICK";
/**
* 分配
*/
public static final String ASSIGN = "ASSIGN";
/**
* 取消
*/
public static final String CANCEL = "CANCEL";
/**
* 添加用户
*/
public static final String ADD_USER = "ADD_USER";
/**
* 移除用户
*/
public static final String REMOVE_USER = "REMOVE_USER";
/**
* 导出
*/
public static final String EXPORT = "EXPORT";
/**
* 合并
*/
public static final String MERGE = "MERGE";
/**
* 审核
*/
public static final String APPROVAL = "APPROVAL";
/**
* 作废
*/
public static final String VOIDED = "VOIDED";
/**
* 归档
*/
public static final String ARCHIVE = "ARCHIVE";
/**
* 取消归档
*/
public static final String UNARCHIVE = "UNARCHIVE";
/**
* 下载
*/
public static final String DOWNLOAD = "DOWNLOAD";
private LogType() {
// 私有构造函数防止实例化
}
/**
* 判断给定的日志类型是否包含关键字。
*
* @param logType 当前日志类型
* @param keyword 待匹配的日志类型关键字
*
* @return 如果日志类型包含关键字,则返回 true否则返回 false
*/
public static boolean contains(String logType, String keyword) {
if (logType == null || keyword == null) {
return false;
}
return logType.contains(keyword);
}
}

View File

@@ -0,0 +1,5 @@
package cn.cordys.aspectj.constants;
public enum RequestSource {
API, WEB, SKILL, MCP
}

View File

@@ -0,0 +1,177 @@
package cn.cordys.aspectj.context;
import cn.cordys.aspectj.dto.LogContextInfo;
import java.util.*;
/**
* 用于记录日志上下文变量的工具类,支持方法级别和全局变量的管理。
* <p>
* 本类使用了 {@link InheritableThreadLocal},以确保子线程能够继承父线程的变量。
* </p>
*/
public class OperationLogContext {
public static final String OPERATION_LOG_CONTEXT_KEY = "OPERATION_LOG_CONTEXT_KEY";
/**
* 存储方法级别变量的栈,每个方法调用对应一个栈帧。
*/
private static final InheritableThreadLocal<Deque<Map<String, Object>>> VARIABLE_MAP_STACK = new InheritableThreadLocal<>();
/**
* 存储全局变量的映射。
*/
private static final InheritableThreadLocal<Map<String, Object>> GLOBAL_VARIABLE_MAP = new InheritableThreadLocal<>();
// 防止实例化工具类
private OperationLogContext() {
throw new IllegalStateException("Utility class");
}
/**
* 向当前方法栈中放入变量。
*
* @param name 变量名
* @param value 变量值
*/
public static void putVariable(String name, Object value) {
// 初始化栈
Deque<Map<String, Object>> mapStack = Optional.ofNullable(VARIABLE_MAP_STACK.get())
.orElseGet(() -> {
Deque<Map<String, Object>> stack = new ArrayDeque<>();
VARIABLE_MAP_STACK.set(stack);
return stack;
});
if (mapStack.isEmpty()) {
mapStack.push(new HashMap<>());
}
Objects.requireNonNull(mapStack.peek()).put(name, value);
}
/**
* 设置额外信息
*/
public static LogContextInfo getContext() {
return (LogContextInfo) getVariable(OPERATION_LOG_CONTEXT_KEY);
}
/**
* 设置额外信息
*/
public static void setContext(LogContextInfo logContextInfo) {
putVariable(OPERATION_LOG_CONTEXT_KEY, logContextInfo);
}
/**
* 设置资源名称
*
* @param resourceName
*/
public static void setResourceName(String resourceName) {
setContext(
LogContextInfo.builder()
.resourceName(resourceName)
.build()
);
}
/**
* 获取当前方法栈中指定的变量值。
*
* @param key 变量名
*
* @return 变量值,如果找不到则返回 null
*/
public static Object getVariable(String key) {
Map<String, Object> variableMap = Optional.ofNullable(VARIABLE_MAP_STACK.get())
.map(Deque::peek)
.orElse(null);
return variableMap == null ? null : variableMap.get(key);
}
/**
* 获取当前方法栈中或全局变量映射中指定的变量值。
* <p>
* 若在方法栈中找不到变量,则会尝试从全局变量映射中获取。
* </p>
*
* @param key 变量名
*
* @return 变量值,如果找不到则返回 null
*/
public static Object getMethodOrGlobal(String key) {
// 优先从方法栈中获取
Object result = Optional.ofNullable(VARIABLE_MAP_STACK.get())
.map(stack -> {
assert stack.peek() != null;
return stack.peek().get(key);
})
.orElse(null);
if (result == null) {
// 若方法栈中没有,则从全局变量中获取
result = Optional.ofNullable(GLOBAL_VARIABLE_MAP.get())
.map(map -> map.get(key))
.orElse(null);
}
return result;
}
/**
* 获取当前方法栈中的所有变量。
*
* @return 当前方法栈中的变量映射,若栈为空,则返回一个空的 HashMap
*/
public static Map<String, Object> getVariables() {
return Optional.ofNullable(VARIABLE_MAP_STACK.get())
.map(Deque::peek)
.orElse(new HashMap<>());
}
/**
* 获取全局变量映射。
*
* @return 全局变量映射,若未初始化,则返回一个空的 HashMap
*/
public static Map<String, Object> getGlobalVariableMap() {
return Optional.ofNullable(GLOBAL_VARIABLE_MAP.get())
.orElse(new HashMap<>());
}
/**
* 清除当前方法栈中的变量。
*/
public static void clear() {
Optional.ofNullable(VARIABLE_MAP_STACK.get()).ifPresent(Deque::pop);
}
/**
* 清除全局变量映射中的所有变量。
*/
public static void clearGlobal() {
Optional.ofNullable(GLOBAL_VARIABLE_MAP.get()).ifPresent(Map::clear);
}
/**
* 在进入方法时初始化一个空的 Span 放入栈中,方法执行完后会弹出。
* <p>
* 该方法通常用于日志追踪中的 Span 管理。
* </p>
*/
public static void putEmptySpan() {
Deque<Map<String, Object>> mapStack = Optional.ofNullable(VARIABLE_MAP_STACK.get())
.orElseGet(() -> {
Deque<Map<String, Object>> stack = new ArrayDeque<>();
VARIABLE_MAP_STACK.set(stack);
return stack;
});
mapStack.push(new HashMap<>());
if (GLOBAL_VARIABLE_MAP.get() == null) {
GLOBAL_VARIABLE_MAP.set(new HashMap<>());
}
}
}

View File

@@ -0,0 +1,45 @@
package cn.cordys.aspectj.dto;
import cn.cordys.common.util.JSON;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LogContextInfo implements java.io.Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 原始值
*/
private Object originalValue;
/**
* 修改后的值
*/
private Object modifiedValue;
/**
* 资源的id
* 优先级高于注解
*/
private String resourceId;
/**
* 资源的名称
* 优先级高于注解
*/
private String resourceName;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@@ -0,0 +1,107 @@
package cn.cordys.aspectj.dto;
import cn.cordys.common.groups.Created;
import cn.cordys.common.groups.Updated;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 日志数据传输对象DTO继承自操作日志OperationLog
* 用于封装操作日志的具体数据,包含变更前后内容、是否需要历史记录等信息。
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LogDTO {
@Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{operation_log.id.not_blank}", groups = {Updated.class})
@Size(min = 1, max = 50, message = "{operation_log.id.length_range}", groups = {Created.class, Updated.class})
private String id;
@Schema(description = "组织id", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{operation_log.organization_id.not_blank}", groups = {Created.class})
@Size(min = 1, max = 50, message = "{operation_log.organization_id.length_range}", groups = {Created.class, Updated.class})
private String organizationId;
@Schema(description = "操作时间")
private Long createTime;
@Schema(description = "操作人")
private String createUser;
@Schema(description = "资源id")
private String resourceId;
@Schema(description = "资源名称")
private String resourceName;
/**
* 无需对比的操作日志详情
* 例如可以移入公海
* 详情记录为:客户 xxx 移入公海 xxx
*/
@Schema(description = "日志详情")
private String detail;
@Schema(description = "操作方法", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{operation_log.method.not_blank}", groups = {Created.class})
@Size(min = 1, max = 255, message = "{operation_log.method.length_range}", groups = {Created.class, Updated.class})
private String method;
@Schema(description = "操作类型/add/update/delete", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{operation_log.type.not_blank}", groups = {Created.class})
@Size(min = 1, max = 20, message = "{operation_log.type.length_range}", groups = {Created.class, Updated.class})
private String type;
@Schema(description = "操作模块")
private String module;
@Schema(description = "操作路径")
private String path;
@Schema(description = "登录地")
private String loginAddress;
@Schema(description = "平台")
private String platform;
@Schema(description = "请求来源")
private String requestSource;
/**
* 原始值
*/
private Object originalValue;
/**
* 修改后的值
*/
private Object modifiedValue;
/**
* 默认构造函数
*/
public LogDTO() {
}
/**
* 带参构造函数,用于快速初始化日志数据
*
* @param organizationId 组织ID
* @param resourceId 数据源ID
* @param createUser 创建用户
* @param type 日志类型
* @param module 模块
*/
public LogDTO(String organizationId, String resourceId, String createUser, String type, String module, String resourceName) {
this.setOrganizationId(organizationId);
this.setResourceId(resourceId);
this.setCreateUser(createUser);
this.setType(type);
this.setModule(module);
this.setResourceName(resourceName);
this.setCreateTime(System.currentTimeMillis());
}
}

View File

@@ -0,0 +1,13 @@
package cn.cordys.aspectj.handler;
import cn.cordys.aspectj.dto.LogDTO;
public interface OperationLogHandler {
/**
* 处理日志
*
* @param operationLog 操作日志
*/
void handleLog(LogDTO operationLog);
}

View File

@@ -0,0 +1,83 @@
package cn.cordys.aspectj.handler;
import cn.cordys.aspectj.builder.OperationLog;
import cn.cordys.aspectj.constants.RequestSource;
import cn.cordys.aspectj.dto.LogDTO;
import cn.cordys.common.util.ServletUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
/**
* 操作日志 ILogRecordService 实现类
* <p>
* 基于 {@link OperationLogHandler} 实现,记录操作日志
*/
@Service
public class OperationLogService {
@Resource
private OperationLogHandler operationLogHandler;
public static final String ACCESS_KEY_HEADER = "X-Access-Key";
public static final String SECRET_KEY_HEADER = "X-Secret-Key";
public static final String REQUEST_SOURCE_HEADER = "X-Request-Source";
public static void fillModuleFields(LogDTO reqDTO, OperationLog operationLog) {
reqDTO.setCreateTime(System.currentTimeMillis());
reqDTO.setType(operationLog.getType()); // 大模块类型例如CRM 客户
reqDTO.setCreateUser(operationLog.getOperator());
reqDTO.setModule(operationLog.getSubType());// 操作类型CURD
reqDTO.setResourceId(operationLog.getResourceId()); // 资源id
reqDTO.setResourceName(operationLog.getResourceName()); // 资源名称
reqDTO.setDetail(operationLog.getDetail()); // 资源名称
}
private static void fillRequestFields(LogDTO reqDTO) {
// 获得 Request 对象
HttpServletRequest request = ServletUtils.getRequest();
if (request == null) {
return;
}
String accessKey = request.getHeader(ACCESS_KEY_HEADER);
String secretKey = request.getHeader(SECRET_KEY_HEADER);
String requestSource = request.getHeader(REQUEST_SOURCE_HEADER);
reqDTO.setRequestSource(resolveRequestSource(requestSource, accessKey, secretKey));
// 补全请求信息
reqDTO.setMethod(request.getMethod());
reqDTO.setPath(request.getRequestURI());
}
public void record(OperationLog operationLog) {
// 1. 补全通用字段
LogDTO reqDTO = new LogDTO();
// 补全模块信息
fillModuleFields(reqDTO, operationLog);
// 补全请求信息
fillRequestFields(reqDTO);
// todo 组织或项目信息
// 2. 异步记录日志
assert operationLogHandler != null;
operationLogHandler.handleLog(reqDTO);
}
/**
* 根据请求头解析请求来源,优先级:
* 1. 明确指定的 X-Request-Source
* 2. 携带鉴权密钥对 (Access/Secret) 时视为 API 调用
* 3. 否则默认为 WEB
*/
private static String resolveRequestSource(String requestSource, String accessKey, String secretKey) {
if (StringUtils.isNotBlank(requestSource)) {
return requestSource;
}
if (StringUtils.isNotBlank(accessKey) && StringUtils.isNotBlank(secretKey)) {
return RequestSource.API.name();
}
return RequestSource.WEB.name();
}
}

View File

@@ -0,0 +1,6 @@
package cn.cordys.common.constants;
public enum ApplicationNumScope {
SYSTEM,
TASK
}

View File

@@ -0,0 +1,121 @@
package cn.cordys.common.constants;
import cn.cordys.common.util.Translator;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Payload;
import org.apache.commons.collections4.CollectionUtils;
import java.lang.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 枚举值校验注解,确保值为指定枚举类中的有效值。
* 可选排除某些枚举值。
*
* @author jianxing
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = EnumValue.EnumValueValidator.class)
public @interface EnumValue {
/**
* 错误提示信息,当校验失败时使用。
*
* @return 错误提示消息
*/
String message() default "{enum_value_valid_message}";
/**
* 必须的属性,用于分组校验。
*
* @return 分组校验的类
*/
Class<?>[] groups() default {};
/**
* 校验负载。
*
* @return 校验负载的类
*/
Class<? extends Payload>[] payload() default {};
/**
* 校验时使用的枚举类。
*
* @return 枚举类
*/
Class<? extends Enum<?>> enumClass();
/**
* 校验时排除的枚举值,仅支持字符串类型。
*
* @return 排除的枚举值
*/
String[] excludeValues() default {};
/**
* 枚举值校验的实现类。
*
* @see EnumValue
*/
class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
private Class<? extends Enum<?>> enumClass;
private String[] excludeValues;
@Override
public void initialize(EnumValue enumValue) {
this.enumClass = enumValue.enumClass();
this.excludeValues = enumValue.excludeValues();
}
/**
* 校验参数是否在枚举值中。
* 如果设置了排除值,校验值不应在排除列表中。
*
* @param value 待校验的值
* @param context 校验上下文
*
* @return 校验结果,若值有效返回 true否则返回 false
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
// 如果值为空,则认为有效
if (value == null) {
return true;
}
// 获取枚举类的所有实例
Enum<?>[] enums = enumClass.getEnumConstants();
List<Object> values = new ArrayList<>();
// 获取枚举类的所有有效值
for (Enum<?> item : enums) {
if (item instanceof ValueEnum) {
values.add(((ValueEnum<?>) item).getValue());
} else {
values.add(item.name());
}
}
// 判断是否排除指定的枚举值
boolean isExcludeValue = excludeValues != null && Arrays.stream(excludeValues).anyMatch(value::equals);
boolean valid = values.contains(value) && !isExcludeValue;
// 如果校验失败,生成自定义错误消息
if (!valid) {
context.disableDefaultConstraintViolation();
String errorValues = CollectionUtils.subtract(values, Arrays.asList(excludeValues)).toString();
context.buildConstraintViolationWithTemplate(Translator.get("enum_value_valid_message") + errorValues).addConstraintViolation();
}
return valid;
}
}
}

View File

@@ -0,0 +1,20 @@
package cn.cordys.common.constants;
import lombok.Getter;
/**
* 系统内置用户ID
*
* @author jianxing
*/
@Getter
public enum InternalUser {
ADMIN("admin");
private final String value;
InternalUser(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,11 @@
package cn.cordys.common.constants;
/**
* @author guoquqi
*/
public enum MoveTypeEnum {
BEFORE,
AFTER,
APPEND
}

View File

@@ -0,0 +1,6 @@
package cn.cordys.common.constants;
@FunctionalInterface
public interface QuadFunction<T, U, V, W, R> {
R apply(T t, U u, V v, W w);
}

View File

@@ -0,0 +1,17 @@
package cn.cordys.common.constants;
/**
* 用于参数校验注解 EnumValue 的枚举接口
* 如果枚举定义了类似 value 的值,可以实现改接口,即可使用于 EnumValue 注解
* 如果枚举值只需要通过 name() 获取,可以不实现该接口
*
* @author jianxing
*/
public interface ValueEnum<T> {
/**
* 获取枚举值
*
* @return 枚举值
*/
T getValue();
}

View File

@@ -0,0 +1,27 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseTree {
@Schema(description = "节点id")
private String id;
@Schema(description = "节点名称")
private String name;
@Schema(description = "排序单位")
private long pos;
@Schema(description = "组织id")
private String organizationId;
@Schema(description = "父节点id")
private String parentId;
}

View File

@@ -0,0 +1,81 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jodd.util.StringUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.Strings;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BaseTreeNode {
@Schema(description = "节点ID")
private String id;
@Schema(description = "节点名称")
private String name;
@Schema(description = "父节点ID")
private String parentId;
@Schema(description = "组织id")
private String organizationId;
@Schema(description = "子节点")
private List<BaseTreeNode> children = new ArrayList<>();
public BaseTreeNode(String id, String name) {
this.id = id;
this.name = name;
}
public BaseTreeNode(String id, String name, String parentId) {
this.id = id;
this.name = name;
this.parentId = parentId;
}
public static <T extends BaseTreeNode> List<T> buildTree(List<T> nodeList) {
// 用于存储节点的 Mapkey 是节点 ID
Map<String, T> nodeMap = new HashMap<>();
// 用于存储最终的根节点列表
List<T> rootNodes = new ArrayList<>();
// 1. 将所有节点放入 Map 中
for (T node : nodeList) {
nodeMap.put(node.getId(), node);
}
// 2. 遍历节点列表,构建父子关系
for (T node : nodeList) {
if (StringUtil.isBlank(node.getParentId()) || Strings.CI.equals(node.getParentId(), "NONE")) {
// 没有父节点,则为根节点
rootNodes.add(node);
} else {
// 获取父节点
T parentNode = nodeMap.get(node.getParentId());
if (parentNode != null) {
// 将当前节点添加到父节点的子节点列表中
parentNode.addChild(node);
}
}
}
// 3. 返回根节点列表
return rootNodes;
}
public void addChild(BaseTreeNode node) {
node.setParentId(this.getId());
children.add(node);
}
}

View File

@@ -0,0 +1,23 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author jianxing
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeptUserTreeNode extends BaseTreeNode {
@Schema(description = "节点类型")
private String nodeType;
@Schema(description = "是否启用")
private Boolean enabled = true;
@Schema(description = "是否部门负责人")
private Boolean commander = false;
}

View File

@@ -0,0 +1,36 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class JsonDifferenceDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "字段")
private String column;
@Schema(description = "原值")
private Object oldValue;
@Schema(description = "新值")
private Object newValue;
@Schema(description = "字段名称")
private String columnName;
@Schema(description = "原值结果")
private Object oldValueName;
@Schema(description = "新值结果")
private Object newValueName;
@Schema(description = "类型", examples = {"add/新增", "removed/删除", "modified/修改"})
private String type;
}

View File

@@ -0,0 +1,11 @@
package cn.cordys.common.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class NodeSortCountResultDTO {
private boolean isRefreshPos;
private long pos;
}

View File

@@ -0,0 +1,28 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NodeSortDTO {
@Schema(description = "节点ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "{file_module.not.exist}")
private BaseTree node;
@Schema(description = "父节点ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "{file_module.parent.not.exist}")
private BaseTree parent;
@Schema(description = "前一个节点")
private BaseTree previousNode;
@Schema(description = "后一个节点")
private BaseTree nextNode;
}

View File

@@ -0,0 +1,15 @@
package cn.cordys.common.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NodeSortQueryParam {
private String parentId;
private String operator;
private long pos;
}

View File

@@ -0,0 +1,18 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OptionCountDTO implements Serializable {
@Schema(description = "选项数量")
private String key;
@Schema(description = "选项值")
private Integer count;
}

View File

@@ -0,0 +1,18 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OptionDTO implements Serializable {
@Schema(description = "选项ID")
private String id;
@Schema(description = "选项名称")
private String name;
}

View File

@@ -0,0 +1,29 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* @author jianxing
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RoleDataScopeDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "选项ID")
private String id;
@Schema(description = "选项名称")
private String name;
@Schema(description = "数据权限")
private String dataScope;
@Schema(description = "组织ID")
private String organizationId;
}

View File

@@ -0,0 +1,24 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Set;
/**
* @author jianxing
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RolePermissionDTO extends RoleDataScopeDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "权限列表")
private Set<String> permissions;
}

View File

@@ -0,0 +1,23 @@
package cn.cordys.common.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author jianxing
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoleUserTreeNode extends BaseTreeNode {
@Schema(description = "节点类型")
private String nodeType;
@Schema(description = "是否是内置角色")
private Boolean internal = false;
@Schema(description = "是否启用")
private Boolean enabled = true;
}

View File

@@ -0,0 +1,40 @@
package cn.cordys.common.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* @author guoyuqi
*/
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
public class PosRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "组织id", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{system.org_id.not_blank}")
private String orgId;
@Schema(description = "移动用例id", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{system.move_id.not_blank}")
private String moveId;
@Schema(description = "目标用例id", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{system.target_id.not_blank}")
private String targetId;
@Schema(description = "移动类型", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"BEFORE", "AFTER", "APPEND"})
@NotBlank(message = "{system.move_mode.not_blank}")
private String moveMode;
}

View File

@@ -0,0 +1,102 @@
package cn.cordys.common.exception;
import org.apache.commons.lang3.StringUtils;
/**
* SystemException 是自定义的运行时异常,包含错误代码和详细信息。
*/
public class GenericException extends RuntimeException {
/**
* 错误代码
*/
protected IResultCode errorCode;
/**
* 构造方法,接受错误信息。
*
* @param message 错误信息
*/
public GenericException(String message) {
super(message);
}
/**
* 构造方法,接受一个异常对象。
*
* @param t 异常对象
*/
public GenericException(Throwable t) {
super(t);
}
/**
* 构造方法,接受错误代码,默认没有详细信息。
*
* @param errorCode 错误代码
*/
public GenericException(IResultCode errorCode) {
super(StringUtils.EMPTY);
if (errorCode == null) {
throw new IllegalArgumentException("errorCode cannot be null");
}
this.errorCode = errorCode;
}
/**
* 构造方法,接受错误代码和自定义错误信息。
*
* @param errorCode 错误代码
* @param message 错误信息
*/
public GenericException(IResultCode errorCode, String message) {
super(message);
if (errorCode == null) {
throw new IllegalArgumentException("errorCode cannot be null");
}
this.errorCode = errorCode;
}
/**
* 构造方法,接受错误代码和异常对象。
*
* @param errorCode 错误代码
* @param t 异常对象
*/
public GenericException(IResultCode errorCode, Throwable t) {
super(t);
if (errorCode == null) {
throw new IllegalArgumentException("errorCode cannot be null");
}
this.errorCode = errorCode;
}
/**
* 构造方法,接受自定义错误信息和异常对象。
*
* @param message 错误信息
* @param t 异常对象
*/
public GenericException(String message, Throwable t) {
super(message, t);
}
/**
* 获取错误代码。
*
* @return 错误代码
*/
public IResultCode getErrorCode() {
return errorCode;
}
/**
* 重写toString方法提供更有用的错误信息。
*
* @return 错误代码和错误信息
*/
@Override
public String toString() {
return "MSException{errorCode=" + errorCode + ", message=" + getMessage() + "}";
}
}

View File

@@ -0,0 +1,34 @@
package cn.cordys.common.exception;
import cn.cordys.common.util.Translator;
/**
* API 接口状态码
*
* @author jianxing
* <p>
* 1. 如果想返回具有 Http 含义的状态码,使用对应实现类
* 2. 业务状态码,各模块定义自己的状态码枚举类,各自管理
* 3. 业务错误码,定义规则为 6 位数字
* 4. 当需要抛出异常时,给异常设置状态码枚举对象
* <p>
*/
public interface IResultCode {
/**
* 返回状态码
*/
int getCode();
/**
* 返回状态码信息
*/
String getMessage();
/**
* 返回国际化后的状态码信息
* 如果没有匹配则返回原文
*/
default String getTranslationMessage(String message) {
return Translator.get(message, message);
}
}

View File

@@ -0,0 +1,11 @@
package cn.cordys.common.groups;
/**
* 标记接口,表示某个实体或操作是“已创建”的状态。
* <p>
* 该接口可用于分组校验,例如,在某些验证框架中,
* 用于标识新创建的对象,或者在特定条件下执行相关操作。
* </p>
*/
public interface Created {
}

View File

@@ -0,0 +1,11 @@
package cn.cordys.common.groups;
/**
* 标记接口,表示某个实体或操作是“已更新”的状态。
* <p>
* 该接口可用于分组校验,例如,在某些验证框架中,
* 用于标识已更新的对象,或者在特定条件下执行相关操作。
* </p>
*/
public interface Updated {
}

View File

@@ -0,0 +1,67 @@
package cn.cordys.common.pager;
import cn.cordys.common.dto.OptionDTO;
import com.github.pagehelper.Page;
import java.util.List;
import java.util.Map;
/**
* 分页工具类,提供分页信息设置功能。
* <p>
* 该类用于将 PageHelper 分页对象转换为自定义的分页对象 {@link Pager}。
* </p>
*/
public class PageUtils {
/**
* 设置分页信息并返回自定义的分页对象。
* <p>
* 此方法将 PageHelper 的分页数据(如:当前页、每页记录数、总记录数)转换为自定义的 {@link Pager} 对象。
* </p>
*
* @param page PageHelper 分页对象,包含分页相关的信息
* @param list 分页查询结果数据列表
* @param <T> 数据列表的类型
*
* @return 包含分页信息的自定义分页对象 {@link Pager}
*
* @throws RuntimeException 如果设置分页信息时发生错误,抛出运行时异常
*/
public static <T> Pager<T> setPageInfo(Page<?> page, T list) {
try {
Pager<T> pager = new Pager<>();
pager.setList(list);
pager.setPageSize(page.getPageSize());
pager.setCurrent(page.getPageNum());
pager.setTotal(page.getTotal());
return pager;
} catch (Exception e) {
throw new RuntimeException("保存当前页码数据时发生错误!", e);
}
}
/**
* 设置带有选项数据的分页信息
*
* @param page 分页对象
* @param list 数据列表
* @param <T> 数据列表的类型
* @param optionMap 选项集合
*
* @return 包含分页信息的自定义分页对象
*/
public static <T> PagerWithOption<T> setPageInfoWithOption(Page<?> page, T list, Map<String, List<OptionDTO>> optionMap) {
try {
PagerWithOption<T> pager = new PagerWithOption<>();
pager.setList(list);
pager.setPageSize(page.getPageSize());
pager.setCurrent(page.getPageNum());
pager.setTotal(page.getTotal());
pager.setOptionMap(optionMap);
return pager;
} catch (Exception e) {
throw new RuntimeException("保存当前页码数据时发生错误!", e);
}
}
}

View File

@@ -0,0 +1,56 @@
package cn.cordys.common.pager;
import lombok.Data;
/**
* 分页类,封装分页数据。
* <p>
* 该类用于表示分页查询结果,包括数据列表、总记录数、每页记录数和当前页码等信息。
* </p>
*
* @param <T> 数据列表的类型
*/
@Data
public class Pager<T> {
/**
* 数据列表,分页查询结果的具体数据。
*/
private T list;
/**
* 总记录数,表示符合查询条件的总条数。
*/
private long total;
/**
* 每页记录数,表示每一页显示的数据条数。
*/
private long pageSize;
/**
* 当前页码,表示当前显示的是哪一页。
*/
private long current;
/**
* 无参构造函数,初始化一个空的分页对象。
*/
public Pager() {
}
/**
* 带参构造函数,用于初始化分页对象。
*
* @param list 数据列表
* @param total 总记录数
* @param pageSize 每页记录数
* @param current 当前页码
*/
public Pager(T list, long total, long pageSize, long current) {
this.list = list;
this.total = total;
this.pageSize = pageSize;
this.current = current;
}
}

View File

@@ -0,0 +1,18 @@
package cn.cordys.common.pager;
import cn.cordys.common.dto.OptionDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class PagerWithOption<T> extends Pager<T> {
/**
* 选项集合
*/
@Schema(description = "选项集合")
private Map<String, List<OptionDTO>> optionMap;
}

View File

@@ -0,0 +1,22 @@
package cn.cordys.common.response.handler;
import java.lang.annotation.*;
/**
* 标记一个方法为“不需要返回结果”方法的注解。
* <p>
* 该注解用于标记那些不需要返回任何结果或对返回结果不做处理的方法。
* 常用于控制器方法中,表明此方法调用后不需要返回任何数据给客户端。
* </p>
* <p>
* 使用此注解的方法,可以避免框架或处理机制进行不必要的结果处理。
* </p>
*
* @see java.lang.annotation.Annotation
*/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoResultHolder {
}

View File

@@ -0,0 +1,215 @@
package cn.cordys.common.response.handler;
import cn.cordys.common.exception.GenericException;
import cn.cordys.common.exception.IResultCode;
import cn.cordys.common.response.result.CrmHttpResultCode;
import cn.cordys.common.util.ServiceUtils;
import cn.cordys.common.util.Translator;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.lang.ShiroException;
import org.eclipse.jetty.io.EofException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器,处理各类异常并返回统一格式的错误响应。
*/
@RestControllerAdvice
@Slf4j
public class RestControllerExceptionHandler {
/**
* 处理 NOT_FOUND 异常,拼接资源名称以提供更详细的错误信息。
*
* @param message 错误信息模板
*
* @return String 拼接后的错误信息
*/
private static String getNotFoundMessage(String message) {
String resourceName = ServiceUtils.getResourceName();
if (StringUtils.isNotBlank(resourceName)) {
message = String.format(message, Translator.get(resourceName, resourceName));
} else {
message = String.format(message, Translator.get("resource.name"));
}
ServiceUtils.clearResourceName();
return message;
}
/**
* 格式化异常栈信息。
*
* @param e Exception 异常
*
* @return String 异常栈的字符串表示
*/
public static String getStackTraceAsString(Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw, true));
log.error(sw.toString());
return sw.toString();
}
/**
* 处理数据校验异常,返回具体字段的校验信息。
*
* @param ex MethodArgumentNotValidException 异常
*
* @return ResultHolder 返回封装的错误信息HTTP 状态码 400
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultHolder handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResultHolder.error(CrmHttpResultCode.VALIDATE_FAILED.getCode(),
CrmHttpResultCode.VALIDATE_FAILED.getMessage(), errors);
}
/**
* 处理请求方法不支持的异常,返回 HTTP 状态码 405。
*
* @param response HttpServletResponse 响应
* @param exception 异常信息
*
* @return ResultHolder 返回错误信息
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResultHolder handleHttpRequestMethodNotSupportedException(HttpServletResponse response, Exception exception) {
response.setStatus(HttpStatus.METHOD_NOT_ALLOWED.value());
return ResultHolder.error(HttpStatus.METHOD_NOT_ALLOWED.value(), exception.getMessage());
}
/**
* 处理 MSException 异常,根据 errorCode 设置 HTTP 状态码和业务状态码。
*
* @param e MSException 异常
*
* @return ResponseEntity 返回响应实体,包含错误信息
*/
@ExceptionHandler(GenericException.class)
public ResponseEntity<ResultHolder> handlerGenericException(GenericException e) {
IResultCode errorCode = e.getErrorCode();
if (errorCode == null) {
// 未设置 errorCode返回内部服务器错误
return ResponseEntity.internalServerError()
.body(ResultHolder.error(CrmHttpResultCode.FAILED.getCode(), e.getMessage()));
}
int code = errorCode.getCode();
String message = errorCode.getMessage();
message = Translator.get(message, message);
if (errorCode instanceof CrmHttpResultCode) {
// 如果是 CrmHttpResultCode 类型,使用其状态码的后三位作为 HTTP 状态码
if (errorCode.equals(CrmHttpResultCode.NOT_FOUND)) {
message = getNotFoundMessage(message);
}
return ResponseEntity.status(code % 1000)
.body(ResultHolder.error(code, message, e.getMessage()));
} else {
// 其他类型的错误,返回 500 状态码
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ResultHolder.error(code, Translator.get(message, message), e.getMessage()));
}
}
/**
* 处理所有类型的异常,返回 HTTP 状态码 500 并格式化异常栈信息。
*
* @param e Exception 异常
*
* @return ResponseEntity 返回响应实体,包含错误信息
*/
@ExceptionHandler({Exception.class})
public ResponseEntity<ResultHolder> handleException(Exception e) {
return ResponseEntity.internalServerError()
.body(ResultHolder.error(CrmHttpResultCode.FAILED.getCode(),
e.getMessage(), getStackTraceAsString(e)));
}
@ExceptionHandler({NoResourceFoundException.class, UnavailableSecurityManagerException.class})
public ResponseEntity<ResultHolder> handleNoResourceFoundException(NoResourceFoundException e) {
log.error("No static resource");
return null;
}
/**
* 处理 EOF 异常,判断请求路径并返回适当的响应。
*
* @param request HttpServletRequest 请求
* @param e 异常信息
*
* @return ResponseEntity 返回响应实体,包含错误信息
*/
@ExceptionHandler({EofException.class})
public ResponseEntity<Object> handleEofException(HttpServletRequest request, Exception e) {
String requestURI = request.getRequestURI();
if (requestURI != null && (requestURI.startsWith("/assets")
|| requestURI.startsWith("/fonts")
|| requestURI.startsWith("/images")
|| requestURI.startsWith("/templates"))) {
return ResponseEntity.internalServerError().body(null);
}
return ResponseEntity.internalServerError()
.body(ResultHolder.error(CrmHttpResultCode.FAILED.getCode(),
e.getMessage(), getStackTraceAsString(e)));
}
/**
* 处理 Shiro 异常,返回 HTTP 状态码 401。
*
* @param request HttpServletRequest 请求
* @param response HttpServletResponse 响应
* @param exception 异常信息
*
* @return ResultHolder 返回错误信息
*/
@ExceptionHandler(ShiroException.class)
public ResultHolder exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return ResultHolder.error(HttpStatus.UNAUTHORIZED.value(), exception.getMessage());
}
/**
* 处理 Shiro 未授权异常,返回 HTTP 状态码 403。
*
* @param request HttpServletRequest 请求
* @param response HttpServletResponse 响应
* @param exception 异常信息
*
* @return ResultHolder 返回错误信息
*/
@ExceptionHandler(UnauthorizedException.class)
public ResultHolder unauthorizedExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
response.setStatus(HttpStatus.FORBIDDEN.value());
return ResultHolder.error(HttpStatus.FORBIDDEN.value(), exception.getMessage());
}
@ExceptionHandler(AsyncRequestNotUsableException.class)
public ResultHolder asyncRequestNotUsableExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
return null;
}
}

View File

@@ -0,0 +1,145 @@
package cn.cordys.common.response.handler;
import cn.cordys.common.response.result.CrmHttpResultCode;
import lombok.Data;
/**
* ResultHolder 类用于封装接口响应结果,包括状态码、消息、详细信息和返回数据。
*/
@Data
public class ResultHolder {
/**
* 请求是否成功的状态码,默认值为 200成功
*/
private int code = CrmHttpResultCode.SUCCESS.getCode();
/**
* 返回给前端的描述信息,一般是错误信息或成功信息。
*/
private String message;
/**
* 详细描述信息,例如在发生异常时存储异常日志。
*/
private Object messageDetail;
/**
* 返回的数据,可以是任何类型的对象。
*/
private Object data = "";
/**
* 默认构造函数,初始化默认值。
*/
public ResultHolder() {
}
/**
* 构造函数,初始化返回数据。
*
* @param data 返回的数据
*/
public ResultHolder(Object data) {
this.data = data;
}
/**
* 构造函数,初始化状态码和消息。
*
* @param code 状态码
* @param msg 消息
*/
public ResultHolder(int code, String msg) {
this.code = code;
this.message = msg;
}
/**
* 构造函数,初始化状态码、消息和数据。
*
* @param code 状态码
* @param msg 消息
* @param data 返回的数据
*/
public ResultHolder(int code, String msg, Object data) {
this.code = code;
this.message = msg;
this.data = data;
}
/**
* 构造函数,初始化状态码、消息、详细信息和数据。
*
* @param code 状态码
* @param msg 消息
* @param messageDetail 详细信息
* @param data 返回的数据
*/
public ResultHolder(int code, String msg, Object messageDetail, Object data) {
this.code = code;
this.message = msg;
this.messageDetail = messageDetail;
this.data = data;
}
/**
* 成功响应,返回带有数据的 ResultHolder。
*
* @param obj 返回的数据
*
* @return ResultHolder 返回封装的成功响应
*/
public static ResultHolder success(Object obj) {
return new ResultHolder(obj);
}
/**
* 错误响应,返回带有状态码和消息的 ResultHolder。
*
* @param code 状态码
* @param message 错误消息
*
* @return ResultHolder 返回封装的错误响应
*/
public static ResultHolder error(int code, String message) {
return new ResultHolder(code, message, null, null);
}
/**
* 错误响应,返回带有消息和详细信息的 ResultHolder。
*
* @param message 错误消息
* @param messageDetail 错误的详细信息
*
* @return ResultHolder 返回封装的错误响应
*/
public static ResultHolder error(String message, String messageDetail) {
return new ResultHolder(-1, message, messageDetail, null);
}
/**
* 错误响应,返回带有状态码、消息和详细信息的 ResultHolder。
*
* @param code 状态码
* @param message 错误消息
* @param messageDetail 错误的详细信息
*
* @return ResultHolder 返回封装的错误响应
*/
public static ResultHolder error(int code, String message, Object messageDetail) {
return new ResultHolder(code, message, messageDetail, null);
}
/**
* 特殊情况的响应,例如接口可正常返回 HTTP 状态码 200但需要给前端提供错误信息。
*
* @param code 自定义状态码
* @param message 返回给前端的消息
*
* @return ResultHolder 返回封装的响应
*/
public static ResultHolder successCodeErrorInfo(int code, String message) {
return new ResultHolder(code, message, null, null);
}
}

View File

@@ -0,0 +1,81 @@
package cn.cordys.common.response.handler;
import cn.cordys.common.util.JSON;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* <p>统一处理返回结果集的响应体增强类。</p>
* <p>该类用于在返回响应之前统一包装返回的结果,使得所有响应都遵循统一格式。
* 如果返回的是空值,自动包装为一个成功的响应。<p>
*
* <p>支持的消息转换器类型为MappingJackson2HttpMessageConverter 和 StringHttpMessageConverter。</p>
*/
@RestControllerAdvice(value = {"cn.cordys"})
public class ResultResponseBodyAdvice implements ResponseBodyAdvice<Object> {
/**
* 判断当前处理器是否支持该转换器。
*
* @param methodParameter 当前请求的方法参数
* @param converterType 转换器类型
*
* @return 如果支持则返回 true否则返回 false
*/
@Override
public boolean supports(MethodParameter methodParameter,
Class<? extends HttpMessageConverter<?>> converterType) {
return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType) ||
StringHttpMessageConverter.class.isAssignableFrom(converterType);
}
/**
* 在响应体写出之前对响应结果进行处理。
*
* @param body 响应体内容
* @param methodParameter 当前方法参数
* @param mediaType 响应的媒体类型
* @param converterType 当前使用的消息转换器类型
* @param serverHttpRequest 当前的 HTTP 请求
* @param serverHttpResponse 当前的 HTTP 响应
*
* @return 处理后的响应体内容
*/
@Override
public Object beforeBodyWrite(Object body,
MethodParameter methodParameter,
MediaType mediaType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
// 处理空值响应,转换为 JSON 格式的成功响应
if (body == null && StringHttpMessageConverter.class.isAssignableFrom(converterType)) {
serverHttpResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return JSON.toJSONString(ResultHolder.success(body));
}
// 如果方法标注了 NoResultHolder 注解,则不做任何包装
if (methodParameter.hasMethodAnnotation(NoResultHolder.class)) {
return body;
}
// 如果响应体不是 ResultHolder 类型,则包装为 ResultHolder
if (!(body instanceof ResultHolder)) {
if (body instanceof String) {
serverHttpResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return JSON.toJSONString(ResultHolder.success(body));
}
return ResultHolder.success(body);
}
// 如果响应体已经是 ResultHolder 类型,则直接返回
return body;
}
}

View File

@@ -0,0 +1,95 @@
package cn.cordys.common.response.result;
import cn.cordys.common.exception.IResultCode;
/**
* <p>表示 HTTP 状态码的枚举,主要用于在抛出异常时,自动设置 HTTP 响应状态码为对应状态码的后三位数字。</p>
* <p>枚举中的每个状态码代表一个 HTTP 请求的响应状态,常用于 REST API。</p>
*
* <p>状态码采用 100 系列,前三位代表业务域,后三位代表具体的 HTTP 状态码:</p>
* <ul>
* <li>成功100200</li>
* <li>失败100500</li>
* <li>验证失败100400</li>
* <li>未授权100401</li>
* <li>禁止访问100403</li>
* <li>未找到100404</li>
* </ul>
*
* <p>实现 {@link IResultCode} 接口,用于标准化异常处理。</p>
*
* @author jianxing
* @see IResultCode
* @see CrmHttpResultCode#SUCCESS
* @see CrmHttpResultCode#FAILED
* @see CrmHttpResultCode#VALIDATE_FAILED
* @see CrmHttpResultCode#UNAUTHORIZED
* @see CrmHttpResultCode#FORBIDDEN
* @see CrmHttpResultCode#NOT_FOUND
*/
public enum CrmHttpResultCode implements IResultCode {
/**
* 请求成功
*/
SUCCESS(100200, "http_result_success"),
/**
* 请求失败,未知异常
*/
FAILED(100500, "http_result_unknown_exception"),
/**
* 验证失败
*/
VALIDATE_FAILED(100400, "http_result_validate"),
/**
* 未授权,需登录
*/
UNAUTHORIZED(100401, "http_result_unauthorized"),
/**
* 禁止访问
*/
FORBIDDEN(100403, "http_result_forbidden"),
/**
* 资源未找到
*/
NOT_FOUND(100404, "http_result_not_found");
private final int code;
private final String message;
/**
* 枚举构造函数
*
* @param code HTTP 状态码
* @param message 状态码对应的消息键
*/
CrmHttpResultCode(int code, String message) {
this.code = code;
this.message = message;
}
/**
* 获取 HTTP 状态码
*
* @return HTTP 状态码的数值
*/
@Override
public int getCode() {
return code;
}
/**
* 获取状态码的消息
*
* @return 状态码对应的消息,经过翻译处理
*/
@Override
public String getMessage() {
return getTranslationMessage(this.message);
}
}

View File

@@ -0,0 +1,114 @@
package cn.cordys.common.uid;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.springframework.util.Assert;
/**
* Allocate 64 bits for the UID(long)<br>
* sign (fixed 1bit) -> deltaSecond -> workerId -> sequence(within the same second)
**/
public class BitsAllocator {
/**
* Total 64 bits
*/
public static final int TOTAL_BITS = 1 << 6;
/**
* Bits for [sign-> second-> workId-> sequence]
*/
private final int signBits = 1;
private final int timestampBits;
private final int workerIdBits;
private final int sequenceBits;
/**
* Max value for workId & sequence
*/
private final long maxDeltaSeconds;
private final long maxWorkerId;
private final long maxSequence;
/**
* Shift for timestamp & workerId
*/
private final int timestampShift;
private final int workerIdShift;
/**
* Constructor with timestampBits, workerIdBits, sequenceBits<br>
* The highest bit used for sign, so <code>63</code> bits for timestampBits, workerIdBits, sequenceBits
*/
public BitsAllocator(int timestampBits, int workerIdBits, int sequenceBits) {
// make sure allocated 64 bits
int allocateTotalBits = signBits + timestampBits + workerIdBits + sequenceBits;
Assert.isTrue(allocateTotalBits == TOTAL_BITS, "allocate not enough 64 bits");
// initialize bits
this.timestampBits = timestampBits;
this.workerIdBits = workerIdBits;
this.sequenceBits = sequenceBits;
// initialize max value
this.maxDeltaSeconds = ~(-1L << timestampBits);
this.maxWorkerId = ~(-1L << workerIdBits);
this.maxSequence = ~(-1L << sequenceBits);
// initialize shift
this.timestampShift = workerIdBits + sequenceBits;
this.workerIdShift = sequenceBits;
}
/**
* Allocate bits for UID according to delta seconds & workerId & sequence<br>
* <b>Note that: </b>The highest bit will always be 0 for sign
*/
public long allocate(long deltaSeconds, long workerId, long sequence) {
return (deltaSeconds << timestampShift) | (workerId << workerIdShift) | sequence;
}
/**
* Getters
*/
public int getSignBits() {
return signBits;
}
public int getTimestampBits() {
return timestampBits;
}
public int getWorkerIdBits() {
return workerIdBits;
}
public int getSequenceBits() {
return sequenceBits;
}
public long getMaxDeltaSeconds() {
return maxDeltaSeconds;
}
public long getMaxWorkerId() {
return maxWorkerId;
}
public long getMaxSequence() {
return maxSequence;
}
public int getTimestampShift() {
return timestampShift;
}
public int getWorkerIdShift() {
return workerIdShift;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}

View File

@@ -0,0 +1,37 @@
package cn.cordys.common.uid;
import cn.cordys.common.uid.impl.DefaultUidGenerator;
import cn.cordys.common.util.CommonBeanFactory;
/**
* IDGenerator 用于生成唯一的 ID。
* 提供了生成数字 ID 和字符串 ID 的功能。
*/
public class IDGenerator {
// 默认的 UID 生成器实例
private static final DefaultUidGenerator DEFAULT_UID_GENERATOR;
static {
// 从 CommonBeanFactory 获取 DefaultUidGenerator 实例
DEFAULT_UID_GENERATOR = CommonBeanFactory.getBean(DefaultUidGenerator.class);
}
/**
* 生成一个唯一的数字 ID。
*
* @return 唯一的数字 ID
*/
public static Long nextNum() {
return DEFAULT_UID_GENERATOR.getUID();
}
/**
* 生成一个唯一的字符串 ID。
*
* @return 唯一的字符串 ID
*/
public static String nextStr() {
return String.valueOf(DEFAULT_UID_GENERATOR.getUID());
}
}

View File

@@ -0,0 +1,92 @@
package cn.cordys.common.uid;
import cn.cordys.common.constants.ApplicationNumScope;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RIdGenerator;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 用于生成数字 ID 的生成器类支持根据不同的应用场景Scope生成唯一的 ID。
*
* <p>该类使用 Redisson 的分布式 ID 生成器和 Redis 进行管理。</p>
*/
@Component
public class NumGenerator {
// 初始值代表从100001开始生成 ID
private static final long INIT = 100001L;
// 限制每次生成的最大数量
private static final long LIMIT = 1;
// 特定的子范围,用于表示二级的用例
private static final List<ApplicationNumScope> SUB_NUM = List.of(ApplicationNumScope.SYSTEM);
// Redisson 实例,用于获取分布式 ID 生成器
private static Redisson redisson;
// StringRedisTemplate 用于操作 Redis
private static StringRedisTemplate stringRedisTemplate;
/**
* 根据指定的应用场景生成唯一的数字 ID。
*
* @param scope 应用场景(例如:接口用例)
*
* @return 唯一的数字 ID
*/
public static long nextNum(ApplicationNumScope scope) {
return nextNum(scope.name(), scope);
}
/**
* 根据指定的前缀和应用场景生成唯一的数字 ID。
*
* @param prefix 前缀例如ORGANIZATION_ID + "_" + DOMAIN
* @param scope 应用场景(例如:接口用例)
*
* @return 唯一的数字 ID
*/
public static long nextNum(String prefix, ApplicationNumScope scope) {
// 获取分布式 ID 生成器
RIdGenerator idGenerator = redisson.getIdGenerator(prefix + "_" + scope.name());
// 处理子范围的用例(如 SYSTEM
if (SUB_NUM.contains(scope)) {
// 确保 ID 生成器存在,如果不存在则初始化
if (!idGenerator.isExists()) {
idGenerator.tryInit(1, LIMIT);
}
// 返回格式化后的 ID保留 3 位
return Long.parseLong(prefix.split("_")[1] + StringUtils.leftPad(String.valueOf(idGenerator.nextId()), 3, "0"));
} else {
// 其他范围的用例,初始化 ID 生成器
if (!idGenerator.isExists()) {
idGenerator.tryInit(INIT, LIMIT);
}
return idGenerator.nextId();
}
}
/**
* 设置 Redisson 实例,用于分布式 ID 生成器。
*
* @param redisson Redisson 实例
*/
@Resource
public void setRedisson(Redisson redisson) {
NumGenerator.redisson = redisson;
}
/**
* 设置 StringRedisTemplate 实例,用于操作 Redis。
*
* @param stringRedisTemplate StringRedisTemplate 实例
*/
@Resource
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
NumGenerator.stringRedisTemplate = stringRedisTemplate;
}
}

View File

@@ -0,0 +1,122 @@
package cn.cordys.common.uid;
import cn.cordys.common.exception.GenericException;
import cn.cordys.quartz.anno.QuartzScheduled;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@Component
@Slf4j
public class SerialNumGenerator {
private static final int RULE_SIZE = 5;
private static final int DATE_KEY_IDX = 3;
private static final String PREFIX = "serial";
private final StringRedisTemplate redis;
public SerialNumGenerator(StringRedisTemplate redis) {
this.redis = redis;
}
/**
* 按规则生成流水号
*/
public String generateByRules(List<String> rules, String orgId, String formKey) {
if (CollectionUtils.size(rules) < RULE_SIZE) {
throw new GenericException("流水号规则配置有误");
}
Rule r = Rule.from(rules);
// 强制使用年月作为流水号 key 的日期部分
String date = new SimpleDateFormat(r.datePattern()).format(new Date());
String key = "%s:%s:%s:%s:%s".formatted(PREFIX, orgId, formKey, date, r.p1);
try {
// Redis 自增序列
long seq = Objects.requireNonNull(redis.opsForValue().increment(key), "Redis increment 返回 null");
// 构造最终流水号
return ("%s%s%s%s%0" + r.width() + "d")
.formatted(r.p1(), r.p2(), date, r.mid(), seq);
} catch (Exception e) {
log.error("生成流水号失败", e);
return null;
}
}
/**
* 内部规则封装
*/
private record Rule(String p1, String p2, String datePattern, String mid, int width) {
static Rule from(List<String> rules) {
return new Rule(
rules.get(0),
rules.get(1),
rules.get(2),
rules.get(3),
Integer.parseInt(rules.get(4))
);
}
private boolean equals(Rule other) {
return Strings.CS.equals(this.p1, other.p1)
&& Strings.CS.equals(this.p2, other.p2)
&& Strings.CS.equals(this.datePattern, other.datePattern)
&& Strings.CS.equals(this.mid, other.mid)
&& this.width == other.width;
}
}
@QuartzScheduled(cron = "0 0 1 1,16 * ?")
public void clean() {
log.info("开始清理过期流水号Key");
String currentMonth = new SimpleDateFormat("yyyyMM").format(new Date());
try (Cursor<String> cursor = redis.scan(ScanOptions.scanOptions().match("serial:*:*:*:*").count(1000).build())) {
cursor.forEachRemaining(key -> {
String[] ks = key.split(":");
if (ks.length != RULE_SIZE) {
log.warn("存在脏Key: {}", key);
return;
}
if (!currentMonth.equals(ks[DATE_KEY_IDX])) {
redis.delete(key);
log.info("删除过期Key: {}", key);
}
});
} catch (Exception e) {
log.error("流水号过期Key清理异常: ", e);
}
log.info("流水号过期Key清理完成");
}
public boolean sameRule(List<String> oRules, List<String> nRules) {
if (oRules.size() != nRules.size() && oRules.size() != RULE_SIZE) {
return false;
}
Rule or = Rule.from(oRules);
Rule nr = Rule.from(nRules);
return or.equals(nr);
}
/**
* 重置指定规则的流水号
*/
public void resetKey(String datePattern, String formKey, String orgId) {
String date = new SimpleDateFormat(datePattern).format(new Date());
String key = "%s:%s:%s:%s".formatted(PREFIX, orgId, formKey, date);
redis.delete(key);
}
}

View File

@@ -0,0 +1,167 @@
package cn.cordys.common.uid.buffer;
import cn.cordys.common.uid.utils.NamingThreadFactory;
import cn.cordys.common.uid.utils.PaddedAtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Represents an executor for padding {@link RingBuffer}<br>
* There are two kinds of executors: one for scheduled padding, the other for padding immediately.
*/
public class BufferPaddingExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(BufferPaddingExecutor.class);
/**
* Constants
*/
private static final String WORKER_NAME = "RingBuffer-Padding-Worker";
private static final String SCHEDULE_NAME = "RingBuffer-Padding-Schedule";
private static final long DEFAULT_SCHEDULE_INTERVAL = 5 * 60L; // 5 minutes
/**
* Whether buffer padding is running
*/
private final AtomicBoolean running;
/**
* We can borrow UIDs from the future, here store the last second we have consumed
*/
private final PaddedAtomicLong lastSecond;
/**
* RingBuffer & BufferUidProvider
*/
private final RingBuffer ringBuffer;
private final BufferedUidProvider uidProvider;
/**
* Padding immediately by the thread pool
*/
private final ExecutorService bufferPadExecutors;
/**
* Padding schedule thread
*/
private final ScheduledExecutorService bufferPadSchedule;
/**
* Schedule interval Unit as seconds
*/
private long scheduleInterval = DEFAULT_SCHEDULE_INTERVAL;
/**
* Constructor with {@link RingBuffer} and {@link BufferedUidProvider}, default use schedule
*
* @param ringBuffer {@link RingBuffer}
* @param uidProvider {@link BufferedUidProvider}
*/
public BufferPaddingExecutor(RingBuffer ringBuffer, BufferedUidProvider uidProvider) {
this(ringBuffer, uidProvider, true);
}
/**
* Constructor with {@link RingBuffer}, {@link BufferedUidProvider}, and whether use schedule padding
*
* @param ringBuffer {@link RingBuffer}
* @param uidProvider {@link BufferedUidProvider}
*/
public BufferPaddingExecutor(RingBuffer ringBuffer, BufferedUidProvider uidProvider, boolean usingSchedule) {
this.running = new AtomicBoolean(false);
this.lastSecond = new PaddedAtomicLong(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
this.ringBuffer = ringBuffer;
this.uidProvider = uidProvider;
// initialize thread pool
int cores = Runtime.getRuntime().availableProcessors();
bufferPadExecutors = Executors.newFixedThreadPool(cores * 2, new NamingThreadFactory(WORKER_NAME));
// initialize schedule thread
if (usingSchedule) {
bufferPadSchedule = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(SCHEDULE_NAME));
} else {
bufferPadSchedule = null;
}
}
/**
* Start executors such as schedule
*/
public void start() {
if (bufferPadSchedule != null) {
bufferPadSchedule.scheduleWithFixedDelay(this::paddingBuffer, scheduleInterval, scheduleInterval, TimeUnit.SECONDS);
}
}
/**
* Shutdown executors
*/
public void shutdown() {
if (!bufferPadExecutors.isShutdown()) {
bufferPadExecutors.shutdownNow();
}
if (bufferPadSchedule != null && !bufferPadSchedule.isShutdown()) {
bufferPadSchedule.shutdownNow();
}
}
/**
* Whether is padding
*/
public boolean isRunning() {
return running.get();
}
/**
* Padding buffer in the thread pool
*/
public void asyncPadding() {
bufferPadExecutors.submit(this::paddingBuffer);
}
/**
* Padding buffer fill the slots until to catch the cursor
*/
public void paddingBuffer() {
LOGGER.info("Ready to padding buffer lastSecond:{}. {}", lastSecond.get(), ringBuffer);
// is still running
if (!running.compareAndSet(false, true)) {
LOGGER.info("Padding buffer is still running. {}", ringBuffer);
return;
}
// fill the rest slots until to catch the cursor
boolean isFullRingBuffer = false;
while (!isFullRingBuffer) {
List<Long> uidList = uidProvider.provide(lastSecond.incrementAndGet());
for (Long uid : uidList) {
isFullRingBuffer = !ringBuffer.put(uid);
if (isFullRingBuffer) {
break;
}
}
}
// not running now
running.compareAndSet(true, false);
LOGGER.info("End to padding buffer lastSecond:{}. {}", lastSecond.get(), ringBuffer);
}
/**
* Setters
*/
public void setScheduleInterval(long scheduleInterval) {
Assert.isTrue(scheduleInterval > 0, "Schedule interval must positive!");
this.scheduleInterval = scheduleInterval;
}
}

View File

@@ -0,0 +1,15 @@
package cn.cordys.common.uid.buffer;
import java.util.List;
/**
* Buffered UID provider(Lambda supported), which provides UID in the same one second
*/
@FunctionalInterface
public interface BufferedUidProvider {
/**
* Provides UID in one second
*/
List<Long> provide(long momentInSecond);
}

View File

@@ -0,0 +1,14 @@
package cn.cordys.common.uid.buffer;
/**
* If tail catches the cursor it means that the ring buffer is full, any more buffer put request will be rejected.
* Specify the policy to handle the reject. This is a Lambda supported interface
*/
@FunctionalInterface
public interface RejectedPutBufferHandler {
/**
* Reject put buffer request
*/
void rejectPutBuffer(RingBuffer ringBuffer, long uid);
}

View File

@@ -0,0 +1,14 @@
package cn.cordys.common.uid.buffer;
/**
* If cursor catches the tail it means that the ring buffer is empty, any more buffer take request will be rejected.
* Specify the policy to handle the reject. This is a Lambda supported interface
*/
@FunctionalInterface
public interface RejectedTakeBufferHandler {
/**
* Reject take buffer request
*/
void rejectTakeBuffer(RingBuffer ringBuffer);
}

View File

@@ -0,0 +1,249 @@
package cn.cordys.common.uid.buffer;
import cn.cordys.common.uid.utils.PaddedAtomicLong;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import java.util.concurrent.atomic.AtomicLong;
/**
* Represents a ring buffer based on array.<br>
* Using array could improve read element performance due to the CUP cache line. To prevent
* the side effect of False Sharing, {@link PaddedAtomicLong} is using on 'tail' and 'cursor'<p>
* <p>
* A ring buffer is consisted of:
* <li><b>slots:</b> each element of the array is a slot, which is be set with a UID
* <li><b>flags:</b> flag array corresponding the same index with the slots, indicates whether can take or put slot
* <li><b>tail:</b> a sequence of the max slot position to produce
* <li><b>cursor:</b> a sequence of the min slot position to consume
*/
public class RingBuffer {
public static final int DEFAULT_PADDING_PERCENT = 50;
private static final Logger LOGGER = LoggerFactory.getLogger(RingBuffer.class);
/**
* Constants
*/
private static final int START_POINT = -1;
private static final long CAN_PUT_FLAG = 0L;
private static final long CAN_TAKE_FLAG = 1L;
/**
* The size of RingBuffer's slots, each slot hold a UID
*/
@Getter
private final int bufferSize;
private final long indexMask;
private final long[] slots;
private final PaddedAtomicLong[] flags;
/**
* Tail: last position sequence to produce
*/
private final AtomicLong tail = new PaddedAtomicLong(START_POINT);
/**
* Cursor: current position sequence to consume
*/
private final AtomicLong cursor = new PaddedAtomicLong(START_POINT);
/**
* Threshold for trigger padding buffer
*/
private final int paddingThreshold;
/**
* Reject put/take buffer handle policy
*/
private RejectedPutBufferHandler rejectedPutHandler = this::discardPutBuffer;
private RejectedTakeBufferHandler rejectedTakeHandler = this::exceptionRejectedTakeBuffer;
/**
* Executor of padding buffer
*/
private BufferPaddingExecutor bufferPaddingExecutor;
/**
* Constructor with buffer size, paddingFactor default as {@value #DEFAULT_PADDING_PERCENT}
*
* @param bufferSize must be positive & a power of 2
*/
public RingBuffer(int bufferSize) {
this(bufferSize, DEFAULT_PADDING_PERCENT);
}
/**
* Constructor with buffer size & padding factor
*
* @param bufferSize must be positive & a power of 2
* @param paddingFactor percent in (0 - 100). When the count of rest available UIDs reach the threshold, it will trigger padding buffer<br>
* Sample: paddingFactor=20, bufferSize=1000 -> threshold=1000 * 20 /100,
* padding buffer will be triggered when tail-cursor<threshold
*/
public RingBuffer(int bufferSize, int paddingFactor) {
// check buffer size is positive & a power of 2; padding factor in (0, 100)
Assert.isTrue(bufferSize > 0L, "RingBuffer size must be positive");
Assert.isTrue(Integer.bitCount(bufferSize) == 1, "RingBuffer size must be a power of 2");
Assert.isTrue(paddingFactor > 0 && paddingFactor < 100, "RingBuffer size must be positive");
this.bufferSize = bufferSize;
this.indexMask = bufferSize - 1;
this.slots = new long[bufferSize];
this.flags = initFlags(bufferSize);
this.paddingThreshold = bufferSize * paddingFactor / 100;
}
/**
* Put an UID in the ring & tail moved<br>
* We use 'synchronized' to guarantee the UID fill in slot & publish new tail sequence as atomic operations<br>
*
* <b>Note that: </b> It is recommended to put UID in a serialize way, cause we once batch generate a series UIDs and put
* the one by one into the buffer, so it is unnecessary put in multi-threads
* * @return false means that the buffer is full, apply {@link RejectedPutBufferHandler}
*/
public synchronized boolean put(long uid) {
long currentTail = tail.get();
long currentCursor = cursor.get();
// tail catches the cursor, means that you can't put any cause of RingBuffer is full
long distance = currentTail - (currentCursor == START_POINT ? 0 : currentCursor);
if (distance == bufferSize - 1) {
rejectedPutHandler.rejectPutBuffer(this, uid);
return false;
}
// 1. pre-check whether the flag is CAN_PUT_FLAG
int nextTailIndex = calSlotIndex(currentTail + 1);
if (flags[nextTailIndex].get() != CAN_PUT_FLAG) {
rejectedPutHandler.rejectPutBuffer(this, uid);
return false;
}
// 2. put UID in the next slot
// 3. update next slot' flag to CAN_TAKE_FLAG
// 4. publish tail with sequence increase by one
slots[nextTailIndex] = uid;
flags[nextTailIndex].set(CAN_TAKE_FLAG);
tail.incrementAndGet();
// The atomicity of operations above, guarantees by 'synchronized'. In another word,
// the take operation can't consume the UID we just put, until the tail is published(tail.incrementAndGet())
return true;
}
/**
* Take an UID of the ring at the next cursor, this is a lock free operation by using atomic cursor<p>
* <p>
* Before getting the UID, we also check whether reach the padding threshold,
* the padding buffer operation will be triggered in another thread<br>
* If there is no more available UID to be taken, the specified {@link RejectedTakeBufferHandler} will be applied<br>
*
* @return UID
*
* @throws IllegalStateException if the cursor moved back
*/
public long take() {
// spin get next available cursor
long currentCursor = cursor.get();
long nextCursor = cursor.updateAndGet(old -> old == tail.get() ? old : old + 1);
// check for safety consideration, it never occurs
Assert.isTrue(nextCursor >= currentCursor, "Cursor can't move back");
// trigger padding in an async-mode if reach the threshold
long currentTail = tail.get();
if (currentTail - nextCursor < paddingThreshold) {
LOGGER.info("Reach the padding threshold:{}. tail:{}, cursor:{}, rest:{}", paddingThreshold, currentTail,
nextCursor, currentTail - nextCursor);
bufferPaddingExecutor.asyncPadding();
}
// cursor catch the tail, means that there is no more available UID to take
if (nextCursor == currentCursor) {
rejectedTakeHandler.rejectTakeBuffer(this);
}
// 1. check next slot flag is CAN_TAKE_FLAG
int nextCursorIndex = calSlotIndex(nextCursor);
Assert.isTrue(flags[nextCursorIndex].get() == CAN_TAKE_FLAG, "Cursor not in can take status");
// 2. get UID from next slot
// 3. set next slot flag as CAN_PUT_FLAG.
long uid = slots[nextCursorIndex];
flags[nextCursorIndex].set(CAN_PUT_FLAG);
// Note that: Step 2,3 can not swap. If we set flag before get value of slot, the producer may overwrite the
// slot with a new UID, and this may cause the consumer take the UID twice after walk a round the ring
return uid;
}
/**
* Calculate slot index with the slot sequence (sequence % bufferSize)
*/
protected int calSlotIndex(long sequence) {
return (int) (sequence & indexMask);
}
/**
* Discard policy for {@link RejectedPutBufferHandler}, we just do logging
*/
protected void discardPutBuffer(RingBuffer ringBuffer, long uid) {
LOGGER.warn("Rejected putting buffer for uid:{}. {}", uid, ringBuffer);
}
/**
* Policy for {@link RejectedTakeBufferHandler}, throws {@link RuntimeException} after logging
*/
protected void exceptionRejectedTakeBuffer(RingBuffer ringBuffer) {
LOGGER.warn("Rejected take buffer. {}", ringBuffer);
throw new RuntimeException("Rejected take buffer. " + ringBuffer);
}
/**
* Initialize flags as CAN_PUT_FLAG
*/
private PaddedAtomicLong[] initFlags(int bufferSize) {
PaddedAtomicLong[] flags = new PaddedAtomicLong[bufferSize];
for (int i = 0; i < bufferSize; i++) {
flags[i] = new PaddedAtomicLong(CAN_PUT_FLAG);
}
return flags;
}
/**
* Getters
*/
public long getTail() {
return tail.get();
}
public long getCursor() {
return cursor.get();
}
/**
* Setters
*/
public void setBufferPaddingExecutor(BufferPaddingExecutor bufferPaddingExecutor) {
this.bufferPaddingExecutor = bufferPaddingExecutor;
}
public void setRejectedPutHandler(RejectedPutBufferHandler rejectedPutHandler) {
this.rejectedPutHandler = rejectedPutHandler;
}
public void setRejectedTakeHandler(RejectedTakeBufferHandler rejectedTakeHandler) {
this.rejectedTakeHandler = rejectedTakeHandler;
}
@Override
public String toString() {
return "RingBuffer [bufferSize=" + bufferSize +
", tail=" + tail +
", cursor=" + cursor +
", paddingThreshold=" + paddingThreshold + "]";
}
}

View File

@@ -0,0 +1,156 @@
package cn.cordys.common.uid.impl;
import cn.cordys.common.exception.GenericException;
import cn.cordys.common.uid.BitsAllocator;
import cn.cordys.common.uid.worker.WorkerIdAssigner;
import cn.cordys.common.util.TimeUtils;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class DefaultUidGenerator implements DisposableBean {
/**
* ===== Bits allocation =====
* sign(1) + time + worker + sequence = 64
*/
protected int timeBits = 30; // 秒级时间≈34 年
protected int workerBits = 21; // 200w+ worker
protected int seqBits = 12; // 每秒 4096
/**
* ===== Fixed epoch (DO NOT CHANGE AFTER RELEASE) =====
* 2025-01-01 00:00:00
*/
protected String epochStr = "2025-01-01";
protected long epochSeconds;
/**
* ===== Stable fields =====
*/
protected BitsAllocator bitsAllocator;
protected long workerId;
/**
* ===== Volatile fields =====
*/
protected long sequence = 0L;
protected long lastSecond = -1L;
/**
* ===== WorkerId =====
*/
@Resource
protected WorkerIdAssigner workerIdAssigner;
/**
* Spring lifecycle init
*/
@PostConstruct
public void init() {
// init epoch
setEpochStr(epochStr);
// init bits allocator
this.bitsAllocator = new BitsAllocator(timeBits, workerBits, seqBits);
// init worker id
this.workerId = workerIdAssigner.assignWorkerId();
if (workerId < 0 || workerId > bitsAllocator.getMaxWorkerId()) {
throw new IllegalStateException(
"WorkerId " + workerId + " exceeds max " + bitsAllocator.getMaxWorkerId()
);
}
log.info(
"Initialized UID Generator: epoch={}, bits=(time:{}, worker:{}, seq:{}), workerId={}",
epochStr, timeBits, workerBits, seqBits, workerId
);
}
/**
* Get UID
*/
public long getUID() {
Assert.notNull(bitsAllocator, "UidGenerator not initialized");
return nextId();
}
/**
* Generate next UID
*/
protected synchronized long nextId() {
long currentSecond = getCurrentSecond();
// Clock moved backwards
if (currentSecond < lastSecond) {
long refusedSeconds = lastSecond - currentSecond;
throw new GenericException(
String.format("Clock moved backwards. Refusing for %d seconds", refusedSeconds)
);
}
if (currentSecond == lastSecond) {
sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
if (sequence == 0) {
currentSecond = waitNextSecond(lastSecond);
}
} else {
sequence = 0L;
}
lastSecond = currentSecond;
return bitsAllocator.allocate(
currentSecond - epochSeconds,
workerId,
sequence
);
}
/**
* Wait until next second
*/
private long waitNextSecond(long lastSecond) {
long current = getCurrentSecond();
while (current <= lastSecond) {
current = getCurrentSecond();
}
return current;
}
/**
* Get current second
*/
private long getCurrentSecond() {
long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
throw new GenericException(
"Timestamp bits exhausted. Refusing UID generation. Now=" + currentSecond
);
}
return currentSecond;
}
public void setEpochStr(String epochStr) {
if (StringUtils.isNotBlank(epochStr)) {
this.epochStr = epochStr;
this.epochSeconds = TimeUnit.MILLISECONDS.toSeconds(
TimeUtils.parseByDayPattern(epochStr).getTime()
);
}
}
@Override
public void destroy() {
log.info("Shutdown UID Generator...");
}
}

View File

@@ -0,0 +1,93 @@
package cn.cordys.common.uid.utils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DockerUtils
*/
public abstract class DockerUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(DockerUtils.class);
/**
* Environment param keys
*/
private static final String ENV_KEY_HOST = "JPAAS_HOST";
private static final String ENV_KEY_PORT = "JPAAS_HTTP_PORT";
private static final String ENV_KEY_PORT_ORIGINAL = "JPAAS_HOST_PORT_8080";
/**
* Docker host & port
*/
private static String DOCKER_HOST = "";
private static String DOCKER_PORT = "";
/**
* Whether is docker
*/
private static boolean IS_DOCKER;
static {
retrieveFromEnv();
}
/**
* Retrieve docker host
*
* @return empty string if not a docker
*/
public static String getDockerHost() {
return DOCKER_HOST;
}
/**
* Retrieve docker port
*
* @return empty string if not a docker
*/
public static String getDockerPort() {
return DOCKER_PORT;
}
/**
* Whether a docker
*
* @return
*/
public static boolean isDocker() {
return IS_DOCKER;
}
/**
* Retrieve host & port from environment
*/
private static void retrieveFromEnv() {
// retrieve host & port from environment
DOCKER_HOST = System.getenv(ENV_KEY_HOST);
DOCKER_PORT = System.getenv(ENV_KEY_PORT);
// not found from 'JPAAS_HTTP_PORT', then try to find from 'JPAAS_HOST_PORT_8080'
if (StringUtils.isBlank(DOCKER_PORT)) {
DOCKER_PORT = System.getenv(ENV_KEY_PORT_ORIGINAL);
}
boolean hasEnvHost = StringUtils.isNotBlank(DOCKER_HOST);
boolean hasEnvPort = StringUtils.isNotBlank(DOCKER_PORT);
// docker can find both host & port from environment
if (hasEnvHost && hasEnvPort) {
IS_DOCKER = true;
// found nothing means not a docker, maybe an actual machine
} else if (!hasEnvHost && !hasEnvPort) {
IS_DOCKER = false;
} else {
LOGGER.error("Missing host or port from env for Docker. host:{}, port:{}", DOCKER_HOST, DOCKER_PORT);
throw new RuntimeException(
"Missing host or port from env for Docker. host:" + DOCKER_HOST + ", port:" + DOCKER_PORT);
}
}
}

View File

@@ -0,0 +1,38 @@
package cn.cordys.common.uid.utils;
import org.springframework.util.Assert;
/**
* EnumUtils provides the operations for {@link ValuedEnum} such as Parse, value of...
*/
public abstract class EnumUtils {
/**
* Parse the bounded value into ValuedEnum
*/
public static <T extends ValuedEnum<V>, V> T parse(Class<T> clz, V value) {
Assert.notNull(clz, "clz can not be null");
if (value == null) {
return null;
}
for (T t : clz.getEnumConstants()) {
if (value.equals(t.value())) {
return t;
}
}
return null;
}
/**
* Null-safe valueOf function
*/
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
if (name == null) {
return null;
}
return Enum.valueOf(enumType, name);
}
}

View File

@@ -0,0 +1,118 @@
package cn.cordys.common.uid.utils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
/**
* Named thread in ThreadFactory. If there is no specified name for thread, it
* will auto detect using the invoker classname instead.
*/
@Slf4j
public class NamingThreadFactory implements ThreadFactory {
/**
* Is daemon thread
*/
private final boolean daemon;
/**
* UncaughtExceptionHandler
*/
private final UncaughtExceptionHandler uncaughtExceptionHandler;
/**
* Sequences for multi thread name prefix
*/
private final ConcurrentHashMap<String, AtomicLong> sequences;
/**
* Thread name pre
* -- GETTER --
* Getters & Setters
*/
@Setter
@Getter
private String name;
/**
* Constructors
*/
public NamingThreadFactory() {
this(null, false, null);
}
public NamingThreadFactory(String name) {
this(name, false, null);
}
public NamingThreadFactory(String name, boolean daemon) {
this(name, daemon, null);
}
public NamingThreadFactory(String name, boolean daemon, UncaughtExceptionHandler handler) {
this.name = name;
this.daemon = daemon;
this.uncaughtExceptionHandler = handler;
this.sequences = new ConcurrentHashMap<>();
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(this.daemon);
// If there is no specified name for thread, it will auto detect using the invoker classname instead.
// Notice that auto detect may cause some performance overhead
String prefix = this.name;
if (StringUtils.isBlank(prefix)) {
prefix = getInvoker();
}
thread.setName(prefix + "-" + getSequence(prefix));
// no specified uncaughtExceptionHandler, just do logging.
thread.setUncaughtExceptionHandler(Objects.requireNonNullElseGet(this.uncaughtExceptionHandler, () -> (t, e) -> log.error("unhandled exception in thread: " + t.getName(), e)));
return thread;
}
/**
* Get the method invoker's class name
*
* @return
*/
private String getInvoker() {
Exception e = new Exception();
StackTraceElement[] sites = e.getStackTrace();
if (sites.length > 2) {
return ClassUtils.getShortClassName(sites[2].getClassName());
}
return getClass().getSimpleName();
}
/**
* Get sequence for different naming prefix
*
* @param invoker
*
* @return
*/
private long getSequence(String invoker) {
AtomicLong r = this.sequences.get(invoker);
if (r == null) {
r = new AtomicLong(0);
AtomicLong previous = this.sequences.putIfAbsent(invoker, r);
if (previous != null) {
r = previous;
}
}
return r.incrementAndGet();
}
}

View File

@@ -0,0 +1,68 @@
package cn.cordys.common.uid.utils;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
/**
* NetUtils
*/
public abstract class NetUtils {
/**
* Pre-loaded local address
*/
public static InetAddress localAddress;
static {
try {
localAddress = getLocalInetAddress();
} catch (SocketException e) {
throw new RuntimeException("fail to get local ip.");
}
}
/**
* Retrieve the first validated local ip address(the Public and LAN ip addresses are validated).
*
* @return the local address
*
* @throws SocketException the socket exception
*/
public static InetAddress getLocalInetAddress() throws SocketException {
// enumerates all network interfaces
Enumeration<NetworkInterface> enu = NetworkInterface.getNetworkInterfaces();
while (enu.hasMoreElements()) {
NetworkInterface ni = enu.nextElement();
if (ni.isLoopback()) {
continue;
}
Enumeration<InetAddress> addressEnumeration = ni.getInetAddresses();
while (addressEnumeration.hasMoreElements()) {
InetAddress address = addressEnumeration.nextElement();
// ignores all invalidated addresses
if (address.isLinkLocalAddress() || address.isLoopbackAddress() || address.isAnyLocalAddress()) {
continue;
}
return address;
}
}
throw new RuntimeException("No validated local address!");
}
/**
* Retrieve local address
*
* @return the string local address
*/
public static String getLocalAddress() {
return localAddress.getHostAddress();
}
}

View File

@@ -0,0 +1,39 @@
package cn.cordys.common.uid.utils;
import java.io.Serial;
import java.util.concurrent.atomic.AtomicLong;
/**
* Represents a padded {@link AtomicLong} to prevent the FalseSharing problem<p>
* <p>
* The CPU cache line commonly be 64 bytes, here is a sample of cache line after padding:<br>
* 64 bytes = 8 bytes (object reference) + 6 * 8 bytes (padded long) + 8 bytes (a long value)
*/
public class PaddedAtomicLong extends AtomicLong {
@Serial
private static final long serialVersionUID = -3415778863941386253L;
/**
* Padded 6 long (48 bytes)
*/
public volatile long p1, p2, p3, p4, p5, p6 = 7L;
/**
* Constructors from {@link AtomicLong}
*/
public PaddedAtomicLong() {
super();
}
public PaddedAtomicLong(long initialValue) {
super(initialValue);
}
/**
* To prevent GC optimizations for cleaning unused padded references
*/
public long sumPaddingToPreventOptimization() {
return p1 + p2 + p3 + p4 + p5 + p6;
}
}

View File

@@ -0,0 +1,10 @@
package cn.cordys.common.uid.utils;
/**
* {@code ValuedEnum} defines an enumeration which is bounded to a value, you
* may implements this interface when you defines such kind of enumeration, that
* you can use {@link EnumUtils} to simplify parse and valueOf operation.
*/
public interface ValuedEnum<T> {
T value();
}

View File

@@ -0,0 +1,17 @@
package cn.cordys.common.uid.worker;
import cn.cordys.common.uid.impl.DefaultUidGenerator;
/**
* Represents a worker id assigner for {@link DefaultUidGenerator}
*/
public interface WorkerIdAssigner {
/**
* Assign worker id for {@link DefaultUidGenerator}
*
* @return assigned worker id
*/
long assignWorkerId();
}

View File

@@ -0,0 +1,31 @@
package cn.cordys.common.uid.worker;
import cn.cordys.common.uid.utils.ValuedEnum;
/**
* WorkerNodeType
* <li>CONTAINER: Such as Docker
* <li>ACTUAL: Actual machine
*/
public enum WorkerNodeType implements ValuedEnum<Integer> {
CONTAINER(1), ACTUAL(2);
/**
* Lock type
*/
private final Integer type;
/**
* Constructor with field of type
*/
WorkerNodeType(Integer type) {
this.type = type;
}
@Override
public Integer value() {
return type;
}
}

View File

@@ -0,0 +1,115 @@
package cn.cordys.common.util;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Method;
/**
* BeanUtils 提供了用于操作 Java Bean 的工具方法,包括属性复制、反射获取和设置属性值等。
*/
public class BeanUtils {
/**
* 复制源对象的属性到目标对象。
*
* @param target 目标对象
* @param source 源对象
* @param <T> 目标对象类型
*
* @return 目标对象
*
* @throws RuntimeException 如果复制过程失败,抛出运行时异常
*/
public static <T> T copyBean(T target, Object source) {
try {
org.springframework.beans.BeanUtils.copyProperties(source, target);
return target;
} catch (Exception e) {
throw new RuntimeException("Failed to copy object: ", e);
}
}
/**
* 复制源对象的属性到目标对象,并可以指定忽略的属性。
*
* @param target 目标对象
* @param source 源对象
* @param ignoreProperties 要忽略的属性名称
* @param <T> 目标对象类型
*
* @return 目标对象
*
* @throws RuntimeException 如果复制过程失败,抛出运行时异常
*/
public static <T> T copyBean(T target, Object source, String... ignoreProperties) {
try {
org.springframework.beans.BeanUtils.copyProperties(source, target, ignoreProperties);
return target;
} catch (Exception e) {
throw new RuntimeException("Failed to copy object: ", e);
}
}
/**
* 根据字段名获取 Java Bean 的属性值。
*
* @param fieldName 字段名称
* @param bean Java Bean 对象
*
* @return 字段的值,如果获取失败则返回 null
*/
public static Object getFieldValueByName(String fieldName, Object bean) {
try {
if (StringUtils.isBlank(fieldName)) {
return null;
}
String getter = "get" + StringUtils.capitalize(fieldName);
Method method = bean.getClass().getMethod(getter);
return method.invoke(bean);
} catch (Exception e) {
return null;
}
}
/**
* 根据字段名和类型设置 Java Bean 的属性值。
*
* @param bean Java Bean 对象
* @param fieldName 字段名称
* @param value 要设置的值
* @param type 字段类型
*/
public static void setFieldValueByName(Object bean, String fieldName, Object value, Class<?> type) {
try {
if (StringUtils.isBlank(fieldName)) {
return;
}
String setter = "set" + StringUtils.capitalize(fieldName);
Method method = bean.getClass().getMethod(setter, type);
method.invoke(bean, value);
} catch (Exception ignore) {
// 可以根据需求记录日志或进行其他处理
}
}
/**
* 根据字段名和类型获取 Java Bean 的 setter 方法。
*
* @param bean Java Bean 对象
* @param fieldName 字段名称
* @param type 字段类型
*
* @return setter 方法,如果获取失败则返回 null
*/
public static Method getMethod(Object bean, String fieldName, Class<?> type) {
try {
if (StringUtils.isBlank(fieldName)) {
return null;
}
String setter = "set" + StringUtils.capitalize(fieldName);
return bean.getClass().getMethod(setter, type);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,16 @@
package cn.cordys.common.util;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.math.BigDecimal;
public class BigDecimalNoTrailingZeroSerializer extends JsonSerializer<BigDecimal> {
@Override
public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 去除末尾零并转换为普通字符串
gen.writeNumber(value.stripTrailingZeros().toPlainString());
}
}

View File

@@ -0,0 +1,70 @@
package cn.cordys.common.util;
/**
* @Author: jianxing
* @CreateTime: 2025-07-11 10:58
*/
public class CaseFormatUtils {
/**
* 将驼峰字符串转换为下划线分隔的字符串。
*
* @param camelStr 驼峰字符串
*
* @return 下划线字符串
*/
public static String camelToUnderscore(String camelStr) {
return convertCamelToSeparator(camelStr, '_');
}
/**
* 将驼峰字符串转换为指定分隔符的字符串。
*
* @param camelStr 驼峰字符串
* @param separator 分隔符
*
* @return 分隔符字符串
*/
public static String convertCamelToSeparator(String camelStr, char separator) {
if (camelStr == null || camelStr.trim().isEmpty()) {
return camelStr;
}
StringBuilder result = new StringBuilder();
char[] chars = camelStr.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (Character.isUpperCase(c)) {
if (i > 0) {
result.append(separator);
}
result.append(Character.toLowerCase(c));
} else {
result.append(c);
}
}
return result.toString();
}
/**
* 将字符串首字母转换为大写
*
* @param input 输入字符串
*
* @return 首字母大写的字符串
*/
public static String capitalizeFirstLetter(String input) {
if (input == null || input.isEmpty()) {
return input;
}
// 提取第一个字符并转换为大写
char firstChar = Character.toUpperCase(input.charAt(0));
// 如果字符串只有一个字符,直接返回
if (input.length() == 1) {
return String.valueOf(firstChar);
}
// 拼接剩余字符
return firstChar + input.substring(1);
}
}

View File

@@ -0,0 +1,237 @@
package cn.cordys.common.util;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 加密解密工具类,提供 MD5、BASE64 和 AES 加密解密操作。
* 支持常见的加密解密算法,简化了加密过程。
*/
public class CodingUtils {
private static final String UTF_8 = "UTF-8";
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* 加密偏移量AES 加密的初始化向量。
*/
private static final String GCM_IV = "1Av7hf9PgHusUHRm";
private static final int GCM_TAG_LENGTH = 128; // GCM 标签长度(以位为单位)
/**
* MD5加密默认UTF-8字符集
*
* @param src 要加密的字符串
*
* @return 加密后的MD5字符串
*/
public static String md5(String src) {
return md5(src, UTF_8);
}
/**
* MD5加密
*
* @param src 要加密的字符串
* @param charset 使用的字符集
*
* @return 加密后的MD5字符串
*/
public static String md5(String src, String charset) {
if (StringUtils.isBlank(src)) {
throw new IllegalArgumentException("Input for MD5 cannot be null or empty");
}
try {
byte[] strTemp = src.getBytes(StringUtils.defaultIfBlank(charset, UTF_8));
MessageDigest mdTemp = MessageDigest.getInstance("MD5");
mdTemp.update(strTemp);
byte[] md = mdTemp.digest();
char[] str = new char[md.length * 2];
int k = 0;
for (byte byte0 : md) {
str[k++] = HEX_DIGITS[(byte0 >>> 4) & 0xf];
str[k++] = HEX_DIGITS[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
throw new RuntimeException("MD5 encrypt error:", e);
}
}
/**
* BASE64加密
*
* @param src 待加密的字符串
*
* @return 加密后的字符串
*/
public static String base64Encoding(String src) {
if (StringUtils.isBlank(src)) {
throw new IllegalArgumentException("Input for BASE64 encoding cannot be null or empty");
}
try {
return Base64.encodeBase64String(src.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("BASE64 encoding error:", e);
}
}
/**
* AES-GCM加密
*
* @param src 待加密的字符串
* @param secretKey 加密密钥16字节
* @param iv 初始向量12字节
*
* @return 加密后的字符串
*/
public static String aesEncrypt(String src, String secretKey, byte[] iv) {
if (StringUtils.isBlank(src) || StringUtils.isBlank(secretKey)) {
throw new IllegalArgumentException("Input or secretKey cannot be null or empty");
}
try {
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
byte[] encryptedBytes = cipher.doFinal(src.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64String(encryptedBytes);
} catch (Exception e) {
throw new RuntimeException("AES-GCM encrypt error:", e);
}
}
/**
* AES-GCM解密
*
* @param src 待解密的字符串
* @param secretKey 解密密钥16字节
* @param iv 初始向量12字节
*
* @return 解密后的字符串
*/
public static String aesDecrypt(String src, String secretKey, byte[] iv) {
if (StringUtils.isBlank(src) || StringUtils.isBlank(secretKey)) {
throw new IllegalArgumentException("Input or secretKey cannot be null or empty");
}
try {
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
byte[] decodedBytes = Base64.decodeBase64(src);
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("AES-GCM decrypt error:", e);
}
}
/**
* 生成一个新的AES密钥
*
* @return 生成的AES密钥Base64编码
*/
public static String generateSecretKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128);
SecretKey secretKey = keyGen.generateKey();
return bytesToHex(secretKey.getEncoded());
} catch (Exception e) {
throw new RuntimeException("Generate AES secret key error:", e);
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static String generateAK() {
// 生成长度为 20 的随机字母数字字符串
return RandomStringUtils.secure().nextAlphabetic(16);
}
/**
* 生成随机IV用于AES-GCM
*
* @return 随机生成的IV
*/
public static byte[] generateIv() {
return GCM_IV.getBytes();
}
/**
* 计算字符串的哈希值SHA-256并返回其前16个字符的十六进制表示
*
* @param str 需要计算哈希值的字符串
*
* @return 字符串的哈希值16个字符的十六进制表示
*/
public static String hashStr(String str) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(str.getBytes(StandardCharsets.UTF_8));
// 转换为十六进制字符串
char[] hexChars = new char[digest.length * 2];
int k = 0;
for (byte b : digest) {
hexChars[k++] = HEX_DIGITS[(b >>> 4) & 0xf];
hexChars[k++] = HEX_DIGITS[b & 0xf];
}
// 只使用前16个字符平衡长度和唯一性
return new String(hexChars, 0, Math.min(16, hexChars.length));
} catch (NoSuchAlgorithmException e) {
// 降级方案:使用多种哈希组合减少冲突
return str.hashCode() + "-" + str.length();
}
}
public static String aesCBCEncrypt(String src, String secretKey, String iv) {
if (StringUtils.isBlank(src)) {
return src;
}
if (StringUtils.isBlank(secretKey)) {
throw new IllegalArgumentException("Input or secretKey cannot be null or empty");
}
try {
byte[] raw = secretKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
// "算法/模式/补码方式" ECB
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv1 = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv1);
byte[] encrypted = cipher.doFinal(src.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64String(encrypted);
} catch (Exception e) {
throw new RuntimeException("AES encrypt error:", e);
}
}
}

View File

@@ -0,0 +1,197 @@
package cn.cordys.common.util;
import jakarta.servlet.Filter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
/**
* 通用的Spring Bean工厂用于从Spring容器中获取Bean并调用方法。
* 实现了ApplicationContextAware接口以便在需要时获取Spring的应用上下文。
*/
@Component
@Slf4j
public class CommonBeanFactory implements ApplicationContextAware {
public static String BASE_X_P = "cn.cordys.xpack";
// 保存ApplicationContext实例
private static ApplicationContext context;
/**
* 根据Bean名称获取Bean实例
*
* @param beanName Bean的名称
*
* @return 返回Bean实例若未找到则返回null
*/
public static Object getBean(String beanName) {
try {
// 如果上下文或Bean名称为空则返回null
if (context != null && StringUtils.isNotBlank(beanName)) {
return context.getBean(beanName);
}
} catch (BeansException e) {
// 捕获Spring的异常并返回null
return null;
}
return null;
}
/**
* 根据Bean类型获取Bean实例
*
* @param className Bean的类型
* @param <T> 返回的Bean类型
*
* @return 返回Bean实例若未找到则返回null
*/
public static <T> T getBean(Class<T> className) {
try {
// 如果上下文或类型为空则返回null
if (context != null && className != null) {
return context.getBean(className);
}
} catch (BeansException e) {
// 捕获Spring的异常并返回null
return null;
}
return null;
}
/**
* 获取指定类型的所有Bean实例
*
* @param className Bean的类型
* @param <T> 返回的Bean类型
*
* @return 返回所有类型为className的Bean实例的Map
*/
public static <T> Map<String, T> getBeansOfType(Class<T> className) {
return context.getBeansOfType(className);
}
/**
* 调用指定Bean的方法
*
* @param beanName Bean的名称
* @param methodFunction 方法选择器函数接受Bean的类类型并返回一个Method对象
* @param args 方法调用的参数
*
* @return 返回方法的执行结果若发生异常则返回null
*/
public static Object invoke(String beanName, Function<Class<?>, Method> methodFunction, Object... args) {
try {
Object bean = getBean(beanName);
// 检查bean是否存在
if (ObjectUtils.isNotEmpty(bean)) {
Class<?> clazz = bean.getClass();
// 使用提供的methodFunction来获取方法并执行
Method method = methodFunction.apply(clazz);
if (method != null) {
return method.invoke(bean, args);
}
}
} catch (Exception e) {
// 记录错误日志
log.error(e.getMessage(), e);
}
return null;
}
public static boolean packageExists(String basePackage) {
// 不使用默认的候选者过滤器(不只扫描 @Component
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
// 添加一个总是匹配的过滤器:只要有类就算命中
scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*")));
// 按包路径扫描
Set<BeanDefinition> defs = scanner.findCandidateComponents(basePackage);
return !defs.isEmpty();
}
public static boolean packageExists() {
return packageExists(BASE_X_P);
}
public static Filter getFilter() {
try {
if (!packageExists()) {
return null;
}
final Class<? extends Filter> clazz = Class.forName(BASE_X_P + ".crm.ApiKeyPreFilter").asSubclass(Filter.class);
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception ignored) {
return null;
}
}
public static String getUser(HttpServletRequest request) {
if (packageExists()) {
Object user = CommonBeanFactory.invoke("extLicenseService",
clazz -> {
try {
return clazz.getMethod("getUser", HttpServletRequest.class);
} catch (NoSuchMethodException e) {
return null;
}
}, request);
if (user != null) {
return String.valueOf(user);
}
}
return null;
}
/**
* 调用指定类的静态方法
*
* @param className 类的全名,例如 "com.example.MyClass"
* @param methodName 方法名
* @param parameterTypes 方法参数类型
* @param args 方法参数
*
* @return 方法返回值
*/
public static void invokeStatic(String className, String methodName, Class<?>[] parameterTypes, Object... args) {
try {
// 1. 加载类
Class<?> clazz = Class.forName(className);
// 2. 获取方法
Method method = clazz.getMethod(methodName, parameterTypes);
// 3. 调用静态方法,第一个参数传 null
method.invoke(null, args);
} catch (Exception ignored) {
}
}
/**
* 设置ApplicationContextSpring容器会自动注入上下文。
*
* @param ctx 当前的Spring应用上下文
*
* @throws BeansException 如果出现错误
*/
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
context = ctx;
}
}

View File

@@ -0,0 +1,190 @@
package cn.cordys.common.util;
import org.apache.commons.codec.binary.Base64;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.zip.*;
public class CompressUtils {
private CompressUtils() {
}
/***
* Zip压缩
*
* @param data 待压缩数据
* @return 压缩后数据
*/
public static Object zip(Object data) {
if (!(data instanceof byte[] temp)) {
return data;
}
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(bos)) {
ZipEntry entry = new ZipEntry("zip");
zip.putNextEntry(entry);
zip.write(temp);
zip.closeEntry();
return bos.toByteArray();
} catch (Exception e) {
return data;
}
}
private static File getFile(String filePath) throws IOException {
// 创建文件对象
File file = new File(filePath);
// 如果文件不存在,则尝试创建
if (!file.exists() && !file.createNewFile()) {
throw new IOException("Failed to create the file: " + filePath);
}
// 返回文件
return file;
}
public static FileOutputStream getFileStream(File file) throws FileNotFoundException {
return new FileOutputStream(file);
}
private static void zipFile(File file, ZipOutputStream zipOutputStream) throws IOException {
if (file.exists() && file.isFile()) {
try (FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis)) {
ZipEntry entry = new ZipEntry(file.getName());
zipOutputStream.putNextEntry(entry);
// 使用固定大小的缓冲区
final int BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
// 循环读取数据并写入压缩流
while ((bytesRead = bis.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, bytesRead);
}
zipOutputStream.closeEntry(); // 关闭当前 ZipEntry
}
}
}
/**
* 将多个文件压缩
*
* @param zipFilePath 压缩文件所在路径
* @param fileList 要压缩的文件
*/
public static File zipFiles(String zipFilePath, List<File> fileList) throws IOException {
File zipFile = getFile(zipFilePath);
// 文件输出流
FileOutputStream outputStream = getFileStream(zipFile);
// 压缩流
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
// 压缩列表中的文件
for (File file : fileList) {
zipFile(file, zipOutputStream);
}
// 关闭压缩流、文件流
zipOutputStream.close();
outputStream.close();
return zipFile;
}
/**
* 将多个文件压缩至指定路径
*
* @param fileList 待压缩的文件列表
* @param zipFilePath 压缩文件路径
*
* @return 返回压缩好的文件
*/
public static File zipFilesToPath(String zipFilePath, List<File> fileList) throws IOException {
File zipFile = new File(zipFilePath);
// 创建输出流和压缩流
try (FileOutputStream outputStream = new FileOutputStream(zipFile);
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
// 遍历文件列表进行压缩
for (File file : fileList) {
if (file != null && file.exists() && file.isFile()) {
zipFile(file, zipOutputStream);
}
}
}
return zipFile;
}
/***
* Zip解压
*
* @param data 待解压数据
* @return 解压后数据
*/
public static Object unzip(Object data) {
if (!(data instanceof byte[] temp)) {
return data;
}
try (ByteArrayInputStream bis = new ByteArrayInputStream(temp);
ZipInputStream zip = new ZipInputStream(bis)) {
ByteArrayOutputStream bas = new ByteArrayOutputStream();
// 处理压缩包中的每个条目
while (zip.getNextEntry() != null) {
byte[] buf = new byte[1024];
int num;
// 读取数据到缓冲区并写入输出流
while ((num = zip.read(buf)) != -1) {
bas.write(buf, 0, num);
}
}
return bas.toByteArray();
} catch (Exception e) {
return data;
}
}
public static Object zipString(Object data) {
if (!(data instanceof String)) {
return data;
}
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(out)) {
deflaterOutputStream.write(((String) data).getBytes(StandardCharsets.UTF_8));
}
return Base64.encodeBase64String(out.toByteArray());
} catch (Exception e) {
return data;
}
}
public static Object unzipString(Object data) {
if (!(data instanceof String)) {
return data;
}
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
try (OutputStream outputStream = new InflaterOutputStream(os)) {
outputStream.write(Base64.decodeBase64((String) data));
}
return os.toString(StandardCharsets.UTF_8);
} catch (Exception e) {
return data;
}
}
}

View File

@@ -0,0 +1,81 @@
package cn.cordys.common.util;
import java.util.List;
import java.util.stream.Collectors;
/**
* 加密解密工具扩展自CodingUtils提供AES和MD5的加解密方法。
*/
public class EncryptUtils extends CodingUtils {
// 默认的加密密钥和向量
private static final String secretKey = "www.fit2cloud.cn";
/**
* AES加密方法
*
* @param o 要加密的对象
*
* @return 加密后的字符串若传入对象为空则返回null
*/
public static String aesEncrypt(Object o) {
if (o == null) {
return null;
}
return aesEncrypt(o.toString(), secretKey, generateIv());
}
/**
* AES解密方法
*
* @param o 要解密的对象
*
* @return 解密后的字符串若传入对象为空则返回null
*/
public static String aesDecrypt(Object o) {
if (o == null) {
return null;
}
return aesDecrypt(o.toString(), secretKey, generateIv());
}
/**
* 对列表中的对象属性进行AES解密
*
* @param o 要解密的对象列表
* @param attrName 需要解密的属性名
* @param <T> 对象类型
*
* @return 解密后的对象列表
*/
public static <T> Object aesDecrypt(List<T> o, String attrName) {
if (o == null || attrName == null) {
return null;
}
// 对列表中的每个对象属性进行解密
return o.stream()
.filter(element -> BeanUtils.getFieldValueByName(attrName, element) != null)
.peek(element -> {
Object fieldValue = BeanUtils.getFieldValueByName(attrName, element);
if (fieldValue != null) {
String decryptedValue = aesDecrypt(fieldValue.toString(), secretKey, generateIv());
BeanUtils.setFieldValueByName(element, attrName, decryptedValue, String.class);
}
})
.collect(Collectors.toList());
}
/**
* MD5加密方法
*
* @param o 要加密的对象
*
* @return 加密后的MD5字符串若传入对象为空则返回null
*/
public static String md5Encrypt(Object o) {
if (o == null) {
return null;
}
return md5(o.toString());
}
}

View File

@@ -0,0 +1,15 @@
package cn.cordys.common.util;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.CaseUtils;
public class FieldConverter {
public static String toCamelCaseWithCommons(String snakeCase) {
if (StringUtils.isBlank(snakeCase)) {
return snakeCase;
}
return CaseUtils.toCamelCase(snakeCase.toLowerCase(), false, '_');
}
}

View File

@@ -0,0 +1,62 @@
package cn.cordys.common.util;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HikariCPUtils {
/**
* 获取 HikariCP 连接池的使用情况
*
* @param dataSource HikariDataSource 实例
*
* @return HikariCP 连接池状态信息
*/
public static String getHikariCPStatus(HikariDataSource dataSource) {
if (dataSource == null) {
throw new IllegalArgumentException("HikariDataSource cannot be null");
}
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
return "HikariCP Status:\n" +
"Active Connections: " + poolMXBean.getActiveConnections() + "\n" +
"Idle Connections: " + poolMXBean.getIdleConnections() + "\n" +
"Total Connections: " + poolMXBean.getTotalConnections() + "\n" +
"Threads Awaiting Connection: " + poolMXBean.getThreadsAwaitingConnection() + "\n";
}
/**
* 获取 HikariCP 连接池的配置情况
*
* @param dataSource HikariDataSource 实例
*
* @return 连接池配置情况
*/
public static String getHikariCPConfig(HikariDataSource dataSource) {
if (dataSource == null) {
throw new IllegalArgumentException("HikariDataSource cannot be null");
}
return "HikariCP Configuration:\n" +
"Maximum Pool Size: " + dataSource.getMaximumPoolSize() + "\n" +
"Minimum Idle Connections: " + dataSource.getMinimumIdle() + "\n" +
"Connection Timeout: " + dataSource.getConnectionTimeout() + " ms\n" +
"Idle Timeout: " + dataSource.getIdleTimeout() + " ms\n" +
"Max Lifetime: " + dataSource.getMaxLifetime() + " ms\n";
}
/**
* 打印 HikariCP 的状态和配置信息
*/
public static void printHikariCPStatus() {
HikariDataSource dataSource = CommonBeanFactory.getBean(HikariDataSource.class);
if (dataSource == null) {
log.error("HikariDataSource not found");
return;
}
log.info(getHikariCPStatus(dataSource));
log.info(getHikariCPConfig(dataSource));
}
}

View File

@@ -0,0 +1,241 @@
package cn.cordys.common.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
* JSON 工具类,封装了常用的 JSON 序列化和反序列化方法。
* 支持对象与 JSON 字符串、字节数组、集合、映射等类型的相互转换。
*/
public class JSON {
// 默认最大字符串长度
public static final int DEFAULT_MAX_STRING_LEN = Integer.MAX_VALUE;
// ObjectMapper 实例,用于 JSON 操作
private static final ObjectMapper objectMapper = JsonMapper.builder()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS) // 允许 JSON 中未转义的控制字符
.build();
private static final TypeFactory typeFactory = objectMapper.getTypeFactory();
// 静态初始化块,配置 ObjectMapper
static {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 忽略未知属性
objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true); // 使用 BigDecimal 处理浮点数
objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true); // 允许 JSON 中的注释
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); // 自动检测所有类的字段属性
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); // 允许序列化空对象
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); // 接受单个值作为数组处理
objectMapper.getFactory()
.setStreamReadConstraints(StreamReadConstraints.builder().maxStringLength(DEFAULT_MAX_STRING_LEN).build()); // 设置读取字符流时的长度限制
objectMapper.registerModule(new JavaTimeModule()); // 注册 Java 8 时间模块
}
/**
* 将对象序列化为 JSON 字符串。
*
* @param value 需要序列化的对象
*
* @return JSON 字符串
*/
public static String toJSONString(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (IOException e) {
throw new RuntimeException("JSON 序列化失败", e);
}
}
/**
* 将对象序列化为格式化的 JSON 字符串(带缩进)。
*
* @param value 需要序列化的对象
*
* @return 格式化的 JSON 字符串
*/
public static String toFormatJSONString(Object value) {
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
} catch (IOException e) {
throw new RuntimeException("JSON 序列化失败", e);
}
}
/**
* 将对象序列化为字节数组。
*
* @param value 需要序列化的对象
*
* @return JSON 字节数组
*/
public static byte[] toJSONBytes(Object value) {
try {
return objectMapper.writeValueAsBytes(value);
} catch (IOException e) {
throw new RuntimeException("JSON 序列化失败", e);
}
}
/**
* 将 JSON 字符串反序列化为 Java 对象。
*
* @param content JSON 字符串
*
* @return 反序列化后的 Java 对象
*/
public static Object parseObject(String content) {
return parseObject(content, Object.class);
}
/**
* 将 JSON 字符串反序列化为指定类型的 Java 对象。
*
* @param content JSON 字符串
* @param valueType 目标 Java 类
* @param <T> Java 类的类型
*
* @return 反序列化后的 Java 对象
*/
public static <T> T parseObject(String content, Class<T> valueType) {
try {
return objectMapper.readValue(content, valueType);
} catch (IOException e) {
throw new RuntimeException("JSON 反序列化失败", e);
}
}
/**
* 将 JSON 字符串反序列化为指定类型的 Java 对象。
*
* @param content JSON 字符串
* @param valueType 目标 Java 类型引用
* @param <T> Java 类的类型
*
* @return 反序列化后的 Java 对象
*/
public static <T> T parseObject(String content, TypeReference<T> valueType) {
try {
return objectMapper.readValue(content, valueType);
} catch (IOException e) {
throw new RuntimeException("JSON 反序列化失败", e);
}
}
/**
* 将输入流中的 JSON 数据反序列化为 Java 对象。
*
* @param src 输入流
* @param valueType 目标 Java 类
* @param <T> Java 类的类型
*
* @return 反序列化后的 Java 对象
*/
public static <T> T parseObject(InputStream src, Class<T> valueType) {
try {
return objectMapper.readValue(src, valueType);
} catch (IOException e) {
throw new RuntimeException("JSON 反序列化失败", e);
}
}
/**
* 将 JSON 字符串反序列化为 Java 对象的集合。
*
* @param content JSON 字符串
*
* @return 反序列化后的集合对象
*/
public static List parseArray(String content) {
return parseArray(content, Object.class);
}
/**
* 将 JSON 字符串反序列化为指定类型的 Java 对象的集合。
*
* @param content JSON 字符串
* @param valueType 集合元素类型
* @param <T> 集合元素类型
*
* @return 反序列化后的集合对象
*/
public static <T> List<T> parseArray(String content, Class<T> valueType) {
CollectionType javaType = typeFactory.constructCollectionType(List.class, valueType);
try {
return objectMapper.readValue(content, javaType);
} catch (IOException e) {
throw new RuntimeException("JSON 反序列化失败", e);
}
}
/**
* 将 JSON 字符串反序列化为指定类型的 Java 对象的集合。
*
* @param content JSON 字符串
* @param valueType 集合元素类型引用
* @param <T> 集合元素类型
*
* @return 反序列化后的集合对象
*/
public static <T> List<T> parseArray(String content, TypeReference<T> valueType) {
try {
JavaType subType = typeFactory.constructType(valueType);
CollectionType javaType = typeFactory.constructCollectionType(List.class, subType);
return objectMapper.readValue(content, javaType);
} catch (IOException e) {
throw new RuntimeException("JSON 反序列化失败", e);
}
}
/**
* 将 JSON 字符串反序列化为 Map 对象。
*
* @param jsonObject JSON 字符串
*
* @return 反序列化后的 Map 对象
*/
public static Map parseMap(String jsonObject) {
try {
return objectMapper.readValue(jsonObject, new TypeReference<>() {
});
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 反序列化失败", e);
}
}
public static Map<String, Object> parseToMap(String jsonObject) {
try {
return objectMapper.readValue(jsonObject, new TypeReference<>() {
});
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 反序列化失败", e);
}
}
public static <T> T parseObject(InputStream src, TypeReference<T> valueType) {
try {
return objectMapper.readValue(src, valueType);
} catch (IOException e) {
throw new RuntimeException("JSON 反序列化失败", e);
}
}
public static final ObjectMapper MAPPER = new ObjectMapper();
}

View File

@@ -0,0 +1,150 @@
package cn.cordys.common.util;
import cn.cordys.common.dto.JsonDifferenceDTO;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.NullNode;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class JsonDifferenceUtils {
public static List<JsonDifferenceDTO> compareJson(String oldJson, String newJson, List<JsonDifferenceDTO> JsonDifferenceDTO) throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode oldNode = oldJson != null ? mapper.readTree(oldJson) : mapper.createObjectNode();
JsonNode newNode = newJson != null ? mapper.readTree(newJson) : mapper.createObjectNode();
if (oldNode.isArray() && newNode.isArray()) {
compareArrayNodesById(oldNode, newNode, JsonDifferenceDTO);
}
compareJsonNodes(oldNode, newNode, JsonDifferenceDTO);
return JsonDifferenceDTO;
}
private static void compareJsonNodes(JsonNode oldNode, JsonNode newNode, List<JsonDifferenceDTO> JsonDifferenceDTOList) {
Iterator<String> fieldNames1 = oldNode.fieldNames();
//遍历
while (fieldNames1.hasNext()) {
String fieldName = fieldNames1.next();
JsonNode oldValue = oldNode.get(fieldName);
JsonNode newValue = newNode.get(fieldName);
if ((newValue instanceof NullNode || newValue == null) && (oldValue instanceof NullNode || newValue == null)) {
// 都是 null 的情况
continue;
}
if (!newNode.has(fieldName)) {
//删除的属性
JsonDifferenceDTO removed = new JsonDifferenceDTO();
removed.setColumn(fieldName);
removed.setOldValue(getValue(oldValue));
removed.setType("removed");
JsonDifferenceDTOList.add(removed);
} else if (!isNodeEquals(oldValue, newValue)) {
//更新的属性
JsonDifferenceDTO diff = new JsonDifferenceDTO();
diff.setColumn(fieldName);
diff.setOldValue(getValue(oldValue));
diff.setNewValue(getValue(newValue));
diff.setType("modified");
JsonDifferenceDTOList.add(diff);
}
}
//遍历查找新增的属性
Iterator<String> fieldNames2 = newNode.fieldNames();
while (fieldNames2.hasNext()) {
String fieldName = fieldNames2.next();
if (!oldNode.has(fieldName)) {
JsonDifferenceDTO add = new JsonDifferenceDTO();
add.setColumn(fieldName);
add.setNewValue(getValue(newNode.get(fieldName)));
add.setType("add");
JsonDifferenceDTOList.add(add);
}
}
}
private static boolean isNodeEquals(JsonNode oldValue, JsonNode newValue) {
if (oldValue.isNumber() && newValue.isNumber()) {
// 避免小数点,科学计数法等格式导致的比较不一致
return oldValue.asDouble() == newValue.asDouble();
}
// 数字与字符串的跨类型比较(如计算字段: 99 vs "99"
if (oldValue.isNumber() && newValue.isTextual()) {
return isNumericTextEqual(oldValue, newValue);
}
if (oldValue.isTextual() && newValue.isNumber()) {
return isNumericTextEqual(newValue, oldValue);
}
return oldValue.equals(newValue);
}
/**
* 比较数值节点与文本节点是否相等
* 解决计算字段等场景中,同一值因类型不同(数字 vs 字符串)导致的误判
*
* @param numberNode 数值节点
* @param textNode 文本节点
* @return 是否相等
*/
private static boolean isNumericTextEqual(JsonNode numberNode, JsonNode textNode) {
try {
String text = textNode.asText();
if (text.contains(".") || text.contains("e") || text.contains("E")) {
return numberNode.asDouble() == Double.parseDouble(text);
}
return numberNode.asLong() == Long.parseLong(text);
} catch (NumberFormatException e) {
return false;
}
}
/**
* 比较数组节点 (ID作为节点唯一性)
*
* @param oldNode 旧节点
* @param newNode 新节点
* @param jsonDifferenceDTO 差异属性集合 [节点1:属性1:值1 => 值2]
*/
private static void compareArrayNodesById(JsonNode oldNode, JsonNode newNode, List<JsonDifferenceDTO> jsonDifferenceDTO) {
List<String> oldNodeIds = new ArrayList<>();
Iterator<JsonNode> elements = oldNode.elements();
while (elements.hasNext()) {
JsonNode oldElement = elements.next();
boolean found = false;
oldNodeIds.add(oldElement.get("id").asText());
for (JsonNode newElement : newNode) {
if (newElement.get("id").equals(oldElement.get("id"))) {
compareJsonNodes(oldElement, newElement, jsonDifferenceDTO);
found = true;
break;
}
}
if (!found) {
JsonDifferenceDTO removed = new JsonDifferenceDTO();
removed.setOldValue(oldElement.get("name"));
removed.setType("removed");
jsonDifferenceDTO.add(removed);
}
}
for (JsonNode newElement : newNode) {
if (!oldNodeIds.contains(newElement.get("id").asText())) {
JsonDifferenceDTO added = new JsonDifferenceDTO();
added.setNewValue(newElement.get("name"));
added.setType("add");
jsonDifferenceDTO.add(added);
}
}
}
public static Object getValue(JsonNode jsonNode) {
if (jsonNode.isArray() || jsonNode.isObject()) {
return JSON.parseObject(jsonNode.toString());
} else {
return jsonNode.asText();
}
}
}

View File

@@ -0,0 +1,130 @@
package cn.cordys.common.util;
import java.util.*;
/**
* 该类实现了 K-means 聚类算法的核心功能,包括数据点分配、质心更新等操作。
* 它提供了基于输入数据集和指定簇数进行 K-means 聚类的工具方法。
*/
public class KMeansUtils {
/**
* 执行 K-means 聚类算法。
*
* @param data 输入的数据集,每个数据点是一个特征向量。
* @param k 聚类的簇数。
*
* @return 包含每个数据点所属簇的列表,元素值为簇的索引。
*/
public static List<Integer> performKMeans(List<double[]> data, int k) {
int maxIterations = 100;
List<double[]> centroids = initializeCentroids(data, k);
List<Integer> clusters = new ArrayList<>(Collections.nCopies(data.size(), -1));
for (int iter = 0; iter < maxIterations; iter++) {
boolean converged = true;
// 第 1 步:将数据点分配给最近的质心
for (int i = 0; i < data.size(); i++) {
int closestCentroid = findClosestCentroid(data.get(i), centroids);
if (clusters.get(i) != closestCentroid) {
clusters.set(i, closestCentroid);
converged = false;
}
}
// 第 2 步:重新计算质心
for (int j = 0; j < k; j++) {
double[] newCentroid = recalculateCentroid(data, clusters, j);
if (!Arrays.equals(newCentroid, centroids.get(j))) {
centroids.set(j, newCentroid);
converged = false;
}
}
if (converged) break;
}
return clusters;
}
/**
* 随机初始化 K 个质心。
*
* @param data 输入的数据集,每个数据点是一个特征向量。
* @param k 聚类的簇数。
*
* @return 初始化的 K 个质心。
*/
private static List<double[]> initializeCentroids(List<double[]> data, int k) {
List<double[]> centroids = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < k; i++) {
centroids.add(data.get(random.nextInt(data.size())));
}
return centroids;
}
/**
* 计算一个数据点到所有质心的欧几里得距离,并返回距离最近的质心的索引。
*
* @param point 输入的数据点。
* @param centroids 所有的质心列表。
*
* @return 最近质心的索引。
*/
private static int findClosestCentroid(double[] point, List<double[]> centroids) {
double minDistance = Double.MAX_VALUE;
int closestCentroid = -1;
for (int i = 0; i < centroids.size(); i++) {
double distance = euclideanDistance(point, centroids.get(i));
if (distance < minDistance) {
minDistance = distance;
closestCentroid = i;
}
}
return closestCentroid;
}
/**
* 计算两个数据点之间的欧几里得距离。
*
* @param point1 第一个数据点。
* @param point2 第二个数据点。
*
* @return 两个数据点之间的欧几里得距离。
*/
private static double euclideanDistance(double[] point1, double[] point2) {
double sum = 0;
for (int i = 0; i < point1.length; i++) {
sum += Math.pow(point1[i] - point2[i], 2);
}
return Math.sqrt(sum);
}
/**
* 重新计算给定簇的质心。
*
* @param data 输入的数据集,每个数据点是一个特征向量。
* @param clusters 每个数据点所属的簇的索引。
* @param clusterIndex 当前簇的索引。
*
* @return 该簇的新质心。
*/
private static double[] recalculateCentroid(List<double[]> data, List<Integer> clusters, int clusterIndex) {
double[] centroid = new double[data.getFirst().length];
int count = 0;
for (int i = 0; i < data.size(); i++) {
if (clusters.get(i) == clusterIndex) {
for (int j = 0; j < centroid.length; j++) {
centroid[j] += data.get(i)[j];
}
count++;
}
}
for (int i = 0; i < centroid.length; i++) {
centroid[i] /= count;
}
return centroid;
}
}

View File

@@ -0,0 +1,104 @@
package cn.cordys.common.util;
import java.util.List;
/**
* 该类提供了用于训练和预测逻辑回归模型的工具方法。
*/
public class LogisticRegressionUtils {
// 学习率
private static final double LEARNING_RATE = 0.01;
// 最大迭代次数
private static final int ITERATIONS = 1000;
/**
* 使用梯度下降法训练逻辑回归模型。
*
* @param features 输入特征列表,每个元素为一个特征向量。
* @param labels 对应的标签列表,标签值为 0 或 1。
*
* @return 训练得到的权重数组。
*/
public static double[] train(List<double[]> features, List<Integer> labels) {
// 初始化权重
double[] weights = new double[features.getFirst().length];
// 进行迭代优化
for (int i = 0; i < ITERATIONS; i++) {
// 计算梯度
double[] gradients = computeGradients(features, labels, weights);
// 更新权重
for (int j = 0; j < weights.length; j++) {
weights[j] -= LEARNING_RATE * gradients[j];
}
}
return weights;
}
/**
* 计算给定特征和标签的梯度。
*
* @param features 输入特征列表,每个元素为一个特征向量。
* @param labels 对应的标签列表,标签值为 0 或 1。
* @param weights 当前的模型权重。
*
* @return 计算得到的梯度数组。
*/
private static double[] computeGradients(List<double[]> features, List<Integer> labels, double[] weights) {
double[] gradients = new double[weights.length];
for (int i = 0; i < features.size(); i++) {
// 计算预测值
double prediction = sigmoid(dotProduct(weights, features.get(i)));
int label = labels.get(i);
double error = prediction - label;
// 计算梯度
for (int j = 0; j < weights.length; j++) {
gradients[j] += error * features.get(i)[j];
}
}
return gradients;
}
/**
* 计算两个向量的点积。
*
* @param weights 模型的权重向量。
* @param feature 输入特征向量。
*
* @return 点积的值。
*/
private static double dotProduct(double[] weights, double[] feature) {
double sum = 0;
for (int i = 0; i < weights.length; i++) {
sum += weights[i] * feature[i];
}
return sum;
}
/**
* Sigmoid 激活函数,将线性输出转化为概率值。
*
* @param x 输入的线性值。
*
* @return 转化后的概率值(范围在 0 和 1 之间)。
*/
private static double sigmoid(double x) {
return 1.0 / (1.0 + Math.exp(-x));
}
/**
* 基于训练得到的权重和输入特征进行预测。
*
* @param weights 训练得到的模型权重。
* @param feature 输入的特征向量。
*
* @return 预测结果,返回 1 表示正类,返回 0 表示负类。
*/
public static int predict(double[] weights, double[] feature) {
double prediction = sigmoid(dotProduct(weights, feature));
return prediction >= 0.5 ? 1 : 0;
}
}

View File

@@ -0,0 +1,39 @@
package cn.cordys.common.util;
import cn.cordys.common.dto.NodeSortCountResultDTO;
public class NodeSortUtils {
//默认节点间隔
public static final long DEFAULT_NODE_INTERVAL_POS = 4096;
/**
* 计算排序
*
* @param previousNodePos 前一个节点的pos 如果没有节点则为-1
* @param nextNodePos 后一个节点的pos 如果没有节点则为-1
*
* @return 计算后的num值以及是否需要刷新整棵树的num(如果两个节点之间的num值小于2则需要刷新整棵树的num)
*/
public static NodeSortCountResultDTO countModuleSort(long previousNodePos, long nextNodePos) {
boolean refreshNum = false;
long num;
if (nextNodePos < 0 && previousNodePos < 0) {
num = 0;
} else if (nextNodePos < 0) {
num = previousNodePos + DEFAULT_NODE_INTERVAL_POS;
} else if (previousNodePos < 0) {
num = nextNodePos / 2;
if (num < 2) {
refreshNum = true;
}
} else {
long quantityDifference = (nextNodePos - previousNodePos) / 2;
if (quantityDifference <= 2) {
refreshNum = true;
}
num = previousNodePos + quantityDifference;
}
return new NodeSortCountResultDTO(refreshNum, num);
}
}

View File

@@ -0,0 +1,14 @@
package cn.cordys.common.util;
/**
* 执行单次接口
* @author song-cc-rock
*/
@FunctionalInterface
public interface OnceInterface {
/**
* 执行
*/
void execute();
}

View File

@@ -0,0 +1,16 @@
package cn.cordys.common.util;
/**
* 执行单次接口 (参数版本)
* @param <P> 参数类型
* @author song-cc-rock
*/
@FunctionalInterface
public interface OnceInterfaceAction<P> {
/**
* 执行 (参数)
* @param param 参数
*/
void execute(P param);
}

View File

@@ -0,0 +1,141 @@
package cn.cordys.common.util;
import cn.cordys.common.constants.MoveTypeEnum;
import cn.cordys.common.constants.QuadFunction;
import cn.cordys.common.dto.request.PosRequest;
import cn.cordys.common.exception.GenericException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
public class ServiceUtils {
// 用于排序的pos
public static final int POS_STEP = 4096;
/**
* 保存资源名称,在处理 NOT_FOUND 异常时,拼接资源名称
*/
private static final ThreadLocal<String> resourceName = new ThreadLocal<>();
// 反射元数据缓存,减少重复反射开销
private static final Map<Class<?>, Accessors> ACCESSOR_CACHE = new ConcurrentHashMap<>();
public static String getResourceName() {
return resourceName.get();
}
public static void clearResourceName() {
resourceName.remove();
}
public static <T> void updatePosFieldByAsc(
PosRequest request,
Class<T> clazz,
String userId,
String resourceType,
Function<String, T> selectByPrimaryKeyFunc,
QuadFunction<String, Long, String, String, Long> getPrePosFunc,
QuadFunction<String, Long, String, String, Long> getLastPosFunc,
Consumer<T> updateByPrimaryKeySelectiveFuc) {
updatePosField(request, clazz, userId, resourceType, selectByPrimaryKeyFunc, getPrePosFunc, getLastPosFunc, updateByPrimaryKeySelectiveFuc, true);
}
public static <T> void updatePosFieldByDesc(
PosRequest request,
Class<T> clazz,
String userId,
String resourceType,
Function<String, T> selectByPrimaryKeyFunc,
QuadFunction<String, Long, String, String, Long> getPrePosFunc,
QuadFunction<String, Long, String, String, Long> getLastPosFunc,
Consumer<T> updateByPrimaryKeySelectiveFuc) {
updatePosField(request, clazz, userId, resourceType, selectByPrimaryKeyFunc, getPrePosFunc, getLastPosFunc, updateByPrimaryKeySelectiveFuc, false);
}
private static <T> void updatePosField(
PosRequest request,
Class<T> clazz,
String userId,
String resourceType,
Function<String, T> selectByPrimaryKeyFunc,
QuadFunction<String, Long, String, String, Long> getPrePosFunc,
QuadFunction<String, Long, String, String, Long> getLastPosFunc,
Consumer<T> updateByPrimaryKeySelectiveFuc,
boolean asc) {
try {
Accessors acc = getAccessors(clazz);
// 获取移动的参考对象
T target = selectByPrimaryKeyFunc.apply(request.getTargetId());
if (target == null) {
// 如果参考对象被删除,则不处理
return;
}
Long targetPos = (Long) acc.getPos.invoke(target);
boolean moveAfter = MoveTypeEnum.AFTER.name().equals(request.getMoveMode());
Long neighborPos;
long pos;
if (asc) {
if (moveAfter) {
pos = targetPos + POS_STEP;
neighborPos = getLastPosFunc.apply(request.getOrgId(), targetPos, userId, resourceType);
} else {
pos = targetPos - POS_STEP;
neighborPos = getPrePosFunc.apply(request.getOrgId(), targetPos, userId, resourceType);
}
} else {
if (moveAfter) {
pos = targetPos - POS_STEP;
neighborPos = getPrePosFunc.apply(request.getOrgId(), targetPos, userId, resourceType);
} else {
pos = targetPos + POS_STEP;
neighborPos = getLastPosFunc.apply(request.getOrgId(), targetPos, userId, resourceType);
}
}
if (neighborPos != null) {
// 如果不是第一个或最后一个则取中间值
pos = (targetPos + neighborPos) / 2;
}
@SuppressWarnings("unchecked")
T updateObj = (T) acc.newInstance();
acc.setId.invoke(updateObj, request.getMoveId());
acc.setPos.invoke(updateObj, pos);
updateByPrimaryKeySelectiveFuc.accept(updateObj);
} catch (Exception e) {
throw new GenericException("更新 pos 字段失败: " + e.getMessage());
}
}
private static Accessors getAccessors(Class<?> clazz) {
return ACCESSOR_CACHE.computeIfAbsent(clazz, c -> {
try {
Method getPos = c.getMethod("getPos");
Method setId = c.getMethod("setId", String.class);
Method setPos = c.getMethod("setPos", Long.class);
Constructor<?> ctor = c.getDeclaredConstructor();
if (!ctor.canAccess(null)) {
ctor.setAccessible(true);
}
return new Accessors(getPos, setId, setPos, ctor);
} catch (Exception e) {
throw new GenericException("初始化反射元数据失败: " + e.getMessage());
}
});
}
private record Accessors(Method getPos, Method setId, Method setPos, Constructor<?> ctor) {
Object newInstance() throws Exception {
return ctor.newInstance();
}
}
}

View File

@@ -0,0 +1,59 @@
package cn.cordys.common.util;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 客户端工具类
*/
public class ServletUtils {
/**
* @param request 请求
*
* @return ua
*/
public static String getUserAgent(HttpServletRequest request) {
String ua = request.getHeader("User-Agent");
return ua != null ? ua : "";
}
/**
* 获得请求
*
* @return HttpServletRequest
*/
public static HttpServletRequest getRequest() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (!(requestAttributes instanceof ServletRequestAttributes)) {
return null;
}
return ((ServletRequestAttributes) requestAttributes).getRequest();
}
public static String getUserAgent() {
HttpServletRequest request = getRequest();
if (request == null) {
return null;
}
return getUserAgent(request);
}
public static String getRequestHost(HttpServletRequest request) {
String port = ":" + request.getServerPort();
if (request.getServerPort() == 80 || request.getServerPort() == 443) {
port = "";
}
return request.getScheme() + "://" + request.getServerName() + port;
}
public static String getUrl() {
HttpServletRequest request = getRequest();
if (request == null) {
return null;
}
return getRequestHost(request);
}
}

View File

@@ -0,0 +1,31 @@
package cn.cordys.common.util;
import org.apache.commons.collections4.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class SubListUtils {
public static int DEFAULT_EXPORT_BATCH_SIZE = 500;
public static int DEFAULT_QUERY_BATCH_SIZE = 200;
/**
* 将较长的数组截断成较短的数组进行批处理
*/
public static <T> void dealForSubList(List<T> totalList, int batchSize, Consumer<List<T>> subFunc) {
if (CollectionUtils.isEmpty(totalList)) {
return;
}
List<T> dealList = new ArrayList<>(totalList);
while (dealList.size() > batchSize) {
List<T> subList = dealList.subList(0, batchSize);
subFunc.accept(subList);
dealList = dealList.subList(subList.size(), dealList.size());
}
if (CollectionUtils.isNotEmpty(dealList)) {
subFunc.accept(dealList);
}
}
}

View File

@@ -0,0 +1,79 @@
package cn.cordys.common.util;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* DateUtils provides date formatting, parsing
*/
public abstract class TimeUtils extends DateUtils {
/**
* Patterns
*/
public static final String MONTH_PATTERN = "yyyy-MM";
public static final String DAY_PATTERN = "yyyy-MM-dd";
public static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* Parse date by 'yyyy-MM-dd' pattern
*/
public static Date parseByDayPattern(String str) {
return parseDate(str, DAY_PATTERN);
}
/**
* Parse date without Checked exception
*
* @throws RuntimeException when ParseException occurred
*/
public static Date parseDate(String str, String pattern) {
try {
return parseDate(str, new String[]{pattern});
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* Format date by 'yyyy-MM-dd HH:mm:ss' pattern
*/
public static String formatByDateTimePattern(Date date) {
return DateFormatUtils.format(date, DATETIME_PATTERN);
}
public static String getMonthStr(Long timeStamp) {
if (timeStamp == null) {
return StringUtils.EMPTY;
}
SimpleDateFormat dateFormat = new SimpleDateFormat(MONTH_PATTERN);
return dateFormat.format(timeStamp);
}
public static String getDateStr(Long timeStamp) {
if (timeStamp == null) {
return StringUtils.EMPTY;
}
SimpleDateFormat dateFormat = new SimpleDateFormat(DAY_PATTERN);
return dateFormat.format(timeStamp);
}
public static String getDateTimeStr(Long timeStamp) {
if (timeStamp == null) {
return null;
}
SimpleDateFormat dateFormat = new SimpleDateFormat(DATETIME_PATTERN);
return dateFormat.format(timeStamp);
}
public static Long getTodayStart() {
Date startDate = parseDate(DateFormatUtils.format(new Date(), DAY_PATTERN) + " 00:00:00", DATETIME_PATTERN);
return startDate.getTime();
}
}

View File

@@ -0,0 +1,76 @@
package cn.cordys.common.util;
import jakarta.annotation.Resource;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import java.util.Locale;
/**
* 翻译工具类,用于从消息源中获取本地化的消息。
* <p>
* 该类提供了根据消息键key从消息源中获取翻译的功能。
* </p>
*/
public class Translator {
private static MessageSource messageSource;
/**
* 根据给定的消息键获取翻译内容。
*
* @param key 消息键
*
* @return 翻译后的消息,如果没有找到对应的消息,则返回 "Not Support Key: " + key
*/
public static String get(String key) {
return messageSource.getMessage(key, null, "Not Support Key: " + key, LocaleContextHolder.getLocale());
}
/**
* 根据给定的消息键获取翻译内容,如果没有找到对应的消息,则返回指定的默认消息。
*
* @param key 消息键
* @param defaultMessage 默认消息
*
* @return 翻译后的消息,若未找到则返回默认消息
*/
public static String get(String key, String defaultMessage) {
return messageSource.getMessage(key, null, defaultMessage, LocaleContextHolder.getLocale());
}
/**
* 根据给定的消息键和指定的语言环境获取翻译内容。
*
* @param key 消息键
* @param locale 指定的语言环境
*
* @return 翻译后的消息
*/
public static String get(String key, Locale locale) {
return messageSource.getMessage(key, null, locale);
}
/**
* 根据给定的消息键和参数获取翻译内容。
* 支持格式化参数的插入。
*
* @param key 消息键
* @param args 格式化参数
*
* @return 翻译后的消息
*/
public static String getWithArgs(String key, Object... args) {
return messageSource.getMessage(key, args, "Not Support Key: " + key, LocaleContextHolder.getLocale());
}
/**
* 注入 MessageSource 用于国际化消息处理。
*
* @param messageSource Spring 的 MessageSource 实例
*/
@Resource
public void setMessageSource(MessageSource messageSource) {
Translator.messageSource = messageSource;
}
}

View File

@@ -0,0 +1,27 @@
package cn.cordys.common.util.rsa;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
/**
* RSA 密钥对类,封装公钥和私钥。
* <p>
* 该类用于存储 RSA 加密算法中的公钥和私钥,供加密解密操作使用。
* </p>
*/
@Setter
@Getter
public class RsaKey implements Serializable {
/**
* 公钥,使用 RSA 加密时的公钥部分。
*/
private String publicKey;
/**
* 私钥,使用 RSA 解密时的私钥部分。
*/
private String privateKey;
}

View File

@@ -0,0 +1,245 @@
package cn.cordys.common.util.rsa;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* RSA 加密解密工具类,提供 RSA 算法相关的加密、解密、密钥生成等操作。
* <p>
* 该类支持使用 RSA 公钥和私钥进行加密、解密、密钥生成等操作。
* </p>
*/
public class RsaUtils {
/**
* 字符编码集
*/
public static final String CHARSET = StandardCharsets.UTF_8.name();
/**
* RSA 算法名称
*/
public static final String RSA_ALGORITHM = "RSA";
/**
* RSA 加密填充方式
*/
private static final String RSA_CIPHER_TRANSFORMATION_OAEP = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
/**
* RSA 密钥对缓存
*/
private static RsaKey rsaKey;
/**
* 获取当前的 RSA 密钥对,如果未创建则生成新的密钥对。
*
* @return 当前的 RSA 密钥对
*
* @throws NoSuchAlgorithmException 如果无法生成密钥对
*/
public static RsaKey getRsaKey() throws NoSuchAlgorithmException {
if (rsaKey == null) {
rsaKey = createKeys();
}
return rsaKey;
}
/**
* 设置 RSA 密钥对。
*
* @param rsaKey RSA 密钥对
*/
public static void setRsaKey(RsaKey rsaKey) {
RsaUtils.rsaKey = rsaKey;
}
/**
* 创建一个 1024 位的 RSA 密钥对。
*
* @return 生成的 RSA 密钥对
*
* @throws NoSuchAlgorithmException 如果无法生成密钥对
*/
public static RsaKey createKeys() throws NoSuchAlgorithmException {
return createKeys(1024);
}
/**
* 创建一个指定大小的 RSA 密钥对。
*
* @param keySize 密钥大小例如10242048
*
* @return 生成的 RSA 密钥对
*
* @throws NoSuchAlgorithmException 如果无法生成密钥对
*/
public static RsaKey createKeys(int keySize) throws NoSuchAlgorithmException {
// 创建 RSA 密钥对生成器
KeyPairGenerator kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
kpg.initialize(keySize);
// 生成密钥对
KeyPair keyPair = kpg.generateKeyPair();
// 获取公钥和私钥并进行 Base64 编码
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyStr = Base64.getEncoder().encodeToString(publicKey.getEncoded());
String privateKeyStr = Base64.getEncoder().encodeToString(privateKey.getEncoded());
// 封装密钥对
RsaKey rsaKey = new RsaKey();
rsaKey.setPublicKey(publicKeyStr);
rsaKey.setPrivateKey(privateKeyStr);
return rsaKey;
}
/**
* 使用私钥对原文进行加密。
*
* @param originalText 原文
* @param privateKey 私钥Base64 编码)
*
* @return 加密后的密文
*
* @throws NoSuchAlgorithmException 如果加密失败
*/
public static String privateEncrypt(String originalText, String privateKey) throws Exception {
RSAPrivateKey rsaPrivateKey = getPrivateKey(privateKey);
return privateEncrypt(originalText, rsaPrivateKey);
}
/**
* 使用私钥对原文进行加密。
*
* @param originalText 原文
* @param privateKey 私钥
*
* @return 加密后的密文
*
* @throws Exception 如果加密失败
*/
private static String privateEncrypt(String originalText, RSAPrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_CIPHER_TRANSFORMATION_OAEP);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] encryptedData = rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, originalText.getBytes(CHARSET), privateKey.getModulus().bitLength());
return Base64.getEncoder().encodeToString(encryptedData);
}
/**
* 使用私钥对密文进行解密。
*
* @param cipherText 密文
* @param privateKey 私钥Base64 编码)
*
* @return 解密后的原文
*
* @throws NoSuchAlgorithmException 如果解密失败
*/
public static String privateDecrypt(String cipherText, String privateKey) throws Exception {
RSAPrivateKey rsaPrivateKey = getPrivateKey(privateKey);
return privateDecrypt(cipherText, rsaPrivateKey);
}
/**
* 使用私钥对密文进行解密。
*
* @param cipherText 密文
* @param privateKey 私钥
*
* @return 解密后的原文
*
* @throws Exception 如果解密失败
*/
private static String privateDecrypt(String cipherText, RSAPrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedData = rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.getDecoder().decode(cipherText), privateKey.getModulus().bitLength());
return new String(decryptedData, CHARSET);
}
/**
* 获取私钥对象。
*
* @param privateKey 私钥Base64 编码)
*
* @return 私钥对象
*
* @throws NoSuchAlgorithmException 如果无法获取私钥
*/
private static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException {
try {
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的私钥格式", e);
}
}
/**
* 分块处理加密和解密过程,避免一次性数据过大导致的内存问题。
*
* @param cipher 加解密器
* @param opmode 操作模式(加密/解密)
* @param data 待加解密的数据
* @param keySize 密钥大小
*
* @return 处理后的数据
*
* @throws Exception 如果处理过程中发生错误
*/
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] data, int keySize) throws Exception {
int maxBlock = (opmode == Cipher.DECRYPT_MODE) ? keySize / 8 : keySize / 8 - 11;
int offset = 0;
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
while (offset < data.length) {
int length = Math.min(maxBlock, data.length - offset);
byte[] buffer = cipher.doFinal(data, offset, length);
out.write(buffer, 0, buffer.length);
offset += length;
}
return out.toByteArray();
}
}
// 使用公钥加密
public static String publicEncrypt(String originalText, String publicKey) throws Exception {
RSAPublicKey rsaPublicKey = getPublicKey(publicKey);
return publicEncrypt(originalText, rsaPublicKey);
}
private static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException {
try {
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("无效的公钥格式", e);
}
}
private static String publicEncrypt(String originalText, RSAPublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedData = rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, originalText.getBytes(CHARSET), publicKey.getModulus().bitLength());
return Base64.getEncoder().encodeToString(encryptedData);
}
}

View File

@@ -0,0 +1,58 @@
package cn.cordys.config;
import cn.cordys.mybatis.BaseMapper;
import cn.cordys.mybatis.DataAccessLayer;
import jakarta.annotation.Resource;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.ResolvableType;
import java.util.Objects;
/**
* 数据访问配置类。
* 提供 MyBatis 的 {@link BaseMapper} 的动态注入支持。
*/
@Configuration
public class DataAccessConfig {
/**
* 注入的 MyBatis {@link SqlSession},用于操作数据库。
*/
@Resource
private SqlSession sqlSession;
/**
* 提供泛型 {@link BaseMapper} 的动态实例。
* 使用 Spring 的原型作用域,每次注入时动态解析泛型类型并实例化。
*
* @param injectionPoint 当前注入点的信息,用于解析目标泛型类型
* @param <E> 泛型参数,表示实体类类型
*
* @return 对应实体类的 {@link BaseMapper} 实例
*
* @throws IllegalArgumentException 如果注入点的字段类型无法解析
*/
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public <E> BaseMapper<E> simpleBaseMapper(InjectionPoint injectionPoint) {
// 解析注入点字段的泛型类型
ResolvableType resolved = ResolvableType.forField(
Objects.requireNonNull(injectionPoint.getField(), "InjectionPoint 的字段信息不能为空")
);
// 获取泛型参数类型并验证
@SuppressWarnings("unchecked")
Class<E> parameterClass = (Class<E>) Objects.requireNonNull(
resolved.getGeneric(0).resolve(),
"无法解析泛型参数类型,请确认使用了明确的泛型声明"
);
// 返回对应的 BaseMapper 实例
return DataAccessLayer.with(parameterClass, sqlSession);
}
}

View File

@@ -0,0 +1,69 @@
package cn.cordys.config;
import cn.cordys.common.util.Translator;
import jakarta.validation.Validator;
import org.hibernate.validator.HibernateValidator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
/**
* 配置类用于设置国际化和校验相关的Bean。
* <p>
* 该配置类包括:
* 1. 国际化翻译器 (Translator) 的 Bean 配置。
* 2. 使用 JSR-303 规范的校验器 (Validator),并设置国际化消息源。
* </p>
*/
@Configuration
public class I18nConfig {
/**
* 创建 Translator Bean提供国际化的翻译功能。
* <p>
* 该 Bean 仅在没有其他 Translator Bean 的情况下创建。
* </p>
*
* @return Translator 对象
*/
@Bean
@ConditionalOnMissingBean
public Translator translator() {
return new Translator();
}
/**
* 配置 JSR-303 校验的国际化消息源。
* <p>
* 使用 Hibernate Validator 作为校验提供者,并将指定的 MessageSource 作为消息源。
* </p>
*
* @param messageSource 消息源,用于提供国际化的错误信息
*
* @return 配置好的 LocalValidatorFactoryBean
*/
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean(MessageSource messageSource) {
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
localValidatorFactoryBean.setProviderClass(HibernateValidator.class);
localValidatorFactoryBean.setValidationMessageSource(messageSource);
return localValidatorFactoryBean;
}
/**
* 创建 Validator Bean用于执行 JSR-303 校验。
* <p>
* 该 Bean 使用 LocalValidatorFactoryBean 作为校验工厂,提供一个验证器实例。
* </p>
*
* @param localValidatorFactoryBean 校验工厂
*
* @return 校验器实例
*/
@Bean
public Validator validator(LocalValidatorFactoryBean localValidatorFactoryBean) {
return localValidatorFactoryBean.getValidator();
}
}

Some files were not shown because too many files have changed in this diff Show More