1.初始化版本,2.引入thirdpart模块,引入canteen模块,初始化菜品分类,spu

This commit is contained in:
zjf committed 2024-12-06 16:54:30 +08:00
commit aff77150d6
1595 files changed
+168833

No files matched your search

@@ -0,0 +1,38 @@
<?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>com.bsd</groupId>
<artifactId>pjcloud-common</artifactId>
<version>5.7.0</version>
</parent>
<artifactId>pjcloud-common-audit</artifactId>
<packaging>jar</packaging>
<description>pjcloud 数据审计相关工具类</description>
<dependencies>
<!-- 数据对比实现 -->
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-core</artifactId>
</dependency>
<!-- 切面依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 提供日志插入 API -->
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-upms-api</artifactId>
</dependency>
<!-- 获取上下文的操作用户 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,51 @@
package com.bsd.pjcloud.common.audit;
import com.bsd.pjcloud.admin.api.feign.RemoteAuditLogService;
import com.bsd.pjcloud.common.audit.aop.AuditAspect;
import com.bsd.pjcloud.common.audit.handle.DefaultAuditLogHandle;
import com.bsd.pjcloud.common.audit.handle.IAuditLogHandle;
import com.bsd.pjcloud.common.audit.handle.ICompareHandle;
import com.bsd.pjcloud.common.audit.handle.JavesCompareHandle;
import com.bsd.pjcloud.common.audit.support.SpelParser;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.Optional;
/**
* 审计自动配置类
*
* @author pjcloud
* @date 2023/2/26
*/
@EnableAsync
@AutoConfiguration
@Import({ AuditAspect.class, SpelParser.class })
public class AuditAutoConfiguration {
/**
* 默认注入 javers 的比较器实现
* @param auditNameHandleOptional 注入审计用户来源
* @return ICompareHandle
*/
@Bean
@ConditionalOnMissingBean
public ICompareHandle compareHandle(Optional<IAuditLogHandle> auditNameHandleOptional) {
return new JavesCompareHandle(auditNameHandleOptional);
}
/**
* 默认的审计日志存储策略
* @return DefaultAuditLogHandle
*/
@Bean
@ConditionalOnMissingBean
public IAuditLogHandle auditLogHandle(RemoteAuditLogService logService, KeyStrResolver tenantKeyStrResolver) {
return new DefaultAuditLogHandle(logService, tenantKeyStrResolver);
}
}
@@ -0,0 +1,47 @@
package com.bsd.pjcloud.common.audit.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author pjcloud
*
* 记需要进行审计的方法
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {
/**
* 此审计的名称
* @return 审计名称
*/
String name();
@AliasFor("spel")
String value() default "";
/**
* 默认查询的表达式
* @return string
*/
@AliasFor("value")
String spel() default "";
/**
* 查询原有结果的表达式,如果为空取 spel()
* @return string
*/
String oldVal() default "";
/**
* 查询编辑后的表达式,如果为空取 spel()
* @return string
*/
String newVal() default "";
}
@@ -0,0 +1,36 @@
package com.bsd.pjcloud.common.audit.aop;
import com.bsd.pjcloud.common.audit.annotation.Audit;
import com.bsd.pjcloud.common.audit.handle.ICompareHandle;
import com.bsd.pjcloud.common.audit.support.SpelParser;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.util.StringUtils;
/**
* @author pjcloud
* @date 2023/2/26
*
* 拦截被@Audit注解标记的方法,并记录前后变化值
*/
@Aspect
@RequiredArgsConstructor
public class AuditAspect {
private final ICompareHandle compareHandle;
@Around("@annotation(audit)")
public Object auditLog(ProceedingJoinPoint joinPoint, Audit audit) throws Throwable {
// 获取变更之前的结果
Object oldVal = SpelParser.parser(joinPoint,
StringUtils.hasText(audit.oldVal()) ? audit.oldVal() : audit.spel());
Object result = joinPoint.proceed();
compareHandle.compare(oldVal, joinPoint, audit);
return result;
}
}
@@ -0,0 +1,83 @@
package com.bsd.pjcloud.common.audit.handle;
import com.bsd.pjcloud.admin.api.entity.SysAuditLog;
import com.bsd.pjcloud.admin.api.feign.RemoteAuditLogService;
import com.bsd.pjcloud.common.audit.annotation.Audit;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.javers.core.Changes;
import org.javers.core.diff.Change;
import org.javers.core.diff.changetype.ValueChange;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 默认的审计日志存储
*
* @author pjcloud
* @date 2023/2/27
*/
@Slf4j
@RequiredArgsConstructor
public class DefaultAuditLogHandle implements IAuditLogHandle {
private final RemoteAuditLogService remoteLogService;
private final KeyStrResolver tenantKeyStrResolver;
@Override
public void handle(Audit audit, Changes changes) {
// 如果变更项为空则不进行审计
if (changes.isEmpty()) {
return;
}
// 获取当前操作人
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 如果获取不到授权信息、或者没有身份信息的接口 直接跳过处理
if (Objects.isNull(authentication) || !authentication.isAuthenticated()) {
return;
}
List<SysAuditLog> auditLogList = new ArrayList<>();
for (Change change : changes) {
ValueChange valueChange = (ValueChange) change;
SysAuditLog auditLog = new SysAuditLog();
auditLog.setAuditName(audit.name());
auditLog.setAuditField(valueChange.getPropertyName()); // 修改的字段名称
if (Objects.nonNull(valueChange.getLeft())) {
auditLog.setBeforeVal(valueChange.getLeft().toString()); // 更改前的值
}
if (Objects.nonNull(valueChange.getRight())) {
auditLog.setAfterVal(valueChange.getRight().toString()); // getRight
}
auditLog.setCreateBy(authentication.getName()); // 操作人
auditLog.setCreateTime(LocalDateTime.now()); // 操作时间
auditLog.setTenantId(Long.parseLong(tenantKeyStrResolver.key())); // 设置操作所属租户
auditLogList.add(auditLog);
}
// 异步保存日志,提升性能
IAuditLogHandle auditLogHandle = SpringContextHolder.getBean(IAuditLogHandle.class);
auditLogHandle.asyncSend(auditLogList);
}
@Async
public void asyncSend(List<SysAuditLog> auditLogList) {
remoteLogService.saveLog(auditLogList);
}
}
@@ -0,0 +1,21 @@
package com.bsd.pjcloud.common.audit.handle;
import com.bsd.pjcloud.admin.api.entity.SysAuditLog;
import com.bsd.pjcloud.common.audit.annotation.Audit;
import org.javers.core.Changes;
import java.util.List;
/**
* @author pjcloud
* @date 2023/2/26
*
* 审计日志处理器
*/
public interface IAuditLogHandle {
void handle(Audit audit, Changes changes);
void asyncSend(List<SysAuditLog> auditLogList);
}
@@ -0,0 +1,22 @@
package com.bsd.pjcloud.common.audit.handle;
import com.bsd.pjcloud.common.audit.annotation.Audit;
import org.aspectj.lang.ProceedingJoinPoint;
import org.javers.core.Changes;
/**
* 比较器抽象类
*
* @author pjcloud
* @date 2023/2/26
*/
public interface ICompareHandle {
/**
* 比较两个对象是否变更,及其变更后如何审计
* @param oldVal 原有值
* @param newVal 变更后值
*/
Changes compare(Object oldVal, ProceedingJoinPoint joinPoint, Audit audit);
}
@@ -0,0 +1,37 @@
package com.bsd.pjcloud.common.audit.handle;
import com.bsd.pjcloud.common.audit.annotation.Audit;
import com.bsd.pjcloud.common.audit.support.DataAuditor;
import com.bsd.pjcloud.common.audit.support.SpelParser;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.javers.core.Changes;
import org.springframework.util.StringUtils;
import java.util.Optional;
/**
* javers 比较实现
*
* @author pjcloud
* @date 2023/2/26
*/
@RequiredArgsConstructor
public class JavesCompareHandle implements ICompareHandle {
private final Optional<IAuditLogHandle> auditLogHandleOptional;
/**
* 比较两个对象是否变更,及其变更后如何审计
* @param oldVal 原有值
*/
@Override
public Changes compare(Object oldVal, ProceedingJoinPoint joinPoint, Audit audit) {
Object newVal = SpelParser.parser(joinPoint,
StringUtils.hasText(audit.newVal()) ? audit.newVal() : audit.spel());
Changes compare = DataAuditor.compare(oldVal, newVal);
auditLogHandleOptional.ifPresent(handle -> handle.handle(audit, compare));
return compare;
}
}
@@ -0,0 +1,27 @@
package com.bsd.pjcloud.common.audit.support;
import lombok.experimental.UtilityClass;
import org.javers.core.Changes;
import org.javers.core.Javers;
import org.javers.core.JaversBuilder;
import org.javers.core.diff.Diff;
import static org.javers.core.diff.ListCompareAlgorithm.LEVENSHTEIN_DISTANCE;
/**
* javers 审计工具
*
* @author pjcloud
* @date 2023/2/26
*/
@UtilityClass
public class DataAuditor {
private final Javers javers = JaversBuilder.javers().withListCompareAlgorithm(LEVENSHTEIN_DISTANCE).build();
public Changes compare(Object newObj, Object oldObj) {
Diff compare = javers.compare(newObj, oldObj);
return compare.getChanges();
}
}
@@ -0,0 +1,39 @@
package com.bsd.pjcloud.common.audit.support;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* 表达式处理器
*
* @author pjcloud
* @date 2023/2/26
*/
public class SpelParser implements ApplicationContextAware {
private final static SpelExpressionParser parser = new SpelExpressionParser();
private static ApplicationContext applicationContext;
public static Object parser(ProceedingJoinPoint joinPoint, String spel) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
for (int i = 0; i < joinPoint.getArgs().length; i++) {
String paramName = ((MethodSignature) joinPoint.getSignature()).getParameterNames()[i];
context.setVariable(paramName, joinPoint.getArgs()[i]);
}
return parser.parseExpression(spel).getValue(context);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpelParser.applicationContext = applicationContext;
}
}
@@ -0,0 +1 @@
com.bsd.pjcloud.common.audit.AuditAutoConfiguration
+530
View File
@@ -0,0 +1,530 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>group.springframework</groupId>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<version>2024.0.0</version>
<relativePath/>
</parent>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-bom</artifactId>
<packaging>pom</packaging>
<version>${pjcloud.version}</version>
<description>pjcloud 公共版本控制,在此pom定义的依赖,全局业务微服务不需要指定版本。</description>
<properties>
<pjcloud.version>5.7.0</pjcloud.version>
<mybatis-plus.version>3.5.9</mybatis-plus.version>
<mybatis-plus-join.version>1.5.2</mybatis-plus-join.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<druid.version>1.2.23</druid.version>
<hutool.version>5.8.34</hutool.version>
<mysql.connector.version>8.3.0</mysql.connector.version>
<oracle.version>21.3.0.0</oracle.version>
<sqlserver.version>8.4.1.jre8</sqlserver.version>
<dm.version>8.1.3.140</dm.version>
<highgo.version>6.2.0</highgo.version>
<knife4j.version>3.0.5</knife4j.version>
<springdoc.version>2.1.0</springdoc.version>
<swagger.core.version>2.2.14</swagger.core.version>
<mp.weixin.version>4.6.7.B</mp.weixin.version>
<ijpay.version>2.9.10-17</ijpay.version>
<groovy.version>3.0.3</groovy.version>
<javax.version>4.0.1</javax.version>
<jsoup.version>1.15.3</jsoup.version>
<aviator.version>5.4.3</aviator.version>
<flowable.version>7.0.0</flowable.version>
<security.oauth.version>2.5.2.RELEASE</security.oauth.version>
<fastjson.version>1.2.83</fastjson.version>
<xxl.job.version>2.4.2</xxl.job.version>
<aliyun.version>3.0.52.ALL</aliyun.version>
<aws.version>1.12.261</aws.version>
<javers.version>7.3.3</javers.version>
<seata.version>1.7.0</seata.version>
<asm.version>7.1</asm.version>
<sensitive.word.version>0.21.0</sensitive.word.version>
<log4j2.version>2.17.1</log4j2.version>
<docker.plugin.version>0.33.0</docker.plugin.version>
<cloud.plugin.version>1.0.0</cloud.plugin.version>
<sentinel.version>1.8.4</sentinel.version>
<anyline.version>8.7.2-jdk17-20240808</anyline.version>
<sms4j.version>3.2.1</sms4j.version>
<jakarta.mail.version>2.0.1</jakarta.mail.version>
<dashscope.version>2.16.5</dashscope.version>
<velocity.version>2.3</velocity.version>
<common.io.version>2.16.1</common.io.version>
<velocity.tool.version>3.1</velocity.tool.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-core</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-audit</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-data</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-gateway</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-gray</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-datasource</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-idempotent</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-job</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-log</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-oss</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-security</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-sensitive</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-sentinel</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-feign</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-sequence</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-swagger</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-seata</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-xss</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-sse</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-websocket</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-encrypt-api</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-excel</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-upms-api</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-app-server-api</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-flow-task-api</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-flow-engine-api</artifactId>
<version>${pjcloud.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
<!-- 必备:敏感词-->
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-word</artifactId>
<version>${sensitive.word.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${dynamic-ds.version}</version>
</dependency>
<!-- 连表查询依赖 -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<version>${mybatis-plus-join.version}</version>
</dependency>
<!-- 连表查询依赖 -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-annotation</artifactId>
<version>${mybatis-plus-join.version}</version>
</dependency>
<!-- druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!--mysql 驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<!--oracle 驱动-->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>${oracle.version}</version>
</dependency>
<!-- mssql -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>${sqlserver.version}</version>
</dependency>
<!--DM8-->
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<version>${dm.version}</version>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmDialect-for-hibernate6.1</artifactId>
<version>${dm.version}</version>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>HgdbJdbc</artifactId>
<version>${highgo.version}</version>
</dependency>
<!--anyline-->
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-environment-spring-data-jdbc</artifactId>
<version>${anyline.version}</version>
</dependency>
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-data-jdbc-mysql</artifactId>
<version>${anyline.version}</version>
</dependency>
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-data-jdbc-oracle</artifactId>
<version>${anyline.version}</version>
</dependency>
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-data-jdbc-postgresql</artifactId>
<version>${anyline.version}</version>
</dependency>
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-data-jdbc-dm</artifactId>
<version>${anyline.version}</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- commons.io -->
<dependency>
<artifactId>commons-io</artifactId>
<groupId>commons-io</groupId>
<version>${common.io.version}</version>
</dependency>
<!--计算引擎-->
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>${aviator.version}</version>
</dependency>
<!-- 对象对比工具-->
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-core</artifactId>
<version>${javers.version}</version>
</dependency>
<!--springdoc -->
<dependency>
<groupId>io.springboot</groupId>
<artifactId>knife4j-openapi3-ui</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!--springdoc -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
<version>${swagger.core.version}</version>
</dependency>
<!--微信依赖-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>${mp.weixin.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-cp</artifactId>
<version>${mp.weixin.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-io</artifactId>
<groupId>commons-io</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-common</artifactId>
<version>${mp.weixin.version}</version>
</dependency>
<!--支付相关SDK-->
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>${ijpay.version}</version>
</dependency>
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-AliPay</artifactId>
<version>${ijpay.version}</version>
</dependency>
<!--定义groovy 版本-->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>${groovy.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax.version}</version>
</dependency>
<!--稳定版本,替代spring security bom内置-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${security.oauth.version}</version>
</dependency>
<!--jsoup html 解析组件-->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
</dependency>
<!--工作流依赖-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
<version>${flowable.version}</version>
</dependency>
<!-- 短信发送工具类 -->
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-spring-boot-starter</artifactId>
<version>${sms4j.version}</version>
</dependency>
<!-- 邮件发送工具 -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>${jakarta.mail.version}</version>
</dependency>
<!-- 阿里云百炼工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>${dashscope.version}</version>
<exclusions>
<exclusion>
<artifactId>slf4j-simple</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!--webhook-->
<dependency>
<groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-oa-core</artifactId>
<version>${sms4j.version}</version>
</dependency>
<!-- velocity -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<dependency>
<groupId>org.apache.velocity.tools</groupId>
<artifactId>velocity-tools-generic</artifactId>
<version>${velocity.tool.version}</version>
</dependency>
<!-- 指定 log4j 版本-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>${log4j2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>${log4j2.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--hutool bom-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bom</artifactId>
<version>${hutool.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>${sentinel.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-servlet</artifactId>
<version>${sentinel.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>${sentinel.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>${sentinel.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-api-gateway-adapter-common</artifactId>
<version>${sentinel.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 增加云效nexus示例仓库 (演示使用,可自行删除) -->
<distributionManagement>
<repository>
<id>rdc-releases</id>
<url>https://packages.aliyun.com/maven/repository/2161442-release-DcBZC1/</url>
</repository>
<snapshotRepository>
<id>rdc-snapshots</id>
<url>https://packages.aliyun.com/maven/repository/2161442-snapshot-FzKqZK/</url>
</snapshotRepository>
</distributionManagement>
</project>
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2025, pjcloud All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
~
~ Redistributions of source code must retain the above copyright notice,
~ this list of conditions and the following disclaimer.
~ Redistributions in binary form must reproduce the above copyright
~ notice, this list of conditions and the following disclaimer in the
~ documentation and/or other materials provided with the distribution.
~ Neither the name of the pig4cloud.com developer nor the names of its
~ contributors may be used to endorse or promote products derived from
~ this software without specific prior written permission.
~ Author: pjcloud
~
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common</artifactId>
<version>5.7.0</version>
</parent>
<artifactId>pjcloud-common-core</artifactId>
<packaging>jar</packaging>
<description>pjcloud 公共工具类核心包</description>
<dependencies>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
</dependency>
<!--mvc 相关配置-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<scope>provided</scope>
</dependency>
<!--server-api-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<!--hibernate-validator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--json模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<!--TTL-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${ttl.version}</version>
</dependency>
<!--swagger 依赖-->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,26 @@
package com.bsd.pjcloud.common.core.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* 明确异步线程池配置,避免和 @EnableScheduling 造成的线程池冲突
*
* @author pjcloud
* @date 2024/4/20
*/
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class AsyncConfiguration implements AsyncConfigurer {
private final ThreadPoolTaskExecutor applicationTaskExecutor;
@Override
public Executor getAsyncExecutor() {
return applicationTaskExecutor;
}
}
@@ -0,0 +1,14 @@
package com.bsd.pjcloud.common.core.config;
import com.bsd.pjcloud.common.core.factory.YamlPropertySourceFactory;
import org.springframework.context.annotation.PropertySource;
/**
* 加载不可变配置项,这样的配置文件一舦是放在jar包中的,不会随着环境的变化而变化。
*
* @author pjcloud
* @date 2024/7/20
*/
@PropertySource(value = "classpath:common-config.yml", factory = YamlPropertySourceFactory.class)
public class ImmutableConfiguration {
}
@@ -0,0 +1,86 @@
package com.bsd.pjcloud.common.core.config;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.bsd.pjcloud.common.core.jackson.PjcloudJavaTimeModule;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.TimeZone;
/**
* JacksonConfig 配置时间转换规则
* {@link com.bsd.pjcloud.common.core.jackson.PjcloudJavaTimeModule}、默认时区等
*
* @author L.cm
* @author pjcloud
* @author lishangbu
* @date 2020-06-15
*/
@Configuration
@ConditionalOnClass(ObjectMapper.class)
@AutoConfigureBefore(JacksonAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class JacksonConfiguration implements WebMvcConfigurer {
private static final String ASIA_SHANGHAI = "Asia/Shanghai";
@Bean
@ConditionalOnMissingBean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
builder.locale(Locale.CHINA);
builder.timeZone(TimeZone.getTimeZone(ASIA_SHANGHAI));
builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN);
builder.serializerByType(Long.class, ToStringSerializer.instance);
builder.modules(new PjcloudJavaTimeModule());
};
}
/**
* 增加GET请求参数中时间类型转换 {@link com.bsd.pjcloud.common.core.jackson.PjcloudJavaTimeModule}
* <ul>
* <li>HH:mm:ss -> LocalTime</li>
* <li>yyyy-MM-dd -> LocalDate</li>
* <li>yyyy-MM-dd HH:mm:ss -> LocalDateTime</li>
* </ul>
* @param registry
*/
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN));
registrar.setDateFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN));
registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN));
registrar.registerFormatters(registry);
}
/**
* 避免form 提交 context-type 不规范中文乱码
* @return Filter
*/
@Bean
public OrderedCharacterEncodingFilter characterEncodingFilter() {
OrderedCharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(StandardCharsets.UTF_8.name());
filter.setForceEncoding(true);
filter.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filter;
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.core.config;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
/**
* @author pjcloud
* @date 2018/11/14
* <p>
* 国际化配置
*/
@Configuration
public class MessageSourceConfiguration {
@Bean
public MessageSource pjcloudMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:i18n/messages");
return messageSource;
}
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author pjcloud
* @date 2018/8/16 RestTemplate
*/
@Configuration
public class RestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@@ -0,0 +1,123 @@
package com.bsd.pjcloud.common.core.constant;
/**
* @author pjcloud
* @date 2019-04-28
* <p>
* 缓存的key 常量
*/
public interface CacheConstants {
/**
* 全局缓存,在缓存名称上加上该前缀表示该缓存不区分租户,比如:
* <p/>
* {@code @Cacheable(value = CacheConstants.GLOBALLY+CacheConstants.MENU_DETAILS, key = "#roleId + '_menu'", unless = "#result == null")}
*/
String GLOBALLY = "gl:";
/**
* 验证码前缀
*/
String DEFAULT_CODE_KEY = "DEFAULT_CODE_KEY:";
/**
* 菜单信息缓存
*/
String MENU_DETAILS = "menu_details";
/**
* 用户信息缓存
*/
String USER_DETAILS = "user_details";
/**
* 移动端用户信息缓存
*/
String USER_DETAILS_MINI = "user_details_mini";
/**
* 角色信息缓存
*/
String ROLE_DETAILS = "role_details";
/**
* 字典信息缓存
*/
String DICT_DETAILS = "dict_details";
/**
* oauth 客户端信息
*/
String CLIENT_DETAILS_KEY = "pjcloud_oauth:client:details";
/**
* spring boot admin 事件key
*/
String EVENT_KEY = GLOBALLY + "event_key";
/**
* 路由存放
*/
String ROUTE_KEY = GLOBALLY + "gateway_route_key";
/**
* 内存reload 时间
*/
String ROUTE_JVM_RELOAD_TOPIC = "gateway_jvm_route_reload_topic";
/**
* redis 重新加载 路由信息
*/
String ROUTE_REDIS_RELOAD_TOPIC = "upms_redis_route_reload_topic";
/**
* redis 重新加载客户端信息
*/
String CLIENT_REDIS_RELOAD_TOPIC = "upms_redis_client_reload_topic";
/**
* 敏感词重新加载
*/
String SENSITIVE_REDIS_RELOAD_TOPIC = "sensitive_client_reload_topic";
/**
* 公众号 reload
*/
String MP_REDIS_RELOAD_TOPIC = "mp_redis_reload_topic";
/**
* 支付 reload 事件
*/
String PAY_REDIS_RELOAD_TOPIC = "pay_redis_reload_topic";
/**
* 参数缓存
*/
String PARAMS_DETAILS = "params_details";
/**
* 租户缓存 (不区分租户)
*/
String TENANT_DETAILS = GLOBALLY + "tenant_details";
/**
* i18n缓存 (不区分租户)
*/
String I18N_DETAILS = GLOBALLY + "i18n_details";
/**
* 客户端配置缓存
*/
String CLIENT_FLAG = "client_config_flag";
/**
* 登录错误次数
*/
String LOGIN_ERROR_TIMES = "login_error_times";
/**
* oauth 缓存前缀
*/
String PROJECT_OAUTH_ACCESS = "token::access_token";
}
@@ -0,0 +1,138 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.constant;
/**
* @author pjcloud
* @date 2017/10/29
*/
public interface CommonConstants {
/**
* header 中租户ID
*/
String TENANT_ID = "TENANT-ID";
/**
* header 中版本信息
*/
String VERSION = "VERSION";
/**
* 租户ID
*/
Long TENANT_ID_1 = 1L;
/**
* 删除
*/
String STATUS_DEL = "1";
/**
* 正常
*/
String STATUS_NORMAL = "0";
/**
* 锁定
*/
String STATUS_LOCK = "9";
/**
* 菜单树根节点
*/
Long MENU_TREE_ROOT_ID = -1L;
/**
* 编码
*/
String UTF8 = "UTF-8";
/**
* 前端工程名
*/
String FRONT_END_PROJECT = "pjcloud-ui";
/**
* 移动端工程名
*/
String UNI_END_PROJECT = "pjcloud-app";
/**
* 后端工程名
*/
String BACK_END_PROJECT = "pjcloud";
/**
* 公共参数
*/
String PIG_PUBLIC_PARAM_KEY = "PIG_PUBLIC_PARAM_KEY";
/**
* 成功标记
*/
Integer SUCCESS = 0;
/**
* 失败标记
*/
Integer FAIL = 1;
/**
* 默认存储bucket
*/
String BUCKET_NAME = "pjcloud";
/**
* 滑块验证码
*/
String IMAGE_CODE_TYPE = "blockPuzzle";
/**
* 验证码开关
*/
String CAPTCHA_FLAG = "captcha_flag";
/**
* 密码传输是否加密
*/
String ENC_FLAG = "enc_flag";
/**
* 客户端允许同时在线数量
*/
String ONLINE_QUANTITY = "online_quantity";
/**
* 请求开始时间
*/
String REQUEST_START_TIME = "REQUEST-START-TIME";
/**
* 当前页
*/
String CURRENT = "current";
/**
* size
*/
String SIZE = "size";
}
@@ -0,0 +1,21 @@
package com.bsd.pjcloud.common.core.constant;
/**
* 分页相关的参数
*
* @author lishangbu
* @date 2018/11/22
*/
public interface PaginationConstants {
/**
* 当前页
*/
String CURRENT = "current";
/**
* 每页大小
*/
String SIZE = "size";
}
@@ -0,0 +1,257 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.constant;
/**
* @author pjcloud
* @date 2017-12-18
*/
public interface SecurityConstants {
/**
* 启动时是否检查Inner注解安全性
*/
boolean INNER_CHECK = true;
/**
* 刷新
*/
String REFRESH_TOKEN = "refresh_token";
/**
* 验证码有效期
*/
int CODE_TIME = 60;
/**
* 验证码长度
*/
String CODE_SIZE = "4";
/**
* 角色前缀
*/
String ROLE = "ROLE_";
/**
* 前缀
*/
String PIGX_PREFIX = "pjcloud_";
/**
* token 相关前缀
*/
String TOKEN_PREFIX = "token:";
/**
* oauth 相关前缀
*/
String OAUTH_PREFIX = "oauth:";
/**
* 授权码模式code key 前缀
*/
String OAUTH_CODE_PREFIX = "oauth:code:";
/**
* 项目的license
*/
String PIGX_LICENSE = "https://pig4cloud.com";
/**
* 内部
*/
String FROM_IN = "Y";
/**
* 标志
*/
String FROM = "from";
/**
* 请求header
*/
String HEADER_FROM_IN = FROM + "=" + FROM_IN;
/**
* OAUTH URL
*/
String OAUTH_TOKEN_URL = "/oauth2/token";
/**
* 移动端授权
*/
String GRANT_MOBILE = "mobile";
/**
* TOC 客户端
*/
String HEADER_TOC = "CLIENT-TOC";
/**
* TOC 客户端
*/
String HEADER_TOC_YES = "Y";
/**
* QQ获取token
*/
String QQ_AUTHORIZATION_CODE_URL = "https://graph.qq.com/oauth2.0/token?grant_type="
+ "authorization_code&code=%S&client_id=%s&redirect_uri=" + "%s&client_secret=%s";
/**
* 微信获取OPENID
*/
String WX_AUTHORIZATION_CODE_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"
+ "?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
/**
* 微信小程序OPENID
*/
String MINI_APP_AUTHORIZATION_CODE_URL = "https://api.weixin.qq.com/sns/jscode2session"
+ "?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
/**
* 码云获取token
*/
String GITEE_AUTHORIZATION_CODE_URL = "https://gitee.com/oauth/token?grant_type="
+ "authorization_code&code=%S&client_id=%s&redirect_uri=" + "%s&client_secret=%s";
/**
* 开源中国获取token
*/
String OSC_AUTHORIZATION_CODE_URL = "https://www.oschina.net/action/openapi/token";
/**
* QQ获取用户信息
*/
String QQ_USER_INFO_URL = "https://graph.qq.com/oauth2.0/me?access_token=%s";
/**
* 码云获取用户信息
*/
String GITEE_USER_INFO_URL = "https://gitee.com/api/v5/user?access_token=%s";
/**
* 开源中国用户信息
*/
String OSC_USER_INFO_URL = "https://www.oschina.net/action/openapi/user?access_token=%s&dataType=json";
/**
* 钉钉获取 token
*/
String DING_OLD_GET_TOKEN = "https://oapi.dingtalk.com/gettoken";
/**
* 钉钉同步部门列表
*/
String DING_OLD_DEPT_URL = "https://oapi.dingtalk.com/topapi/v2/department/listsub";
/**
* 钉钉部门用户id列表
*/
String DING_DEPT_USERIDS_URL = "https://oapi.dingtalk.com/topapi/user/listid";
/**
* 钉钉用户详情
*/
String DING_USER_INFO_URL = "https://oapi.dingtalk.com/topapi/v2/user/get";
/**
* {bcrypt} 加密的特征码
*/
String BCRYPT = "{bcrypt}";
/**
* 客户端模式
*/
String CLIENT_CREDENTIALS = "client_credentials";
/**
* 客户端编号
*/
String CLIENT_ID = "client_id";
/**
* 客户端唯一令牌
*/
String CLIENT_RECREATE = "recreate_flag";
/**
* 用户ID字段
*/
String DETAILS_USER_ID = "user_id";
/**
* 用户名
*/
String DETAILS_USERNAME = "username";
/**
* 姓名
*/
String NAME = "name";
/**
* 协议字段
*/
String DETAILS_LICENSE = "license";
/**
* 激活字段 兼容外围系统接入
*/
String ACTIVE = "active";
/**
* AES 加密
*/
String AES = "aes";
/**
* 授权码模式confirm
*/
String CUSTOM_CONSENT_PAGE_URI = "/token/confirm_access";
/**
* {noop} 加密的特征码
*/
String NOOP = "{noop}";
/**
* 短信登录 参数名称
*/
String SMS_PARAMETER_NAME = "mobile";
/**
* 手机号登录
*/
String APP = "mobile";
/**
* 用户信息
*/
String DETAILS_USER = "user_info";
/**
* 不脱敏的权限
*/
String NO_MASK = "no_mask";
}
@@ -0,0 +1,58 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.constant;
/**
* @author pjcloud
* @date 2018年06月22日16:41:01 服务名称
*/
public interface ServiceNameConstants {
/**
* 认证中心
*/
String AUTH_SERVICE = "pjcloud-auth";
/**
* UMPS模块
*/
String UPMS_SERVICE = "pjcloud-upms-biz";
/**
* app服务
*/
String APP_SERVER = "pjcloud-app-server-biz";
/**
* 流程引擎
*/
String FLOW_ENGINE_SERVER = "pjcloud-flow-engine-biz";
/**
* 流程工单
*/
String FLOW_TASK_SERVER = "pjcloud-flow-task-biz";
/**
* 代码生成模块
*/
String CODEGEN_SERVICE = "pjcloud-codegen";
}
@@ -0,0 +1,36 @@
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2020-11-18
* <p>
* 验证码状态
*/
@Getter
@AllArgsConstructor
public enum CaptchaFlagTypeEnum {
/**
* 开启验证码
*/
ON("1", "开启验证码"),
/**
* 关闭验证码
*/
OFF("0", "关闭验证码");
/**
* 类型
*/
private String type;
/**
* 描述
*/
private String description;
}
@@ -0,0 +1,36 @@
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2019-05-16
* <p>
* 字典类型
*/
@Getter
@AllArgsConstructor
public enum DictTypeEnum {
/**
* 字典类型-系统内置(不可修改)
*/
SYSTEM("1", "系统内置"),
/**
* 字典类型-业务类型
*/
BIZ("0", "业务类");
/**
* 类型
*/
private String type;
/**
* 描述
*/
private String description;
}
@@ -0,0 +1,36 @@
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2020-11-18
* <p>
* 密码是否加密传输
*/
@Getter
@AllArgsConstructor
public enum EncFlagTypeEnum {
/**
* 是
*/
YES("1", "是"),
/**
* 否
*/
NO("0", "否");
/**
* 类型
*/
private String type;
/**
* 描述
*/
private String description;
}
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2018/8/15 社交登录类型
*/
@Getter
@AllArgsConstructor
public enum LoginTypeEnum {
/**
* 账号密码登录
*/
PWD("PWD", "账号密码登录"),
/**
* 验证码登录
*/
SMS("SMS", "验证码登录"),
APPSMS("APP-SMS", "APP验证码登录"),
/**
* QQ登录
*/
QQ("QQ", "QQ登录"),
/**
* 微信登录
*/
WECHAT("WX", "微信登录"),
/**
* 微信小程序
*/
MINI_APP("MINI", "微信小程序"),
/**
* 码云登录
*/
GITEE("GITEE", "码云登录"),
/**
* 开源中国登录
*/
OSC("OSC", "开源中国登录"),
/**
* 钉钉
*/
DINGTALK("DINGTALK", "钉钉"),
/**
* 企业微信
*/
WEIXIN_CP("WEIXIN_CP", "企业微信"),
/**
* CAS 登录
*/
CAS("CAS", "CAS 登录");
/**
* 类型
*/
private String type;
/**
* 描述
*/
private String description;
}
@@ -0,0 +1,41 @@
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2020-02-17
* <p>
* 菜单类型
*/
@Getter
@AllArgsConstructor
public enum MenuTypeEnum {
/**
* 左侧菜单
*/
LEFT_MENU("0", "left"),
/**
* 顶部菜单
*/
TOP_MENU("2", "top"),
/**
* 按钮
*/
BUTTON("1", "button");
/**
* 类型
*/
private String type;
/**
* 描述
*/
private String description;
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2018/9/30 流程状态
*/
@Getter
@AllArgsConstructor
public enum ProcessStatusEnum {
/**
* 激活
*/
ACTIVE("active"),
/**
* 暂停
*/
SUSPEND("suspend");
/**
* 状态
*/
private final String status;
}
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2018/9/30 资源类型
*/
@Getter
@AllArgsConstructor
public enum ResourceTypeEnum {
/**
* 图片资源
*/
IMAGE("image", "图片资源"),
/**
* xml资源
*/
XML("xml", "xml资源");
/**
* 类型
*/
private final String type;
/**
* 描述
*/
private final String description;
}
@@ -0,0 +1,60 @@
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* @author pjcloud
* @date 2020-01-19
* <p>
* 前端类型类型
*/
@Getter
@AllArgsConstructor
public enum StyleTypeEnum {
/**
* 前端类型-avue 风格
*/
AVUE("0", "avue"),
/**
* 前端类型-element 风格
*/
ELEMENT("1", "element"),
/**
* 前端风格-uview 风格
*/
UVIEW("2", "uview"),
/**
* magic
*/
MAGIC("3", "magic"),
/**
* element-plus
*/
PLUS("4", "plus");
/**
* 类型
*/
private String style;
/**
* 描述
*/
private String description;
public static String getDesc(String style) {
return Arrays.stream(StyleTypeEnum.values())
.filter(styleTypeEnum -> styleTypeEnum.getStyle().equals(style))
.findFirst()
.get()
.getDescription();
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2018/9/30 流程状态
*/
@Getter
@AllArgsConstructor
public enum TaskStatusEnum {
/**
* 未提交
*/
UNSUBMIT("0", "未提交"),
/**
* 审核中
*/
CHECK("1", "审核中"),
/**
* 已完成
*/
COMPLETED("2", "已完成"),
/**
* 驳回
*/
OVERRULE("9", "驳回");
/**
* 类型
*/
private final String status;
/**
* 描述
*/
private final String description;
}
@@ -0,0 +1,22 @@
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum UserTypeEnum {
TOB("0", "面向后台应用"), TOC("1", "面向小程序");
/**
* 类型
*/
private final String status;
/**
* 描述
*/
private final String description;
}
@@ -0,0 +1,28 @@
package com.bsd.pjcloud.common.core.constant.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* yes and no enum
*
* @author pjcloud
* @date 2023/10/25
*/
@Getter
@AllArgsConstructor
public enum YesNoEnum {
YES("1", "是"), NO("0", "否");
/**
* 编码
*/
private String code;
/**
* 描述
*/
private String desc;
}
@@ -0,0 +1,49 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.exception;
import lombok.NoArgsConstructor;
/**
* @author pjcloud
* @date 😴2018年06月22日16:21:57
*/
@NoArgsConstructor
public class CheckedException extends RuntimeException {
private static final long serialVersionUID = 1L;
public CheckedException(String message) {
super(message);
}
public CheckedException(Throwable cause) {
super(cause);
}
public CheckedException(String message, Throwable cause) {
super(message, cause);
}
public CheckedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
@@ -0,0 +1,113 @@
package com.bsd.pjcloud.common.core.exception;
/**
* 错误编码
*
* @author pjcloud
* @date 2022/3/30
*/
public interface ErrorCodes {
/**
* 系统编码错误
*/
String SYS_PARAM_CONFIG_ERROR = "sys.param.config.error";
/**
* 系统内置参数不能删除
*/
String SYS_PARAM_DELETE_SYSTEM = "sys.param.delete.system";
/**
* 用户已存在
*/
String SYS_USER_USERNAME_EXISTING = "sys.user.username.existing";
/**
* 用户已存在
*/
String SYS_USER_PHONE_EXISTING = "sys.user.phone.existing";
/**
* 用户原密码错误,修改失败
*/
String SYS_USER_UPDATE_PASSWORDERROR = "sys.user.update.passwordError";
/**
* 用户信息为空
*/
String SYS_USER_USERINFO_EMPTY = "sys.user.userInfo.empty";
/**
* 获取当前用户信息失败
*/
String SYS_USER_QUERY_ERROR = "sys.user.query.error";
String SYS_USER_IMPORT_SUCCEED = "sys.user.import.succeed";
/**
* 部门名称不存在
*/
String SYS_DEPT_DEPTNAME_INEXISTENCE = "sys.dept.deptName.inexistence";
/**
* 岗位名称不存在
*/
String SYS_POST_POSTNAME_INEXISTENCE = "sys.post.postName.inexistence";
/**
* 岗位名称或编码已经存在
*/
String SYS_POST_NAMEORCODE_EXISTING = "sys.post.nameOrCode.existing";
/**
* 角色名称不存在
*/
String SYS_ROLE_ROLENAME_INEXISTENCE = "sys.role.roleName.inexistence";
/**
* 角色名或角色编码已经存在
*/
String SYS_ROLE_NAMEORCODE_EXISTING = "sys.role.nameOrCode.existing";
/**
* 菜单存在下级节点 删除失败
*/
String SYS_MENU_DELETE_EXISTING = "sys.menu.delete.existing";
/**
* 系统内置字典不允许删除
*/
String SYS_DICT_DELETE_SYSTEM = "sys.dict.delete.system";
/**
* 系统内置字典不能修改
*/
String SYS_DICT_UPDATE_SYSTEM = "sys.dict.update.system";
/**
* 验证码发送频繁
*/
String SYS_APP_SMS_OFTEN = "sys.app.sms.often";
/**
* 手机号未注册
*/
String SYS_APP_PHONE_UNREGISTERED = "sys.app.phone.unregistered";
/**
* 企微调用接口错误
*/
String SYS_CONNECT_CP_DEPT_SYNC_ERROR = "sys.connect.cp.dept.sync.error";
/**
* 企微调用接口错误
*/
String SYS_CONNECT_CP_USER_SYNC_ERROR = "sys.connect.cp.user.sync.error";
/**
* 用户信息为空
*/
String APP_USER_USERINFO_EMPTY = "app.user.userInfo.empty";
}
@@ -0,0 +1,37 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.exception;
/**
* @author pjcloud
* @date 2018年06月22日16:22:15
*/
public class ValidateCodeException extends RuntimeException {
private static final long serialVersionUID = -7285211528095468156L;
public ValidateCodeException() {
}
public ValidateCodeException(String msg) {
super(msg);
}
}
@@ -0,0 +1,44 @@
package com.bsd.pjcloud.common.core.factory;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import org.springframework.lang.Nullable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
/**
* @author pjcloud
* @date 2022/3/29
* <p>
* 读取自定义 yaml 文件工厂类
*/
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
Properties propertiesFromYaml = loadYamlIntoProperties(resource);
String sourceName = name != null ? name : resource.getResource().getFilename();
return new PropertiesPropertySource(sourceName, propertiesFromYaml);
}
private Properties loadYamlIntoProperties(EncodedResource resource) throws FileNotFoundException {
try {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
factory.afterPropertiesSet();
return factory.getObject();
}
catch (IllegalStateException e) {
Throwable cause = e.getCause();
if (cause instanceof FileNotFoundException)
throw (FileNotFoundException) e.getCause();
throw e;
}
}
}
@@ -0,0 +1,61 @@
package com.bsd.pjcloud.common.core.jackson;
import cn.hutool.core.date.DatePattern;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.PackageVersion;
import com.fasterxml.jackson.datatype.jsr310.deser.*;
import com.fasterxml.jackson.datatype.jsr310.ser.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
/**
* java 8 时间默认序列化
*
* @author L.cm
* @author lishanbu
*/
public class PjcloudJavaTimeModule extends SimpleModule {
/**
* 指定序列化规则
*/
public PjcloudJavaTimeModule() {
super(PackageVersion.VERSION);
// ======================= 时间序列化规则 ===============================
// yyyy-MM-dd HH:mm:ss
this.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
// yyyy-MM-dd
this.addSerializer(LocalDate.class,
new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)));
// HH:mm:ss
this.addSerializer(LocalTime.class,
new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)));
// Instant 类型序列化
this.addSerializer(Instant.class, InstantSerializer.INSTANCE);
// Duration 类型序列化
this.addSerializer(Duration.class, DurationSerializer.INSTANCE);
// ======================= 时间反序列化规则 ==============================
// yyyy-MM-dd HH:mm:ss
this.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
// yyyy-MM-dd
this.addDeserializer(LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)));
// HH:mm:ss
this.addDeserializer(LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)));
// Instant 反序列化
this.addDeserializer(Instant.class, InstantDeserializer.INSTANT);
// Duration 反序列化
this.addDeserializer(Duration.class, DurationDeserializer.INSTANCE);
}
}
@@ -0,0 +1,113 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.util;
import lombok.experimental.UtilityClass;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.web.method.HandlerMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
/**
* 类工具类
*
* @author L.cm
*/
@UtilityClass
public class ClassUtils extends org.springframework.util.ClassUtils {
private final ParameterNameDiscoverer PARAMETERNAMEDISCOVERER = new DefaultParameterNameDiscoverer();
/**
* 获取方法参数信息
* @param constructor 构造器
* @param parameterIndex 参数序号
* @return {MethodParameter}
*/
public MethodParameter getMethodParameter(Constructor<?> constructor, int parameterIndex) {
MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex);
methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER);
return methodParameter;
}
/**
* 获取方法参数信息
* @param method 方法
* @param parameterIndex 参数序号
* @return {MethodParameter}
*/
public MethodParameter getMethodParameter(Method method, int parameterIndex) {
MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex);
methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER);
return methodParameter;
}
/**
* 获取Annotation
* @param method Method
* @param annotationType 注解类
* @param <A> 泛型标记
* @return {Annotation}
*/
public <A extends Annotation> A getAnnotation(Method method, Class<A> annotationType) {
Class<?> targetClass = method.getDeclaringClass();
// The method may be on an interface, but we need attributes from the target
// class.
// If the target class is null, the method will be unchanged.
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
// If we are dealing with method with generic parameters, find the original
// method.
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
// 先找方法,再找方法上的类
A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType);
if (null != annotation) {
return annotation;
}
// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType);
}
/**
* 获取Annotation
* @param handlerMethod HandlerMethod
* @param annotationType 注解类
* @param <A> 泛型标记
* @return {Annotation}
*/
public <A extends Annotation> A getAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) {
// 先找方法,再找方法上的类
A annotation = handlerMethod.getMethodAnnotation(annotationType);
if (null != annotation) {
return annotation;
}
// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
Class<?> beanType = handlerMethod.getBeanType();
return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType);
}
}
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.common.core.util;
/**
* @author pjcloud
* @date 2020/9/29
* <p>
* 字符串处理,方便其他模块解耦处理
*/
public interface KeyStrResolver {
/**
* 字符串加工
* @param in 输入字符串
* @param split 分割符
* @return 输出字符串
*/
String extract(String in, String split);
/**
* 字符串获取
* @return 模块返回字符串
*/
String key();
}
@@ -0,0 +1,47 @@
package com.bsd.pjcloud.common.core.util;
import lombok.experimental.UtilityClass;
import org.springframework.context.MessageSource;
import java.util.Locale;
/**
* i18n 工具类
*
* @author pjcloud
* @date 2022/3/30
*/
@UtilityClass
public class MsgUtils {
/**
* 通过code 获取中文错误信息
* @param code
* @return
*/
public String getMessage(String code) {
MessageSource messageSource = SpringContextHolder.getBean("pjcloudMessageSource");
return messageSource.getMessage(code, null, Locale.CHINA);
}
/**
* 通过code 和参数获取中文错误信息
* @param code
* @return
*/
public String getMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("pjcloudMessageSource");
return messageSource.getMessage(code, objects, Locale.CHINA);
}
/**
* security 通过code 和参数获取中文错误信息
* @param code
* @return
*/
public String getSecurityMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("securityMessageSource");
return messageSource.getMessage(code, objects, Locale.CHINA);
}
}
@@ -0,0 +1,105 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.util;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 响应信息主体
*
* @param <T>
* @author pjcloud
*/
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Schema(description = "响应信息主体")
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Getter
@Setter
@JsonPropertyDescription(value = "调用成功返回0,调用失败返回1")
@Schema(description = "返回标记:成功标记=0,失败标记=1")
private int code;
@Getter
@Setter
@JsonPropertyDescription(value = "返回具体的信息")
@Schema(description = "返回信息")
private String msg;
@Getter
@Setter
@Schema(description = "数据")
private T data;
public static <T> R<T> ok() {
return restResult(null, CommonConstants.SUCCESS, null);
}
public static <T> R<T> ok(T data) {
return restResult(data, CommonConstants.SUCCESS, null);
}
public static <T> R<T> ok(T data, String msg) {
return restResult(data, CommonConstants.SUCCESS, msg);
}
public static <T> R<T> failed() {
return restResult(null, CommonConstants.FAIL, null);
}
public static <T> R<T> failed(String msg) {
return restResult(null, CommonConstants.FAIL, msg);
}
public static <T> R<T> failed(T data) {
return restResult(data, CommonConstants.FAIL, null);
}
public static <T> R<T> failed(T data, String msg) {
return restResult(data, CommonConstants.FAIL, msg);
}
static <T> R<T> restResult(T data, int code, String msg) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public boolean isOk() {
return this.code == CommonConstants.SUCCESS;
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bsd.pjcloud.common.core.util;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StreamUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
/**
* Request包装类:允许 body 重复读取
*
* @author Hccake
*/
@Slf4j
public class RepeatBodyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] bodyByteArray;
private final Map<String, String[]> parameterMap;
public RepeatBodyRequestWrapper(HttpServletRequest request) {
super(request);
this.bodyByteArray = getByteBody(request);
// 使用 HashMap 以便后续可以修改
this.parameterMap = new HashMap<>(request.getParameterMap());
}
@Override
public BufferedReader getReader() {
return ObjectUtils.isEmpty(this.bodyByteArray) ? null
: new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bodyByteArray);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true; // 可以读取
}
@Override
public void setReadListener(ReadListener readListener) {
// doNothing
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
private static byte[] getByteBody(HttpServletRequest request) {
byte[] body = new byte[0];
try {
body = StreamUtils.copyToByteArray(request.getInputStream());
} catch (IOException e) {
log.error("解析流中数据异常", e);
}
return body;
}
@Override
public Map<String, String[]> getParameterMap() {
return this.parameterMap; // 返回可变的 parameterMap
}
/**
* 设置新的参数映射
* @param parameterMap 新的参数映射
*/
public void setParameterMap(Map<String, String[]> parameterMap) {
this.parameterMap.clear();
this.parameterMap.putAll(parameterMap);
}
@Override
public String getParameter(String name) {
String[] values = parameterMap.get(name);
return (values != null && values.length > 0) ? values[0] : null;
}
@Override
public String[] getParameterValues(String name) {
return parameterMap.get(name);
}
}
@@ -0,0 +1,289 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.util;
import cn.hutool.core.util.ObjectUtil;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 简化{@code R<T>} 的访问操作,例子 <pre>
* R<Integer> result = R.ok(0);
* // 使用场景1: 链式操作: 断言然后消费
* RetOps.of(result)
* .assertCode(-1,r -> new RuntimeException("error "+r.getCode()))
* .assertDataNotEmpty(r -> new IllegalStateException("oops!"))
* .useData(System.out::println);
*
* // 使用场景2: 读取原始值(data),这里返回的是Optional
* RetOps.of(result).getData().orElse(null);
*
* // 使用场景3: 类型转换
* R<String> s = RetOps.of(result)
* .assertDataNotNull(r -> new IllegalStateException("nani??"))
* .map(i -> Integer.toHexString(i))
* .peek();
* </pre>
*
* @author CJ (power4j@outlook.com)
* @date 2022/5/12
* @since 4.4
*/
public class RetOps<T> {
/** 状态码为成功 */
public static final Predicate<R<?>> CODE_SUCCESS = r -> CommonConstants.SUCCESS == r.getCode();
/** 数据有值 */
public static final Predicate<R<?>> HAS_DATA = r -> ObjectUtil.isNotEmpty(r.getData());
/** 数据有值,并且包含元素 */
public static final Predicate<R<?>> HAS_ELEMENT = r -> ObjectUtil.isNotEmpty(r.getData());
/** 状态码为成功并且有值 */
public static final Predicate<R<?>> DATA_AVAILABLE = CODE_SUCCESS.and(HAS_DATA);
private final R<T> original;
// ~ 初始化
// ===================================================================================================
RetOps(R<T> original) {
this.original = original;
}
public static <T> RetOps<T> of(R<T> original) {
return new RetOps<>(Objects.requireNonNull(original));
}
// ~ 杂项方法
// ===================================================================================================
/**
* 观察原始值
* @return R
*/
public R<T> peek() {
return original;
}
/**
* 读取{@code code}的值
* @return 返回code的值
*/
public int getCode() {
return original.getCode();
}
/**
* 读取{@code data}的值
* @return 返回 Optional 包装的data
*/
public Optional<T> getData() {
return Optional.ofNullable(original.getData());
}
/**
* 有条件地读取{@code data}的值
* @param predicate 断言函数
* @return 返回 Optional 包装的data,如果断言失败返回empty
*/
public Optional<T> getDataIf(Predicate<? super R<?>> predicate) {
return predicate.test(original) ? getData() : Optional.empty();
}
/**
* 读取{@code msg}的值
* @return 返回Optional包装的 msg
*/
public Optional<String> getMsg() {
return Optional.ofNullable(original.getMsg());
}
/**
* 对{@code code}的值进行相等性测试
* @param value 基准值
* @return 返回ture表示相等
*/
public boolean codeEquals(int value) {
return original.getCode() == value;
}
/**
* 对{@code code}的值进行相等性测试
* @param value 基准值
* @return 返回ture表示不相等
*/
public boolean codeNotEquals(int value) {
return !codeEquals(value);
}
/**
* 是否成功
* @return 返回ture表示成功
* @see CommonConstants#SUCCESS
*/
public boolean isSuccess() {
return codeEquals(CommonConstants.SUCCESS);
}
/**
* 是否失败
* @return 返回ture表示失败
*/
public boolean notSuccess() {
return !isSuccess();
}
// ~ 链式操作
// ===================================================================================================
/**
* 断言{@code code}的值
* @param expect 预期的值
* @param func 用户函数,负责创建异常对象
* @param <Ex> 异常类型
* @return 返回实例,以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public <Ex extends Exception> RetOps<T> assertCode(int expect, Function<? super R<T>, ? extends Ex> func)
throws Ex {
if (codeNotEquals(expect)) {
throw func.apply(original);
}
return this;
}
/**
* 断言成功
* @param func 用户函数,负责创建异常对象
* @param <Ex> 异常类型
* @return 返回实例,以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public <Ex extends Exception> RetOps<T> assertSuccess(Function<? super R<T>, ? extends Ex> func) throws Ex {
return assertCode(CommonConstants.SUCCESS, func);
}
/**
* 断言业务数据有值
* @param func 用户函数,负责创建异常对象
* @param <Ex> 异常类型
* @return 返回实例,以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public <Ex extends Exception> RetOps<T> assertDataNotNull(Function<? super R<T>, ? extends Ex> func) throws Ex {
if (Objects.isNull(original.getData())) {
throw func.apply(original);
}
return this;
}
/**
* 断言业务数据有值,并且包含元素
* @param func 用户函数,负责创建异常对象
* @param <Ex> 异常类型
* @return 返回实例,以便于继续进行链式操作
* @throws Ex 断言失败时抛出
*/
public <Ex extends Exception> RetOps<T> assertDataNotEmpty(Function<? super R<T>, ? extends Ex> func) throws Ex {
if (ObjectUtil.isEmpty(original.getData())) {
throw func.apply(original);
}
return this;
}
/**
* 对业务数据(data)转换
* @param mapper 业务数据转换函数
* @param <U> 数据类型
* @return 返回新实例,以便于继续进行链式操作
*/
public <U> RetOps<U> map(Function<? super T, ? extends U> mapper) {
R<U> result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg());
return of(result);
}
/**
* 对业务数据(data)转换
* @param predicate 断言函数
* @param mapper 业务数据转换函数
* @param <U> 数据类型
* @return 返回新实例,以便于继续进行链式操作
* @see RetOps#CODE_SUCCESS
* @see RetOps#HAS_DATA
* @see RetOps#HAS_ELEMENT
* @see RetOps#DATA_AVAILABLE
*/
public <U> RetOps<U> mapIf(Predicate<? super R<T>> predicate, Function<? super T, ? extends U> mapper) {
R<U> result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg());
return of(result);
}
// ~ 数据消费
// ===================================================================================================
/**
* 消费数据,注意此方法保证数据可用
* @param consumer 消费函数
*/
public void useData(Consumer<? super T> consumer) {
consumer.accept(original.getData());
}
/**
* 条件消费(错误代码匹配某个值)
* @param consumer 消费函数
* @param codes 错误代码集合,匹配任意一个则调用消费函数
*/
public void useDataOnCode(Consumer<? super T> consumer, int... codes) {
useDataIf(o -> Arrays.stream(codes).filter(c -> original.getCode() == c).findFirst().isPresent(), consumer);
}
/**
* 条件消费(错误代码表示成功)
* @param consumer 消费函数
*/
public void useDataIfSuccess(Consumer<? super T> consumer) {
useDataIf(CODE_SUCCESS, consumer);
}
/**
* 条件消费
* @param predicate 断言函数
* @param consumer 消费函数,断言函数返回{@code true}时被调用
* @see RetOps#CODE_SUCCESS
* @see RetOps#HAS_DATA
* @see RetOps#HAS_ELEMENT
* @see RetOps#DATA_AVAILABLE
*/
public void useDataIf(Predicate<? super R<T>> predicate, Consumer<? super T> consumer) {
if (predicate.test(original)) {
consumer.accept(original.getData());
}
}
}
@@ -0,0 +1,127 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* @author pjcloud
* @date 2018/6/27 Spring 工具类
*/
@Slf4j
@Service
@Lazy(false)
public class SpringContextHolder implements BeanFactoryPostProcessor, ApplicationContextAware, DisposableBean {
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext = null;
/**
* 取得存储在静态变量中的ApplicationContext.
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* BeanFactoryPostProcessor, 注入Context到静态变量中.
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
SpringContextHolder.beanFactory = factory;
}
/**
* 实现ApplicationContextAware接口, 注入Context到静态变量中.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextHolder.applicationContext = applicationContext;
}
public static ListableBeanFactory getBeanFactory() {
return null == beanFactory ? applicationContext : beanFactory;
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
return (T) getBeanFactory().getBean(name);
}
/**
* 从静态变量applicationContext中取得Bean, Map<Bean名称,实现类></>
*/
public static <T> Map<String, T> getBeansOfType(Class<T> type) {
return getBeanFactory().getBeansOfType(type);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> requiredType) {
return getBeanFactory().getBean(requiredType);
}
/**
* 清除SpringContextHolder中的ApplicationContext为Null.
*/
public static void clearHolder() {
if (log.isDebugEnabled()) {
log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
}
applicationContext = null;
}
/**
* 发布事件
* @param event
*/
public static void publishEvent(ApplicationEvent event) {
if (applicationContext == null) {
return;
}
applicationContext.publishEvent(event);
}
/**
* 实现DisposableBean接口, 在Context关闭时清理静态变量.
*/
@Override
public void destroy() {
SpringContextHolder.clearHolder();
}
}
@@ -0,0 +1,15 @@
package com.bsd.pjcloud.common.core.util;
import java.lang.annotation.*;
/**
* @author pjcloud
* @since v5.3
* <p>
* table 实体增加此组件自动声明为租户表
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
public @interface TenantTable {
}
@@ -0,0 +1,31 @@
package com.bsd.pjcloud.common.core.util;
/**
* 校验类型
*
* @author pjcloud
* @date 2022/4/26
*/
public class ValidGroup {
/**
* 插入组
*
* @author pjcloud
* @date 2022/4/26
*/
public static interface Insert {
}
/**
* 编辑组
*
* @author pjcloud
* @date 2022/4/26
*/
public static interface Update {
}
}
@@ -0,0 +1,276 @@
/*
*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*
*/
package com.bsd.pjcloud.common.core.util;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
/**
* Miscellaneous utilities for web applications.
*
* @author L.cm
*/
@Slf4j
@UtilityClass
public class WebUtils extends org.springframework.web.util.WebUtils {
private final String BASIC_ = "Basic ";
private final String UNKNOWN = "unknown";
/**
* 判断是否ajax请求 spring ajax 返回含有 ResponseBody 或者 RestController注解
*
* @param handlerMethod HandlerMethod
* @return 是否ajax请求
*/
public boolean isBody(HandlerMethod handlerMethod) {
ResponseBody responseBody = ClassUtils.getAnnotation(handlerMethod, ResponseBody.class);
return responseBody != null;
}
/**
* 读取cookie
*
* @param name cookie name
* @return cookie value
*/
public String getCookieVal(String name) {
HttpServletRequest request = WebUtils.getRequest();
Assert.notNull(request, "request from RequestContextHolder is null");
return getCookieVal(request, name);
}
/**
* 读取cookie
*
* @param request HttpServletRequest
* @param name cookie name
* @return cookie value
*/
public String getCookieVal(HttpServletRequest request, String name) {
Cookie cookie = getCookie(request, name);
return cookie != null ? cookie.getValue() : null;
}
/**
* 清除 某个指定的cookie
*
* @param response HttpServletResponse
* @param key cookie key
*/
public void removeCookie(HttpServletResponse response, String key) {
setCookie(response, key, null, 0);
}
/**
* 设置cookie
*
* @param response HttpServletResponse
* @param name cookie name
* @param value cookie value
* @param maxAgeInSeconds maxage
*/
public void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAgeInSeconds);
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
/**
* 获取 HttpServletRequest
*
* @return {HttpServletRequest}
*/
public HttpServletRequest getRequest() {
try {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
return ((ServletRequestAttributes) requestAttributes).getRequest();
} catch (Exception e) {
return null;
}
}
/**
* 获取 HttpServletResponse
*
* @return {HttpServletResponse}
*/
public HttpServletResponse getResponse() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
}
/**
* 返回json
*
* @param response HttpServletResponse
* @param result 结果对象
*/
public void renderJson(HttpServletResponse response, Object result) {
renderJson(response, result, MediaType.APPLICATION_JSON_VALUE);
}
/**
* 返回json
*
* @param response HttpServletResponse
* @param result 结果对象
* @param contentType contentType
*/
public void renderJson(HttpServletResponse response, Object result, String contentType) {
response.setCharacterEncoding("UTF-8");
response.setContentType(contentType);
try (PrintWriter out = response.getWriter()) {
out.append(JSONUtil.toJsonStr(result));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
/**
* 获取ip
*
* @return {String}
*/
public String getIP() {
return getIP(WebUtils.getRequest());
}
/**
* 获取ip
*
* @param request HttpServletRequest
* @return {String}
*/
public String getIP(HttpServletRequest request) {
Assert.notNull(request, "HttpServletRequest is null");
String ip = request.getHeader("X-Requested-For");
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StrUtil.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return StrUtil.isBlank(ip) ? null : ip.split(",")[0];
}
/**
* 解析 client id
*
* @param header
* @param defVal
* @return 如果解析失败返回默认值
*/
public String extractClientId(String header, final String defVal) {
if (header == null || !header.startsWith(BASIC_)) {
log.debug("请求头中client信息为空: {}", header);
return defVal;
}
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
} catch (IllegalArgumentException e) {
log.debug("Failed to decode basic authentication token: {}", header);
return defVal;
}
String token = new String(decoded, StandardCharsets.UTF_8);
int delim = token.indexOf(":");
if (delim == -1) {
log.debug("Invalid basic authentication token: {}", header);
return defVal;
}
return token.substring(0, delim);
}
/**
* 从请求头中解析 client id
*
* @param header
* @return
*/
public Optional<String> extractClientId(String header) {
return Optional.ofNullable(extractClientId(header, null));
}
/**
* 获取 bearer 令牌
*
* @return {@link String }
*/
@SneakyThrows
public String getToken() {
if (Objects.isNull(WebUtils.getRequest())) {
return null;
}
// 获取请求
HttpServletRequest request = WebUtils.getRequest();
// 获取Authorization头信息
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader != null && StrUtil.startWithAnyIgnoreCase(authorizationHeader, "Bearer ")) {
// 返回去掉"Bearer "前缀的Token
return authorizationHeader.substring(7);
} else {
// 处理Authorization头信息为空或格式不正确的情况
return null;
}
}
}
@@ -0,0 +1,7 @@
com.bsd.pjcloud.common.core.config.AsyncConfiguration
com.bsd.pjcloud.common.core.config.JacksonConfiguration
com.bsd.pjcloud.common.core.config.MessageSourceConfiguration
com.bsd.pjcloud.common.core.config.RestTemplateConfiguration
com.bsd.pjcloud.common.core.config.ImmutableConfiguration
com.bsd.pjcloud.common.core.util.SpringContextHolder
@@ -0,0 +1,16 @@
${AnsiColor.BRIGHT_YELLOW}
::::::::: ::::::::::: :::::::: ::: :::
:+: :+: :+: :+: :+: :+: :+:
+:+ +:+ +:+ +:+ +:+ +:+
+#++:++#+ +#+ :#: +#++:+
+#+ +#+ +#+ +#+# +#+ +#+
#+# #+# #+# #+# #+# #+#
### ########### ######## ### ###
www.pig4cloud.com
Pig Microservice Architecture
${AnsiColor.DEFAULT}
@@ -0,0 +1,37 @@
# 很多第三方组件的配置默认值一般均不需要改动,写在nacos 中比较混乱冗余,就可以写在此处
# sms4j配置
sms:
is-print: false
config-type: interface
sms-oa:
config-type: yaml
# 行为验证码配置
aj:
captcha:
cache-type: redis
# 配置文件加密方法
jasypt:
encryptor:
algorithm: PBEWithMD5AndDES
iv-generator-classname: org.jasypt.iv.NoIvGenerator
# liteflow配置
liteflow:
rule-source: rules/*-rule.xml
print-banner: false
print-execution-log: false
enable: true
# 关闭连表插件的banner
mybatis-plus-join:
banner: false
# sa-token 配置
sa-token:
is-print: false # 不输出 banner
token-name: pig # token 存储的前缀
is-concurrent: true # 是否允许同时在线
is-share: false # 是否允许复用token (同一账号同时登录的情况)
@@ -0,0 +1,21 @@
sys.user.update.passwordError=\u539F\u5BC6\u7801\u9519\u8BEF\uFF0C\u4FEE\u6539\u5931\u8D25
sys.user.query.error=\u83B7\u53D6\u5F53\u524D\u7528\u6237\u4FE1\u606F\u5931\u8D25
sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u4E3A\u624B\u673A\u53F7
sys.user.username.existing={0} \u7528\u6237\u540D\u5DF2\u5B58\u5728
sys.user.phone.existing={0} \u624b\u673a\u53f7\u5df2\u5b58\u5728
sys.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A
sys.dept.deptName.inexistence={0} \u90E8\u95E8\u540D\u79F0\u4E0D\u5B58\u5728
sys.post.postName.inexistence={0} \u5C97\u4F4D\u540D\u79F0\u4E0D\u5B58\u5728
sys.post.nameOrCode.existing={0} {1} \u5C97\u4F4D\u540D\u6216\u5C97\u4F4D\u7F16\u7801\u5DF2\u7ECF\u5B58\u5728
sys.role.roleName.inexistence={0} \u89D2\u8272\u540D\u79F0\u4E0D\u5B58\u5728
sys.role.nameOrCode.existing={0} {1} \u89D2\u8272\u540D\u6216\u89D2\u8272\u7F16\u7801\u5DF2\u7ECF\u5B58\u5728
sys.param.delete.system=\u7CFB\u7EDF\u5185\u7F6E\u53C2\u6570\u4E0D\u80FD\u5220\u9664
sys.param.config.error={0} \u7CFB\u7EDF\u53C2\u6570\u914D\u7F6E\u9519\u8BEF
sys.menu.delete.existing=\u83DC\u5355\u542B\u6709\u4E0B\u7EA7\u4E0D\u80FD\u5220\u9664
sys.app.sms.often=\u9A8C\u8BC1\u7801\u53D1\u9001\u8FC7\u9891\u7E41
sys.app.phone.unregistered={0} \u624B\u673A\u53F7\u672A\u6CE8\u518C
sys.dict.delete.system=\u7CFB\u7EDF\u5185\u7F6E\u5B57\u5178\u9879\u76EE\u4E0D\u80FD\u5220\u9664
sys.dict.update.system=\u7CFB\u7EDF\u5185\u7F6E\u5B57\u5178\u9879\u76EE\u4E0D\u80FD\u4FEE\u6539
sys.connect.cp.dept.sync.error=\u83B7\u53D6\u4F01\u4E1A\u5FAE\u4FE1\u90E8\u95E8\u5217\u8868\u5931\u8D25
sys.connect.cp.user.sync.error=\u83B7\u53D6\u4F01\u4E1A\u5FAE\u4FE1\u7528\u6237\u5217\u8868\u5931\u8D25
app.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2025, pjcloud All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
~
~ Redistributions of source code must retain the above copyright notice,
~ this list of conditions and the following disclaimer.
~ Redistributions in binary form must reproduce the above copyright
~ notice, this list of conditions and the following disclaimer in the
~ documentation and/or other materials provided with the distribution.
~ Neither the name of the pig4cloud.com developer nor the names of its
~ contributors may be used to endorse or promote products derived from
~ this software without specific prior written permission.
~ Author: pjcloud
~
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common</artifactId>
<version>5.7.0</version>
</parent>
<artifactId>pjcloud-common-data</artifactId>
<packaging>jar</packaging>
<description>pjcloud 数据操作相关</description>
<dependencies>
<!--工具类核心包-->
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-core</artifactId>
</dependency>
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring</artifactId>
</dependency>
<!-- mybatis-plus 3.5.9 需要单独引入此依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<!-- 连表查询依赖 -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
</dependency>
<!-- druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<optional>true</optional>
</dependency>
<!--安全依赖获取上下文信息-->
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-security</artifactId>
<optional>true</optional>
</dependency>
<!--feign client-->
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-upms-api</artifactId>
</dependency>
<!--缓存依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.cache;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.lang.Nullable;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
* redis cache 扩展cache name自动化配置
*
* @author L.cm
* @author pjcloud
* <p>
* cachename = xx#ttl
*/
@Slf4j
public class RedisAutoCacheManager extends RedisCacheManager {
private static final String SPLIT_FLAG = "#";
private static final int CACHE_LENGTH = 2;
public RedisAutoCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
boolean allowInFlightCacheCreation,
Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheConfigurations);
}
@Override
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
if (StrUtil.isBlank(name) || !name.contains(SPLIT_FLAG)) {
return super.createRedisCache(name, cacheConfig);
}
String[] cacheArray = name.split(SPLIT_FLAG);
if (cacheArray.length < CACHE_LENGTH) {
return super.createRedisCache(name, cacheConfig);
}
if (cacheConfig != null) {
Duration duration = DurationStyle.detectAndParse(cacheArray[1], ChronoUnit.SECONDS);
cacheConfig = cacheConfig.entryTtl(duration);
}
return super.createRedisCache(cacheArray[0], cacheConfig);
}
/**
* 从上下文中获取租户ID,重写@Cacheable value 值
* @param name
* @return
*/
@Override
public Cache getCache(String name) {
// see https://gitee.wang/pig/pjcloud/issues/613
if (name.startsWith(CacheConstants.GLOBALLY)) {
return super.getCache(name);
}
return super.getCache(TenantContextHolder.getTenantId() + StrUtil.COLON + name);
}
}
@@ -0,0 +1,126 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.cache;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.bsd.pjcloud.common.core.jackson.PjcloudJavaTimeModule;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 扩展redis-cache支持注解cacheName添加超时时间
*
* @author L.cm
*/
@Configuration
@AutoConfigureAfter({RedisAutoConfiguration.class})
@ConditionalOnBean({RedisConnectionFactory.class})
@EnableConfigurationProperties(CacheProperties.class)
public class RedisCacheAutoConfiguration {
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
@Nullable
private final RedisCacheConfiguration redisCacheConfiguration;
RedisCacheAutoConfiguration(CacheProperties cacheProperties, CacheManagerCustomizers customizerInvoker,
ObjectProvider<RedisCacheConfiguration> redisCacheConfiguration) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
this.redisCacheConfiguration = redisCacheConfiguration.getIfAvailable();
}
@Primary
@Bean("cacheResolver")
public CacheManager redisCacheManager(ObjectProvider<RedisConnectionFactory> connectionFactoryObjectProvider) {
RedisConnectionFactory connectionFactory = connectionFactoryObjectProvider.getIfAvailable();
Objects.requireNonNull(connectionFactory, "Bean RedisConnectionFactory is null.");
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration cacheConfiguration = this.determineConfiguration();
List<String> cacheNames = this.cacheProperties.getCacheNames();
Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>();
if (!cacheNames.isEmpty()) {
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>(cacheNames.size());
cacheNames.forEach(it -> cacheConfigMap.put(it, cacheConfiguration));
initialCaches.putAll(cacheConfigMap);
}
boolean allowInFlightCacheCreation = true;
boolean enableTransactions = false;
RedisAutoCacheManager cacheManager = new RedisAutoCacheManager(
redisCacheWriter, cacheConfiguration, allowInFlightCacheCreation, initialCaches
);
cacheManager.setTransactionAware(enableTransactions);
return this.customizerInvoker.customize(cacheManager);
}
private RedisCacheConfiguration determineConfiguration() {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
RedisSerializer<Object> jsonRedisSerializer = RedisSerializer.json();
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(jsonRedisSerializer, "mapper");
objectMapper.registerModules(new PjcloudJavaTimeModule());
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jsonRedisSerializer));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.cache;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* CacheManagerCustomizers配置
*
* @author L.cm
*/
@Configuration
@ConditionalOnMissingBean(CacheManagerCustomizers.class)
public class RedisCacheManagerConfiguration {
@Bean
public CacheManagerCustomizers cacheManagerCustomizers(
ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
return new CacheManagerCustomizers(customizers.getIfAvailable());
}
}
@@ -0,0 +1,26 @@
package com.bsd.pjcloud.common.data.cache;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* @author pjcloud
* @date 2022/2/4
*
* redis message 信道相关配置
*/
@Configuration(proxyBeanMethods = false)
public class RedisMessageConfiguration {
@Bean
@ConditionalOnMissingBean
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
return container;
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.cache;
import cn.hutool.core.util.ReflectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.bsd.pjcloud.common.core.jackson.PjcloudJavaTimeModule;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* RedisTemplate 配置
*
* @author L.cm
*/
@EnableCaching
@Configuration
@AutoConfigureBefore(name = { "org.redisson.spring.starter.RedissonAutoConfigurationV2",
"org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration" })
public class RedisTemplateConfiguration {
@Bean
@Primary
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置 redisTemplate 的序列化器为 json
RedisSerializer<Object> jsonRedisSerializer = RedisSerializer.json();
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(jsonRedisSerializer, "mapper");
objectMapper.registerModules(new PjcloudJavaTimeModule());
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* @author pjcloud
* @date 2018/8/30 数据权限查询参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DataScope extends HashMap {
/**
* 限制范围的字段名称
*/
private String scopeDeptName = "dept_id";
/**
* 本人权限范围字段
*/
private String scopeUserName = "create_by";
/**
* 具体的数据范围
*/
private List<Long> deptList = new ArrayList<>();
/**
* 具体查询的用户数据权限范围
*/
private String username;
/**
* 是否只查询本部门
*/
private Boolean isOnly = false;
/**
* 函数名称,默认 SELECT * ;
*
* <ul>
* <li>COUNT(1)</li>
* </ul>
*/
private DataScopeFuncEnum func = DataScopeFuncEnum.ALL;
/**
* of 获取实例
*/
public static DataScope of() {
return new DataScope();
}
public DataScope deptIds(List<Long> deptIds) {
this.deptList = deptIds;
return this;
}
public DataScope only(boolean isOnly) {
this.isOnly = isOnly;
return this;
}
public DataScope func(DataScopeFuncEnum func) {
this.func = func;
return this;
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据权限函数类型
*
* @author pjcloud
* @date 2020-06-17
*/
@Getter
@AllArgsConstructor
public enum DataScopeFuncEnum {
/**
* 查询全部数据 SELECT * FROM (originSql) temp_data_scope WHERE temp_data_scope.dept_id IN
* (1)
*/
ALL("*", "全部"),
/**
* 查询函数COUNT SELECT COUNT(1) FROM (originSql) temp_data_scope WHERE
* temp_data_scope.dept_id IN (1)
*/
COUNT("COUNT(1)", "自定义");
/**
* 类型
*/
private final String type;
/**
* 描述
*/
private final String description;
}
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
/**
* @author pjcloud
* @date 2019-09-07
* <p>
* data scope 判断处理器,抽象服务扩展
*/
public interface DataScopeHandle {
/**
* 计算用户数据权限
* @param dataScope 数据权限设置
* @return 返回true表示无需进行数据过滤处理,返回false表示需要进行数据过滤
*/
Boolean calcScope(DataScope dataScope);
}
@@ -0,0 +1,98 @@
package com.bsd.pjcloud.common.data.datascope;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import lombok.Setter;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.List;
import java.util.Map;
/**
* @author pjcloud
* @date 2020/11/29
*/
public class DataScopeInnerInterceptor implements DataScopeInterceptor {
@Setter
private DataScopeHandle dataScopeHandle;
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
String originalSql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();
// 查找参数中包含DataScope类型的参数
DataScope dataScope = findDataScopeObject(parameterObject);
if (dataScope == null) {
return;
}
// 返回true 不拦截直接返回原始 SQL (只针对 * 查询)
if (DataScopeFuncEnum.ALL.equals(dataScope.getFunc()) && dataScopeHandle.calcScope(dataScope)) {
return;
}
// 返回true 不拦截直接返回原始 SQL (只针对 COUNT 查询)
if (DataScopeFuncEnum.COUNT.equals(dataScope.getFunc()) && dataScopeHandle.calcScope(dataScope)) {
mpBs.sql(String.format("SELECT %s FROM (%s) temp_data_scope", dataScope.getFunc().getType(), originalSql));
return;
}
List<Long> deptIds = dataScope.getDeptList();
// 1.无数据权限限制,则直接返回 0 条数据
if (CollUtil.isEmpty(deptIds) && StrUtil.isBlank(dataScope.getUsername())) {
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE 1 = 2",
dataScope.getFunc().getType(), originalSql);
}
// 2.如果为本人权限 + 部门权限控制
else if (StrUtil.isNotBlank(dataScope.getUsername()) && CollUtil.isNotEmpty(deptIds)) {
String join = CollectionUtil.join(deptIds, ",");
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s' OR temp_data_scope.%s IN (%s)",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername(), dataScope.getScopeDeptName(), join);
}
// 3. 如果为本人
else if (StrUtil.isNotBlank(dataScope.getUsername())) {
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s'",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername());
}
// 4.部门权限控制
else {
String join = CollectionUtil.join(deptIds, ",");
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s IN (%s)",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeDeptName(), join);
}
mpBs.sql(originalSql);
}
/**
* 查找参数是否包括DataScope对象
*
* @param parameterObj 参数列表
* @return DataScope
*/
private DataScope findDataScopeObject(Object parameterObj) {
if (parameterObj instanceof DataScope) {
return (DataScope) parameterObj;
} else if (parameterObj instanceof Map) {
for (Object val : ((Map<?, ?>) parameterObj).values()) {
if (val instanceof DataScope) {
return (DataScope) val;
}
}
}
return null;
}
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.common.data.datascope;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
/**
* 数据权限抽象
*
* @author pjcloud
* @date 2022/8/9
*/
public interface DataScopeInterceptor extends InnerInterceptor {
}
@@ -0,0 +1,27 @@
package com.bsd.pjcloud.common.data.datascope;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.github.yulichang.injector.MPJSqlInjector;
import java.util.List;
/**
* 支持自定义数据权限方法注入
*
* @author pjcloud
* @date 2020-06-17
*/
public class DataScopeSqlInjector extends MPJSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
methodList.add(new SelectListByScope());
methodList.add(new SelectPageByScope());
methodList.add(new SelectCountByScope());
return methodList;
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2018/12/26
* <p>
* 数据权限类型
*/
@Getter
@AllArgsConstructor
public enum DataScopeTypeEnum {
/**
* 查询全部数据
*/
ALL(0, "全部"),
/**
* 自定义
*/
CUSTOM(1, "自定义"),
/**
* 本级及子级
*/
OWN_CHILD_LEVEL(2, "本级及子级"),
/**
* 本级
*/
OWN_LEVEL(3, "本级"),
/**
* 本人
*/
SELF_LEVEL(4, "本人");
/**
* 类型
*/
private final int type;
/**
* 描述
*/
private final String description;
/**
* 按类型获取
*
* @param dsType DS 型
* @return {@link DataScopeTypeEnum }
*/
public static DataScopeTypeEnum getByType(Integer dsType) {
for (DataScopeTypeEnum value : DataScopeTypeEnum.values()) {
if (value.getType() == dsType) {
return value;
}
}
return null;
}
}
@@ -0,0 +1,48 @@
package com.bsd.pjcloud.common.data.datascope;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 扩展通用 Mapper,支持数据权限 和批量插入
*
* @author pjcloud
* @date 2020-06-17
*/
public interface PjcloudBaseMapper<T> extends MPJBaseMapper<T> {
/**
* 根据 entity 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
* @param scope 数据权限范围
* @return List<T>
*/
List<T> selectListByScope(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper, DataScope scope);
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为 null)
* @param scope 数据权限范围
* @return Page
*/
<E extends IPage<T>> E selectPageByScope(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper,
DataScope scope);
/**
* 根据 Wrapper 条件,查询总记录数
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
* @param scope 数据权限范围
* @return Integer
*/
Long selectCountByScope(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper, DataScope scope);
}
@@ -0,0 +1,157 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.datascope;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.entity.SysDept;
import com.bsd.pjcloud.admin.api.entity.SysRole;
import com.bsd.pjcloud.admin.api.feign.RemoteDataScopeService;
import com.bsd.pjcloud.common.core.constant.enums.UserTypeEnum;
import com.bsd.pjcloud.common.core.util.RetOps;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author pjcloud
* @date 2019-09-07
* <p>
* 默认data scope 判断处理器
*/
/**
* @author pjcloud
* @date 2019-09-07
* <p>
* 默认data scope 判断处理器
*/
@RequiredArgsConstructor
public class PjcloudDefaultDataScopeHandle implements DataScopeHandle {
private final RemoteDataScopeService dataScopeService;
@Override
public Boolean calcScope(DataScope dataScope) {
// 对于TOC客户端,不进行数据权限校验
PjcloudUser user = SecurityUtils.getUser();
if (UserTypeEnum.TOC.getStatus().equals(user.getUserType())) {
return true;
}
// 业务代码里的规则,覆盖计算规则
if (StrUtil.isNotBlank(dataScope.getUsername()) || CollUtil.isNotEmpty(dataScope.getDeptList())) {
return false;
}
// 获取用户角色ID列表
List<Long> roleIdList = SecurityUtils.getRoleIds();
if (CollUtil.isEmpty(roleIdList)) {
return false;
}
// 获取角色列表
List<SysRole> roleList = RetOps.of(dataScopeService.getRoleList(roleIdList))
.getData()
.orElseGet(Collections::emptyList);
if (CollUtil.isEmpty(roleList)) {
return false;
}
// 处理数据权限
return processDataScope(user, dataScope, roleList);
}
/**
* 处理数据权限
*/
private boolean processDataScope(PjcloudUser user, DataScope dataScope, List<SysRole> roleList) {
List<Long> deptList = dataScope.getDeptList();
for (SysRole role : roleList) {
Integer dsType = role.getDsType();
// 处理不同数据权限类型
switch (Objects.requireNonNull(DataScopeTypeEnum.getByType(dsType))) {
case ALL:
return true;
case CUSTOM:
handleCustomScope(role, deptList);
break;
case OWN_CHILD_LEVEL:
handleOwnChildLevelScope(user, deptList);
break;
case OWN_LEVEL:
handleOwnLevelScope(user, deptList);
break;
case SELF_LEVEL:
handleSelfLevelScope(user, dataScope);
break;
default:
break;
}
}
return false;
}
/**
* 处理自定义数据权限
*/
private void handleCustomScope(SysRole role, List<Long> deptList) {
if (StrUtil.isNotBlank(role.getDsScope())) {
deptList.addAll(Arrays.stream(role.getDsScope().split(StrUtil.COMMA))
.map(Long::parseLong)
.collect(Collectors.toList()));
}
}
/**
* 处理本级及下级数据权限
*/
private void handleOwnChildLevelScope(PjcloudUser user, List<Long> deptList) {
List<Long> descendantDeptIds = RetOps.of(dataScopeService.getDescendantList(user.getDeptId()))
.getData()
.orElseGet(Collections::emptyList)
.stream()
.map(SysDept::getDeptId)
.collect(Collectors.toList());
deptList.addAll(descendantDeptIds);
}
/**
* 处理本级数据权限
*/
private void handleOwnLevelScope(PjcloudUser user, List<Long> deptList) {
deptList.add(user.getDeptId());
}
/**
* 处理个人数据权限
*/
private void handleSelfLevelScope(PjcloudUser user, DataScope dataScope) {
dataScope.setUsername(user.getUsername());
}
}
@@ -0,0 +1,33 @@
package com.bsd.pjcloud.common.data.datascope;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* 扩展支持COUNT查询数量
*
* @author pjcloud
* @date 2020/6/17
*/
public class SelectCountByScope extends AbstractMethod {
public SelectCountByScope() {
super("selectCountByScope");
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
String sql = String.format(sqlMethod.getSql(), this.sqlFirst(), this.sqlSelectColumns(tableInfo, true),
tableInfo.getTableName(), this.sqlWhereEntityWrapper(true, tableInfo), this.sqlOrderBy(tableInfo),
this.sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForOther(mapperClass, sqlSource, Long.class);
}
}
@@ -0,0 +1,29 @@
package com.bsd.pjcloud.common.data.datascope;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* @author pjcloud
* @date 2020/4/26
*/
public class SelectListByScope extends AbstractMethod {
public SelectListByScope() {
super("selectListByScope");
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
String sql = String.format(sqlMethod.getSql(), this.sqlFirst(), this.sqlSelectColumns(tableInfo, true),
tableInfo.getTableName(), this.sqlWhereEntityWrapper(true, tableInfo), this.sqlOrderBy(tableInfo),
this.sqlComment());
SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, sqlSource, tableInfo);
}
}
@@ -0,0 +1,29 @@
package com.bsd.pjcloud.common.data.datascope;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
/**
* @author pjcloud
* @date 2020/4/26
*/
public class SelectPageByScope extends AbstractMethod {
public SelectPageByScope() {
super("selectPageByScope");
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_PAGE;
String sql = String.format(sqlMethod.getSql(), this.sqlFirst(), this.sqlSelectColumns(tableInfo, true),
tableInfo.getTableName(), this.sqlWhereEntityWrapper(true, tableInfo), this.sqlOrderBy(tableInfo),
this.sqlComment());
SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, sqlSource, tableInfo);
}
}
@@ -0,0 +1,59 @@
package com.bsd.pjcloud.common.data.handler;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* Mybatis数组,符串互转
* <p>
* MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
*
* @author xuzihui
* @date 2019-11-20
*/
@MappedTypes(value = { Long[].class })
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class JsonLongArrayTypeHandler extends BaseTypeHandler<Long[]> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Long[] parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, ArrayUtil.join(parameter, StrUtil.COMMA));
}
@Override
@SneakyThrows
public Long[] getNullableResult(ResultSet rs, String columnName) {
String reString = rs.getString(columnName);
if (StrUtil.isBlank(reString)) return new Long[]{};
return Convert.toLongArray(reString);
}
@Override
@SneakyThrows
public Long[] getNullableResult(ResultSet rs, int columnIndex) {
String reString = rs.getString(columnIndex);
if (StrUtil.isBlank(reString)) return new Long[]{};
return Convert.toLongArray(reString);
}
@Override
@SneakyThrows
public Long[] getNullableResult(CallableStatement cs, int columnIndex) {
String reString = cs.getString(columnIndex);
if (StrUtil.isBlank(reString)) return new Long[]{};
return Convert.toLongArray(reString);
}
}
@@ -0,0 +1,59 @@
package com.bsd.pjcloud.common.data.handler;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.SneakyThrows;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* Mybatis数组,符串互转
* <p>
* MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
*
* @author xuzihui
* @date 2019-11-20
*/
@MappedTypes(value = { String[].class })
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class JsonStringArrayTypeHandler extends BaseTypeHandler<String[]> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String[] parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, ArrayUtil.join(parameter, StrUtil.COMMA));
}
@Override
@SneakyThrows
public String[] getNullableResult(ResultSet rs, String columnName) {
String reString = rs.getString(columnName);
if (StrUtil.isBlank(reString)) return new String[]{};
return Convert.toStrArray(reString);
}
@Override
@SneakyThrows
public String[] getNullableResult(ResultSet rs, int columnIndex) {
String reString = rs.getString(columnIndex);
if (StrUtil.isBlank(reString)) return new String[]{};
return Convert.toStrArray(reString);
}
@Override
@SneakyThrows
public String[] getNullableResult(CallableStatement cs, int columnIndex) {
String reString = cs.getString(columnIndex);
if (StrUtil.isBlank(reString)) return new String[]{};
return Convert.toStrArray(reString);
}
}
@@ -0,0 +1,211 @@
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bsd.pjcloud.common.data.mybatis;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.druid.DbType;
import com.alibaba.druid.filter.FilterChain;
import com.alibaba.druid.filter.FilterEventAdapter;
import com.alibaba.druid.proxy.jdbc.JdbcParameter;
import com.alibaba.druid.proxy.jdbc.ResultSetProxy;
import com.alibaba.druid.proxy.jdbc.StatementProxy;
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.dialect.sqlserver.visitor.SQLServerSchemaStatVisitor;
import com.alibaba.druid.sql.visitor.SchemaStatVisitor;
import com.alibaba.druid.stat.TableStat;
import com.alibaba.druid.util.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.sql.SQLException;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 打印可执行的 sql 日志
*
* <p>
* 参考:https://jfinal.com/share/2204
* </p>
*
* @author L.cm
*/
@Slf4j
@RequiredArgsConstructor
public class DruidSqlLogFilter extends FilterEventAdapter {
private static final SQLUtils.FormatOption FORMAT_OPTION = new SQLUtils.FormatOption(false, false);
private final PjcloudMybatisProperties properties;
@Override
protected void statementExecuteBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}
@Override
protected void statementExecuteBatchBefore(StatementProxy statement) {
statement.setLastExecuteStartNano();
}
@Override
protected void statementExecuteUpdateBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}
@Override
protected void statementExecuteQueryBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}
@Override
protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) {
statement.setLastExecuteTimeNano();
}
@Override
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
statement.setLastExecuteTimeNano();
}
@Override
protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) {
statement.setLastExecuteTimeNano();
}
@Override
protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) {
statement.setLastExecuteTimeNano();
}
@Override
public void statement_close(FilterChain chain, StatementProxy statement) throws SQLException {
// 先调用父类关闭 statement
super.statement_close(chain, statement);
// 支持动态开启
if (!properties.isShowSql()) {
return;
}
// 是否开启调试
if (!log.isInfoEnabled()) {
return;
}
// 打印可执行的 sql
String sql = statement.getBatchSql();
// sql 为空直接返回
if (StringUtils.isEmpty(sql)) {
return;
}
String dbType = statement.getConnectionProxy().getDirectDataSource().getDbType();
// 判断表名是配置了匹配过滤
if (CollUtil.isNotEmpty(properties.getSkipTable())) {
List<String> skipTableList = properties.getSkipTable();
List<String> tableNameList = getTablesBydruid(sql, dbType);
if (tableNameList.stream()
.anyMatch(tableName -> StrUtil.containsAnyIgnoreCase(tableName,
ArrayUtil.toArray(skipTableList, String.class)))) {
return;
}
}
int parametersSize = statement.getParametersSize();
List<Object> parameters = new ArrayList<>(parametersSize);
for (int i = 0; i < parametersSize; ++i) {
// 转换参数,处理 java8 时间
parameters.add(getJdbcParameter(statement.getParameter(i)));
}
String formattedSql = SQLUtils.format(sql, DbType.of(dbType), parameters, FORMAT_OPTION);
printSql(formattedSql, statement);
}
private static Object getJdbcParameter(JdbcParameter jdbcParam) {
if (jdbcParam == null) {
return null;
}
Object value = jdbcParam.getValue();
// 处理 java8 时间
if (value instanceof TemporalAccessor) {
return value.toString();
}
return value;
}
private static void printSql(String sql, StatementProxy statement) {
// 打印 sql
String sqlLogger = "\n\n======= Sql Logger ======================" + "\n{}"
+ "\n======= Sql Execute Time: {} =======\n";
log.info(sqlLogger, sql.trim(), format(statement.getLastExecuteTimeNano()));
}
/**
* 格式化执行时间,单位为 ms 和 s,保留三位小数
* @param nanos 纳秒
* @return 格式化后的时间
*/
private static String format(long nanos) {
if (nanos < 1) {
return "0ms";
}
double millis = (double) nanos / (1000 * 1000);
// 不够 1 ms,最小单位为 ms
if (millis > 1000) {
return String.format("%.3fs", millis / 1000);
}
else {
return String.format("%.3fms", millis);
}
}
/**
* 从SQL中提取表名(sql中出现的所有表)
* @param sql sql语句
* @param dbType dbType
* @return List<String>
*/
public static List<String> getTablesBydruid(String sql, String dbType) {
List<String> result = new ArrayList<String>();
List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType);
for (SQLStatement stmt : stmtList) {
// 也可以用更精确的解析器,如MySqlSchemaStatVisitor
SchemaStatVisitor visitor;
// SQL server 数据库特殊处理
if (DbType.sqlserver.name().equalsIgnoreCase(dbType)) {
visitor = new SQLServerSchemaStatVisitor();
}
else {
visitor = new SchemaStatVisitor();
}
stmt.accept(visitor);
Map<TableStat.Name, TableStat> tables = visitor.getTables();
for (TableStat.Name name : tables.keySet()) {
result.add(name.getName());
}
}
return result;
}
}
@@ -0,0 +1,168 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.bsd.pjcloud.admin.api.feign.RemoteDataScopeService;
import com.bsd.pjcloud.common.data.datascope.DataScopeInnerInterceptor;
import com.bsd.pjcloud.common.data.datascope.DataScopeInterceptor;
import com.bsd.pjcloud.common.data.datascope.DataScopeSqlInjector;
import com.bsd.pjcloud.common.data.datascope.PjcloudDefaultDataScopeHandle;
import com.bsd.pjcloud.common.data.resolver.SqlFilterArgumentResolver;
import com.bsd.pjcloud.common.data.tenant.PjcloudTenantConfigProperties;
import com.bsd.pjcloud.common.data.tenant.PjcloudTenantHandler;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.sql.DataSource;
import java.util.List;
import java.util.Properties;
/**
* @author pjcloud
* @date 2020-02-08
*/
@Configuration
@ConditionalOnBean(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(PjcloudMybatisProperties.class)
public class MybatisPlusConfiguration implements WebMvcConfigurer {
/**
* 增加请求参数解析器,对请求中的参数注入SQL 检查
*
* @param resolverList
*/
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolverList) {
resolverList.add(new SqlFilterArgumentResolver());
}
/**
* mybatis plus 拦截器配置
*
* @return PjcloudDefaultDatascopeHandle
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(TenantLineInnerInterceptor tenantLineInnerInterceptor,
DataScopeInterceptor dataScopeInterceptor) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 注入多租户支持
interceptor.addInnerInterceptor(tenantLineInnerInterceptor);
// 数据权限
interceptor.addInnerInterceptor(dataScopeInterceptor);
// 分页支持
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setMaxLimit(1000L);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
/**
* 创建租户维护处理器对象
*
* @return 处理后的租户维护处理器
*/
@Bean
@ConditionalOnMissingBean
public TenantLineInnerInterceptor tenantLineInnerInterceptor(PjcloudTenantConfigProperties tenantConfigProperties) {
TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor();
tenantLineInnerInterceptor.setTenantLineHandler(new PjcloudTenantHandler(tenantConfigProperties));
return tenantLineInnerInterceptor;
}
/**
* 数据权限拦截器
*
* @return DataScopeInterceptor
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(PjcloudUser.class)
public DataScopeInterceptor dataScopeInterceptor(RemoteDataScopeService dataScopeService) {
DataScopeInnerInterceptor dataScopeInnerInterceptor = new DataScopeInnerInterceptor();
dataScopeInnerInterceptor.setDataScopeHandle(new PjcloudDefaultDataScopeHandle(dataScopeService));
return dataScopeInnerInterceptor;
}
/**
* 扩展 mybatis-plus baseMapper 支持数据权限
*
* @return
*/
@Bean
@Primary
@ConditionalOnBean(DataScopeInterceptor.class)
public DataScopeSqlInjector dataScopeSqlInjector() {
return new DataScopeSqlInjector();
}
/**
* SQL 日志格式化
*
* @return DruidSqlLogFilter
*/
@Bean
public DruidSqlLogFilter sqlLogFilter(PjcloudMybatisProperties properties) {
return new DruidSqlLogFilter(properties);
}
/**
* 审计字段自动填充
*
* @return {@link MetaObjectHandler}
*/
@Bean
public MybatisPlusMetaObjectHandler mybatisPlusMetaObjectHandler() {
return new MybatisPlusMetaObjectHandler();
}
/**
* 数据库方言配置
*
* @return
*/
@Bean
public DatabaseIdProvider databaseIdProvider() {
VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties properties = new Properties();
properties.setProperty("SQL Server", "mssql");
databaseIdProvider.setProperties(properties);
return databaseIdProvider;
}
}
@@ -0,0 +1,95 @@
package com.bsd.pjcloud.common.data.mybatis;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.ClassUtils;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.Optional;
/**
* MybatisPlus 自动填充配置
*
* @author L.cm
*/
@Slf4j
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.debug("mybatis plus start insert fill ....");
LocalDateTime now = LocalDateTime.now();
// 审计字段自动填充,覆盖用户输入
fillValIfNullByName("createTime", now, metaObject, true);
fillValIfNullByName("updateTime", now, metaObject, true);
fillValIfNullByName("createBy", getUserName(), metaObject, true);
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
// 删除标记自动填充
fillValIfNullByName("delFlag", CommonConstants.STATUS_NORMAL, metaObject, true);
}
@Override
public void updateFill(MetaObject metaObject) {
log.debug("mybatis plus start update fill ....");
fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true);
fillValIfNullByName("updateBy", getUserName(), metaObject, true);
}
/**
* 填充值,先判断是否有手动设置,优先手动设置的值,例如:job必须手动设置
* @param fieldName 属性名
* @param fieldVal 属性值
* @param metaObject MetaObject
* @param isCover 是否覆盖原有值,避免更新操作手动入参
*/
private static void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) {
// 0. 如果填充值为空
if (fieldVal == null) {
return;
}
// 1. 没有 get 方法
if (!metaObject.hasSetter(fieldName)) {
return;
}
// 2. 如果用户有手动设置的值
Object userSetValue = metaObject.getValue(fieldName);
String setValueStr = StrUtil.str(userSetValue, Charset.defaultCharset());
if (StrUtil.isNotBlank(setValueStr) && !isCover) {
return;
}
// 3. field 类型相同时设置
Class<?> getterType = metaObject.getGetterType(fieldName);
if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
metaObject.setValue(fieldName, fieldVal);
}
}
/**
* 获取 spring security 当前的用户名
* @return 当前用户名
*/
private String getUserName() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 匿名接口直接返回
if (authentication instanceof AnonymousAuthenticationToken) {
return null;
}
if (Optional.ofNullable(authentication).isPresent()) {
return authentication.getName();
}
return null;
}
}
@@ -0,0 +1,31 @@
package com.bsd.pjcloud.common.data.mybatis;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import java.util.ArrayList;
import java.util.List;
/**
* Mybatis 配置
*
* @author pjcloud
* @date 2021/6/3
*/
@Data
@RefreshScope
@ConfigurationProperties("pjcloud.mybatis")
public class PjcloudMybatisProperties {
/**
* 是否打印可执行 sql
*/
private boolean showSql = true;
/**
* 跳过表
*/
private List<String> skipTable = new ArrayList<>();
}
@@ -0,0 +1,99 @@
package com.bsd.pjcloud.common.data.resolver;
import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.bsd.pjcloud.admin.api.entity.SysDictItem;
import com.bsd.pjcloud.admin.api.feign.RemoteDictService;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import lombok.experimental.UtilityClass;
import java.util.List;
/**
* @author fxz
* @date 2022/3/24 字典解析器
*/
@UtilityClass
public class DictResolver {
/**
* 根据字典类型获取所有字典项
* @param type 字典类型
* @return 字典数据项集合
*/
public List<SysDictItem> getDictItemsByType(String type) {
Assert.isTrue(StringUtils.isNotBlank(type), "参数不合法");
RemoteDictService remoteDictService = SpringContextHolder.getBean(RemoteDictService.class);
return remoteDictService.getDictByType(type).getData();
}
/**
* 根据字典类型以及字典项字典值获取字典标签
* @param type 字典类型
* @param itemValue 字典项字典值
* @return 字典项标签值
*/
public String getDictItemLabel(String type, String itemValue) {
Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemValue), "参数不合法");
SysDictItem sysDictItem = getDictItemByItemValue(type, itemValue);
return ObjectUtils.isNotEmpty(sysDictItem) ? sysDictItem.getLabel() : StringPool.EMPTY;
}
/**
* 根据字典类型以及字典标签获取字典值
* @param type 字典类型
* @param itemLabel 字典数据标签
* @return 字典数据项值
*/
public String getDictItemValue(String type, String itemLabel) {
Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemLabel), "参数不合法");
SysDictItem sysDictItem = getDictItemByItemLabel(type, itemLabel);
return ObjectUtils.isNotEmpty(sysDictItem) ? sysDictItem.getItemValue() : StringPool.EMPTY;
}
/**
* 根据字典类型以及字典值获取字典项
* @param type 字典类型
* @param itemValue 字典数据值
* @return 字典数据项
*/
public SysDictItem getDictItemByItemValue(String type, String itemValue) {
Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemValue), "参数不合法");
List<SysDictItem> dictItemList = getDictItemsByType(type);
if (CollectionUtils.isNotEmpty(dictItemList)) {
return dictItemList.stream().filter(item -> itemValue.equals(item.getItemValue())).findFirst().orElse(null);
}
return null;
}
/**
* 根据字典类型以及字典标签获取字典项
* @param type 字典类型
* @param itemLabel 字典数据项标签
* @return 字典数据项
*/
public SysDictItem getDictItemByItemLabel(String type, String itemLabel) {
Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemLabel), "参数不合法");
List<SysDictItem> dictItemList = getDictItemsByType(type);
if (CollectionUtils.isNotEmpty(dictItemList)) {
return dictItemList.stream().filter(item -> itemLabel.equals(item.getLabel())).findFirst().orElse(null);
}
return null;
}
}
@@ -0,0 +1,77 @@
package com.bsd.pjcloud.common.data.resolver;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.feign.RemoteParamService;
import com.bsd.pjcloud.common.core.util.SpringContextHolder;
import lombok.experimental.UtilityClass;
import java.util.Map;
import java.util.Objects;
/**
* @author pjcloud
* @date 2020/5/12
* <p>
* 系统参数配置解析器
*/
@UtilityClass
public class ParamResolver {
/**
* 根据多个key 查询value 配置 结果使用hutool 的maputil 进行包装处理 MapUtil.getBool(result,key)
* @param key key
* @return Map<String,Object>
*/
public Map<String, Object> getMap(String... key) {
// 校验入参是否合法
if (Objects.isNull(key)) {
throw new IllegalArgumentException("参数不合法");
}
RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
return remoteParamService.getByKeys(key).getData();
}
/**
* 根据key 查询value 配置
* @param key key
* @param defaultVal 默认值
* @return value
*/
public Long getLong(String key, Long... defaultVal) {
return checkAndGet(key, Long.class, defaultVal);
}
/**
* 根据key 查询value 配置
* @param key key
* @param defaultVal 默认值
* @return value
*/
public String getStr(String key, String... defaultVal) {
return checkAndGet(key, String.class, defaultVal);
}
private <T> T checkAndGet(String key, Class<T> clazz, T... defaultVal) {
// 校验入参是否合法
if (StrUtil.isBlank(key) || defaultVal.length > 1) {
throw new IllegalArgumentException("参数不合法");
}
RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
String result = remoteParamService.getByKey(key).getData();
if (StrUtil.isNotBlank(result)) {
return Convert.convert(clazz, result);
}
if (defaultVal.length == 1) {
return Convert.convert(clazz, defaultVal.clone()[0]);
}
return null;
}
}
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.resolver;
import cn.hutool.core.comparator.CompareUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @author pjcloud
* @date 2019-06-24
* <p>
* 解决Mybatis Plus Order By SQL注入问题
*/
@Slf4j
public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 判断Controller是否包含page 参数
*
* @param parameter 参数
* @return 是否过滤
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Page.class);
}
/**
* @param parameter 入参集合
* @param mavContainer model 和 view
* @param webRequest web相关
* @param binderFactory 入参解析
* @return 检查后新的page对象
* <p>
* page 只支持查询 GET .如需解析POST获取请求报文体处理
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String ascs = request.getParameter("ascs");
String descs = request.getParameter("descs");
String current = request.getParameter("current");
String size = request.getParameter("size");
Page page = new Page();
if (StrUtil.isNotBlank(current)) {
// 如果current page 小于零 视为不合法数据
if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
throw new CheckedException("current page error");
}
page.setCurrent(Long.parseLong(current));
}
if (StrUtil.isNotBlank(size)) {
page.setSize(Long.parseLong(size));
}
List<OrderItem> orderItemList = new ArrayList<>();
Optional.ofNullable(ascs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(asc -> !SqlInjectionUtils.check(asc))
.map(OrderItem::asc)
.collect(Collectors.toList())));
Optional.ofNullable(descs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(desc -> !SqlInjectionUtils.check(desc))
.map(OrderItem::desc)
.collect(Collectors.toList())));
page.addOrder(orderItemList);
return page;
}
}
@@ -0,0 +1,34 @@
package com.bsd.pjcloud.common.data.resolver;
import com.bsd.pjcloud.common.core.util.KeyStrResolver;
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
/**
* @author pjcloud
* @date 2020/9/29
* <p>
* 租户字符串处理(方便其他模块获取)
*/
public class TenantKeyStrResolver implements KeyStrResolver {
/**
* 传入字符串增加 租户编号:in
* @param in 输入字符串
* @param split 分割符
* @return
*/
@Override
public String extract(String in, String split) {
return TenantContextHolder.getTenantId() + split + in;
}
/**
* 返回当前租户ID
* @return
*/
@Override
public String key() {
return String.valueOf(TenantContextHolder.getTenantId());
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
/**
* @author pjcloud
* @date 2018/9/14
*/
@Slf4j
public class PjcloudFeignTenantInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
if (TenantContextHolder.getTenantId() == null) {
log.debug("TTL 中的 租户ID为空,feign拦截器 >> 跳过");
return;
}
requestTemplate.header(CommonConstants.TENANT_ID, TenantContextHolder.getTenantId().toString());
}
}
@@ -0,0 +1,32 @@
package com.bsd.pjcloud.common.data.tenant;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* 多租户配置
*
* @author oathsign
*/
@Data
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "pjcloud.tenant")
public class PjcloudTenantConfigProperties {
/**
* 维护租户列名称
*/
private String column = "tenant_id";
/**
* 多租户的数据表集合
*/
private List<String> tables = new ArrayList<>();
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
/**
* @author pjcloud
* @date 2020/4/29
* <p>
* 租户信息拦截
*/
@Configuration
public class PjcloudTenantConfiguration {
@Bean
public RequestInterceptor pjcloudFeignTenantInterceptor() {
return new PjcloudFeignTenantInterceptor();
}
@Bean
public ClientHttpRequestInterceptor pjcloudTenantRequestInterceptor() {
return new TenantRequestInterceptor();
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.bsd.pjcloud.common.core.util.TenantTable;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import org.springframework.core.annotation.AnnotationUtils;
import java.util.Objects;
/**
* @author pjcloud
* @date 2018-12-26
* <p>
* 租户维护处理器
*/
@Slf4j
@RequiredArgsConstructor
public class PjcloudTenantHandler implements TenantLineHandler {
private final PjcloudTenantConfigProperties properties;
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
*
* @return 租户 ID 值表达式
*/
@Override
public Expression getTenantId() {
Long tenantId = TenantContextHolder.getTenantId();
log.debug("当前租户为 >> {}", tenantId);
if (tenantId == null) {
return new NullValue();
}
return new LongValue(tenantId);
}
/**
* 获取租户字段名
*
* @return 租户字段名
*/
@Override
public String getTenantIdColumn() {
return properties.getColumn();
}
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
@Override
public boolean ignoreTable(String tableName) {
// 判断是否跳过当前查询的租户过滤
if (TenantContextHolder.getTenantSkip()) {
return Boolean.TRUE;
}
Long tenantId = TenantContextHolder.getTenantId();
// 租户中ID 为空,查询全部,不进行过滤
if (tenantId == null) {
return Boolean.TRUE;
}
// 判断实体类是否标记为多租户隔离
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
TenantTable annotation = null;
if (Objects.nonNull(tableInfo)) {
annotation = AnnotationUtils.findAnnotation(tableInfo.getEntityType(), TenantTable.class);
}
return !properties.getTables().contains(tableName) && annotation == null;
}
}
@@ -0,0 +1,147 @@
package com.bsd.pjcloud.common.data.tenant;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.util.function.Supplier;
/**
* 租户运行时代理<br/>
* 这是一个工具类,用于切换租户运行时,保护租户ID上下文<br/>
* 下面这段代码演示问题所在 <pre>
* void methodA(){
* // 因为某些特殊原因,需要手动指定租户
* TenantContextHolder.setTenantId(1);
* // do something ...
* }
* void methodB(){
* // 因为某些特殊原因,需要手动指定租户
* TenantContextHolder.setTenantId(2);
* methodA();
* // 此时租户ID已经变成 1
* // do something ...
* }
* </pre> 嵌套设置租户ID会导致租户上下文难以维护,并且很难察觉,容易导致数据错乱。 推荐的写法: <pre>
* void methodA(){
* TenantBroker.RunAs(1,() -> {
* // do something ...
* });
* }
* void methodB(){
* TenantBroker.RunAs(2,() -> {
* methodA();
* // do something ...
* });
* }
* </pre>
*
* @author CJ (jclazz@outlook.com)
* @date 2020/6/12
* @since 3.9
*/
@Slf4j
@UtilityClass
public class TenantBroker {
@FunctionalInterface
public interface RunAs<T> {
/**
* 执行业务逻辑
* @param tenantId
* @throws Exception
*/
void run(T tenantId) throws Exception;
}
@FunctionalInterface
public interface ApplyAs<T, R> {
/**
* 执行业务逻辑,返回一个值
* @param tenantId
* @return
* @throws Exception
*/
R apply(T tenantId) throws Exception;
}
/**
* 以某个租户的身份运行
* @param tenant 租户ID
* @param func
*/
public void runAs(Long tenant, RunAs<Long> func) {
final Long pre = TenantContextHolder.getTenantId();
try {
log.trace("TenantBroker 切换租户{} -> {}", pre, tenant);
TenantContextHolder.setTenantId(tenant);
func.run(tenant);
}
catch (Exception e) {
throw new TenantBrokerExceptionWrapper(e.getMessage(), e);
}
finally {
log.trace("TenantBroker 还原租户{} <- {}", pre, tenant);
TenantContextHolder.setTenantId(pre);
}
}
/**
* 以某个租户的身份运行
* @param tenant 租户ID
* @param func
* @param <T> 返回数据类型
* @return
*/
public <T> T applyAs(Long tenant, ApplyAs<Long, T> func) {
final Long pre = TenantContextHolder.getTenantId();
try {
log.trace("TenantBroker 切换租户{} -> {}", pre, tenant);
TenantContextHolder.setTenantId(tenant);
return func.apply(tenant);
}
catch (Exception e) {
throw new TenantBrokerExceptionWrapper(e.getMessage(), e);
}
finally {
log.trace("TenantBroker 还原租户{} <- {}", pre, tenant);
TenantContextHolder.setTenantId(pre);
}
}
/**
* 以某个租户的身份运行
* @param supplier
* @param func
*/
public void runAs(Supplier<Long> supplier, RunAs<Long> func) {
runAs(supplier.get(), func);
}
/**
* 以某个租户的身份运行
* @param supplier
* @param func
* @param <T> 返回数据类型
* @return
*/
public <T> T applyAs(Supplier<Long> supplier, ApplyAs<Long, T> func) {
return applyAs(supplier.get(), func);
}
public static class TenantBrokerExceptionWrapper extends RuntimeException {
public TenantBrokerExceptionWrapper(String message, Throwable cause) {
super(message, cause);
}
public TenantBrokerExceptionWrapper(Throwable cause) {
super(cause);
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.experimental.UtilityClass;
/**
* @author pjcloud
* @date 2018/10/4 租户工具类
*/
@UtilityClass
public class TenantContextHolder {
private final ThreadLocal<Long> THREAD_LOCAL_TENANT = new TransmittableThreadLocal<>();
private final ThreadLocal<Boolean> THREAD_LOCAL_TENANT_SKIP_FLAG = new TransmittableThreadLocal<>();
/**
* TTL 设置租户ID<br/>
* <b>谨慎使用此方法,避免嵌套调用。尽量使用 {@code TenantBroker} </b>
* @param tenantId
* @see TenantBroker
*/
public void setTenantId(Long tenantId) {
THREAD_LOCAL_TENANT.set(tenantId);
}
/**
* 设置是否过滤的标识
*/
public void setTenantSkip() {
THREAD_LOCAL_TENANT_SKIP_FLAG.set(Boolean.TRUE);
}
/**
* 获取TTL中的租户ID
* @return
*/
public Long getTenantId() {
return THREAD_LOCAL_TENANT.get();
}
/**
* 获取是否跳过租户过滤的标识
* @return
*/
public Boolean getTenantSkip() {
return THREAD_LOCAL_TENANT_SKIP_FLAG.get() != null && THREAD_LOCAL_TENANT_SKIP_FLAG.get();
}
public void clear() {
THREAD_LOCAL_TENANT.remove();
THREAD_LOCAL_TENANT_SKIP_FLAG.remove();
}
}
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.data.tenant;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.entity.SysTenant;
import com.bsd.pjcloud.common.core.constant.CacheConstants;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import java.util.List;
/**
* @author pjcloud
* @date 2018/9/13
*/
@Slf4j
@Component
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TenantContextHolderFilter extends GenericFilterBean {
private final static String UNDEFINED_STR = "undefined";
private final CacheManager cacheManager;
@Override
@SneakyThrows
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String headerTenantId = request.getHeader(CommonConstants.TENANT_ID);
String paramTenantId = request.getParameter(CommonConstants.TENANT_ID);
log.debug("获取header中的租户ID为:{}", headerTenantId);
if (StrUtil.isNotBlank(headerTenantId) && !StrUtil.equals(UNDEFINED_STR, headerTenantId)) {
TenantContextHolder.setTenantId(Long.parseLong(headerTenantId));
} else if (StrUtil.isNotBlank(paramTenantId) && !StrUtil.equals(UNDEFINED_STR, paramTenantId)) {
TenantContextHolder.setTenantId(Long.parseLong(paramTenantId));
} else {
TenantContextHolder.setTenantId(CommonConstants.TENANT_ID_1);
}
if (!checkTenantStatus(request, response)) {
TenantContextHolder.clear();
return;
}
try {
filterChain.doFilter(request, response);
} finally {
TenantContextHolder.clear();
}
}
/**
* 检查租户状态
*
* @param request 请求
* @param response 响应
* @return boolean
*/
private boolean checkTenantStatus(HttpServletRequest request, HttpServletResponse response) {
// 如果是获取租户列表请求跳过
if (StrUtil.containsAnyIgnoreCase(request.getRequestURI(), "/tenant/list")) {
return true;
}
// 判断租户状态
Cache cache = cacheManager.getCache(CacheConstants.TENANT_DETAILS);
if (cache == null) {
return true;
}
List<SysTenant> tenantList = cache.get(SimpleKey.EMPTY, List.class);
if (CollUtil.isEmpty(tenantList)) {
return true;
}
boolean exist = tenantList.stream().anyMatch(tenant -> NumberUtil.equals(tenant.getId(), TenantContextHolder.getTenantId()));
if (exist) {
return true;
}
response.setStatus(HttpStatus.UPGRADE_REQUIRED.value());
return false;
}
}
@@ -0,0 +1,30 @@
package com.bsd.pjcloud.common.data.tenant;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
/**
* @author pjcloud
* @date 2020/4/29
* <p>
* 传递 RestTemplate 请求的租户ID
*/
public class TenantRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
if (TenantContextHolder.getTenantId() != null) {
request.getHeaders().set(CommonConstants.TENANT_ID, String.valueOf(TenantContextHolder.getTenantId()));
}
return execution.execute(request, body);
}
}
@@ -0,0 +1,9 @@
com.bsd.pjcloud.common.data.cache.RedisTemplateConfiguration
com.bsd.pjcloud.common.data.cache.RedisMessageConfiguration
com.bsd.pjcloud.common.data.cache.RedisCacheManagerConfiguration
com.bsd.pjcloud.common.data.cache.RedisCacheAutoConfiguration
com.bsd.pjcloud.common.data.tenant.PjcloudTenantConfigProperties
com.bsd.pjcloud.common.data.tenant.TenantContextHolderFilter
com.bsd.pjcloud.common.data.tenant.PjcloudTenantConfiguration
com.bsd.pjcloud.common.data.mybatis.MybatisPlusConfiguration
com.bsd.pjcloud.common.data.resolver.TenantKeyStrResolver
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pjcloud-common</artifactId>
<groupId>com.bsd</groupId>
<version>5.7.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-datasource</artifactId>
<packaging>jar</packaging>
<description>pjcloud 动态切换数据源</description>
<dependencies>
<dependency>
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-core</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
</dependency>
<!-- druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<scope>provided</scope>
</dependency>
<!--拦截器依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.datasource;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.creator.druid.DruidDataSourceCreator;
import com.baomidou.dynamic.datasource.creator.hikaricp.HikariDataSourceCreator;
import com.baomidou.dynamic.datasource.processor.DsJakartaHeaderProcessor;
import com.baomidou.dynamic.datasource.processor.DsJakartaSessionProcessor;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.bsd.pjcloud.common.datasource.config.*;
import lombok.RequiredArgsConstructor;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.expression.BeanFactoryResolver;
import java.util.ArrayList;
import java.util.List;
/**
* @author pjcloud
* @date 2020-02-06
* <p>
* 动态数据源切换配置
*/
@Configuration
@RequiredArgsConstructor
@Import(DynamicLogConfiguration.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(DruidDataSourceProperties.class)
public class DynamicDataSourceAutoConfiguration {
/**
* 获取动态数据源提供者
* @param defaultDataSourceCreator 默认数据源创建器
* @param stringEncryptor 字符串加密器
* @param properties 数据源属性
* @return 动态数据源提供者
*/
@Bean
public DynamicDataSourceProvider dynamicDataSourceProvider(DefaultDataSourceCreator defaultDataSourceCreator,
StringEncryptor stringEncryptor, DruidDataSourceProperties properties) {
return new JdbcDynamicDataSourceProvider(defaultDataSourceCreator, stringEncryptor, properties);
}
/**
* 获取默认数据源创建器
* @param druidDataSourceCreator Druid数据源创建器
* @return 默认数据源创建器
*/
@Bean
public DefaultDataSourceCreator defaultDataSourceCreator(DruidDataSourceCreator druidDataSourceCreator) {
DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();
List<DataSourceCreator> creators = new ArrayList<>();
creators.add(druidDataSourceCreator);
defaultDataSourceCreator.setCreators(creators);
return defaultDataSourceCreator;
}
/**
* 获取数据源处理器
* @return 数据源处理器
*/
@Bean
public DsProcessor dsProcessor(BeanFactory beanFactory) {
DsProcessor lastParamDsProcessor = new LastParamDsProcessor();
DsProcessor headerProcessor = new DsJakartaHeaderProcessor();
DsProcessor sessionProcessor = new DsJakartaSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
lastParamDsProcessor.setNextProcessor(headerProcessor);
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return lastParamDsProcessor;
}
/**
* 获取清除TTL数据源过滤器
* @return 清除TTL数据源过滤器
*/
@Bean
public ClearTtlDataSourceFilter clearTtlDsFilter() {
return new ClearTtlDataSourceFilter();
}
}
@@ -0,0 +1,24 @@
package com.bsd.pjcloud.common.datasource.annotation;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import com.bsd.pjcloud.common.datasource.DynamicDataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author Lucky
* @date 2019-05-18
* <p>
* 开启动态数据源
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableAutoConfiguration(exclude = { DruidDataSourceAutoConfigure.class })
@Import(DynamicDataSourceAutoConfiguration.class)
public @interface EnableDynamicDataSource {
}
@@ -0,0 +1,34 @@
package com.bsd.pjcloud.common.datasource.config;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.springframework.core.Ordered;
import org.springframework.web.filter.GenericFilterBean;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
/**
* @author pjcloud
* @date 2020/12/11
* <p>
* 清空上文的DS 设置避免污染当前线程
*/
public class ClearTtlDataSourceFilter extends GenericFilterBean implements Ordered {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
DynamicDataSourceContextHolder.clear();
filterChain.doFilter(servletRequest, servletResponse);
DynamicDataSourceContextHolder.clear();
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
@@ -0,0 +1,41 @@
package com.bsd.pjcloud.common.datasource.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author pjcloud
* @date 2019-05-14
* <p>
* 参考DruidDataSourceWrapper
*/
@Data
@ConfigurationProperties("spring.datasource.druid")
public class DruidDataSourceProperties {
/**
* 数据源用户名
*/
private String username;
/**
* 数据源密码
*/
private String password;
/**
* jdbcurl
*/
private String url;
/**
* 数据源驱动
*/
private String driverClassName;
/**
* 查询数据源的SQL
*/
private String queryDsSql = "select * from gen_datasource_conf where del_flag = '0'";
}
@@ -0,0 +1,14 @@
package com.bsd.pjcloud.common.datasource.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
/**
* @author pjcloud
* @date 2022/8/8
*
* 注入SQL 格式化的插件
*/
@ConditionalOnClass(name = "com.bsd.pjcloud.common.data.mybatis.DruidSqlLogFilter")
public class DynamicLogConfiguration {
}
@@ -0,0 +1,114 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.datasource.config;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.creator.druid.DruidConfig;
import com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider;
import com.bsd.pjcloud.common.datasource.support.DataSourceConstants;
import com.bsd.pjcloud.common.datasource.util.DsConfTypeEnum;
import com.bsd.pjcloud.common.datasource.util.DsJdbcUrlEnum;
import org.jasypt.encryption.StringEncryptor;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
/**
* @author pjcloud
* @date 2020/2/6
* <p>
* 从数据源中获取 配置信息
*/
public class JdbcDynamicDataSourceProvider extends AbstractJdbcDataSourceProvider {
private static final String SQL_LOG_FILTER = "sqlLogFilter";
private final DruidDataSourceProperties properties;
private final StringEncryptor stringEncryptor;
public JdbcDynamicDataSourceProvider(DefaultDataSourceCreator defaultDataSourceCreator,
StringEncryptor stringEncryptor, DruidDataSourceProperties properties) {
super(defaultDataSourceCreator, properties.getDriverClassName(), properties.getUrl(), properties.getUsername(),
properties.getPassword());
this.stringEncryptor = stringEncryptor;
this.properties = properties;
}
/**
* 执行语句获得数据源参数
*
* @param statement 语句
* @return 数据源参数
* @throws SQLException sql异常
*/
@Override
protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException {
ResultSet rs = statement.executeQuery(properties.getQueryDsSql());
Map<String, DataSourceProperty> map = new HashMap<>(8);
while (rs.next()) {
String name = rs.getString(DataSourceConstants.NAME);
String username = rs.getString(DataSourceConstants.DS_USER_NAME);
String password = rs.getString(DataSourceConstants.DS_USER_PWD);
Integer confType = rs.getInt(DataSourceConstants.DS_CONFIG_TYPE);
String dsType = rs.getString(DataSourceConstants.DS_TYPE);
DataSourceProperty property = new DataSourceProperty();
property.setUsername(username);
property.setPassword(stringEncryptor.decrypt(password));
String url;
// JDBC 配置形式
DsJdbcUrlEnum urlEnum = DsJdbcUrlEnum.get(dsType);
if (DsConfTypeEnum.JDBC.getType().equals(confType)) {
url = rs.getString(DataSourceConstants.DS_JDBC_URL);
} else {
String host = rs.getString(DataSourceConstants.DS_HOST);
String port = rs.getString(DataSourceConstants.DS_PORT);
String dsName = rs.getString(DataSourceConstants.DS_NAME);
url = String.format(urlEnum.getUrl(), host, port, dsName);
}
// Druid Config
DruidConfig druidConfig = new DruidConfig();
druidConfig.setProxyFilters(SQL_LOG_FILTER);
druidConfig.setValidationQuery(urlEnum.getValidationQuery());
property.setDruid(druidConfig);
property.setUrl(url);
map.put(name, property);
}
// 添加默认主数据源
DataSourceProperty property = new DataSourceProperty();
property.setUsername(properties.getUsername());
property.setPassword(properties.getPassword());
property.setUrl(properties.getUrl());
DruidConfig druidConfig = new DruidConfig();
druidConfig.setProxyFilters(SQL_LOG_FILTER);
property.setDruid(druidConfig);
map.put(DataSourceConstants.DS_MASTER, property);
return map;
}
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2025, pjcloud All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: pjcloud
*/
package com.bsd.pjcloud.common.datasource.config;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.aopalliance.intercept.MethodInvocation;
/**
* @author pjcloud
* @date 2020/2/6
* <p>
* 参数数据源解析 @DS("#last)
*/
public class LastParamDsProcessor extends DsProcessor {
private static final String LAST_PREFIX = "#last";
/**
* 抽象匹配条件 匹配才会走当前执行器否则走下一级执行器
* @param key DS注解里的内容
* @return 是否匹配
*/
@Override
public boolean matches(String key) {
if (key.startsWith(LAST_PREFIX)) {
// https://github.com/baomidou/dynamic-datasource-spring-boot-starter/issues/213
DynamicDataSourceContextHolder.clear();
return true;
}
return false;
}
/**
* 抽象最终决定数据源
* @param invocation 方法执行信息
* @param key DS注解里的内容
* @return 数据源名称
*/
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
Object[] arguments = invocation.getArguments();
return String.valueOf(arguments[arguments.length - 1]);
}
}
@@ -0,0 +1,66 @@
package com.bsd.pjcloud.common.datasource.support;
/**
* @author pjcloud
* @date 2019-04-01
* <p>
* 数据源相关常量
*/
public interface DataSourceConstants {
/**
* 数据源名称
*/
String NAME = "name";
/**
* 默认数据源(master)
*/
String DS_MASTER = "master";
/**
* jdbcurl
*/
String DS_JDBC_URL = "url";
/**
* 配置类型
*/
String DS_CONFIG_TYPE = "conf_type";
/**
* 用户名
*/
String DS_USER_NAME = "username";
/**
* 密码
*/
String DS_USER_PWD = "password";
/**
* 数据库类型
*/
String DS_TYPE = "ds_type";
/**
* 数据库名称
*/
String DS_NAME = "ds_name";
/**
* 主机类型
*/
String DS_HOST = "host";
/**
* 端口
*/
String DS_PORT = "port";
/**
* 实例名称
*/
String DS_INSTANCE = "instance";
}
@@ -0,0 +1,30 @@
package com.bsd.pjcloud.common.datasource.util;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author pjcloud
* @date 2020/12/11
* <p>
* 数据源配置类型
*/
@Getter
@AllArgsConstructor
public enum DsConfTypeEnum {
/**
* 主机链接
*/
HOST(0, "主机链接"),
/**
* JDBC链接
*/
JDBC(1, "JDBC链接");
private final Integer type;
private final String description;
}
Loaded 100 of 1595 files, more files were not shown because too many files have changed in this diff. Show more