fys
This commit is contained in:
@ -0,0 +1,40 @@
|
||||
package com.fuyuanshen.common.mybatis.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限注解,用于标记数据权限的占位符关键字和替换值
|
||||
* <p>
|
||||
* 一个注解只能对应一个模板
|
||||
* </p>
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface DataColumn {
|
||||
|
||||
/**
|
||||
* 数据权限模板的占位符关键字,默认为 "deptName"
|
||||
*
|
||||
* @return 占位符关键字数组
|
||||
*/
|
||||
String[] key() default "deptName";
|
||||
|
||||
/**
|
||||
* 数据权限模板的占位符替换值,默认为 "dept_id"
|
||||
*
|
||||
* @return 占位符替换值数组
|
||||
*/
|
||||
String[] value() default "dept_id";
|
||||
|
||||
/**
|
||||
* 权限标识符 用于通过菜单权限标识符来获取数据权限
|
||||
* 拥有此标识符的角色 将不会拼接此角色的数据过滤sql
|
||||
*
|
||||
* @return 权限标识符
|
||||
*/
|
||||
String permission() default "";
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.fuyuanshen.common.mybatis.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限组注解,用于标记数据权限配置数组
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface DataPermission {
|
||||
|
||||
/**
|
||||
* 数据权限配置数组,用于指定数据权限的占位符关键字和替换值
|
||||
*
|
||||
* @return 数据权限配置数组
|
||||
*/
|
||||
DataColumn[] value();
|
||||
|
||||
/**
|
||||
* 权限拼接标识符(用于指定连接语句的sql符号)
|
||||
* 如不填 默认 select 用 OR 其他语句用 AND
|
||||
* 内容 OR 或者 AND
|
||||
*/
|
||||
String joinStr() default "";
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package com.fuyuanshen.common.mybatis.aspect;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import com.fuyuanshen.common.mybatis.annotation.DataPermission;
|
||||
import com.fuyuanshen.common.mybatis.helper.DataPermissionHelper;
|
||||
|
||||
/**
|
||||
* 数据权限处理
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
public class DataPermissionAspect {
|
||||
|
||||
/**
|
||||
* 处理请求前执行
|
||||
*/
|
||||
@Before(value = "@annotation(dataPermission)")
|
||||
public void doBefore(JoinPoint joinPoint, DataPermission dataPermission) {
|
||||
DataPermissionHelper.setPermission(dataPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理完请求后执行
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@AfterReturning(pointcut = "@annotation(dataPermission)")
|
||||
public void doAfterReturning(JoinPoint joinPoint, DataPermission dataPermission) {
|
||||
DataPermissionHelper.removePermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截异常操作
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param e 异常
|
||||
*/
|
||||
@AfterThrowing(value = "@annotation(dataPermission)", throwing = "e")
|
||||
public void doAfterThrowing(JoinPoint joinPoint, DataPermission dataPermission, Exception e) {
|
||||
DataPermissionHelper.removePermission();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package com.fuyuanshen.common.mybatis.config;
|
||||
|
||||
import cn.hutool.core.net.NetUtil;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
|
||||
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
||||
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.fuyuanshen.common.core.factory.YmlPropertySourceFactory;
|
||||
import com.fuyuanshen.common.core.utils.SpringUtils;
|
||||
import com.fuyuanshen.common.mybatis.aspect.DataPermissionAspect;
|
||||
import com.fuyuanshen.common.mybatis.handler.InjectionMetaObjectHandler;
|
||||
import com.fuyuanshen.common.mybatis.handler.MybatisExceptionHandler;
|
||||
import com.fuyuanshen.common.mybatis.handler.PlusPostInitTableInfoHandler;
|
||||
import com.fuyuanshen.common.mybatis.interceptor.PlusDataPermissionInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* mybatis-plus配置类(下方注释有插件介绍)
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@EnableTransactionManagement(proxyTargetClass = true)
|
||||
@MapperScan("${mybatis-plus.mapperPackage}")
|
||||
@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 多租户插件 必须放到第一位
|
||||
try {
|
||||
TenantLineInnerInterceptor tenant = SpringUtils.getBean(TenantLineInnerInterceptor.class);
|
||||
interceptor.addInnerInterceptor(tenant);
|
||||
} catch (BeansException ignore) {
|
||||
}
|
||||
// 数据权限处理
|
||||
interceptor.addInnerInterceptor(dataPermissionInterceptor());
|
||||
// 分页插件
|
||||
interceptor.addInnerInterceptor(paginationInnerInterceptor());
|
||||
// 乐观锁插件
|
||||
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限拦截器
|
||||
*/
|
||||
public PlusDataPermissionInterceptor dataPermissionInterceptor() {
|
||||
return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据权限切面处理器
|
||||
*/
|
||||
@Bean
|
||||
public DataPermissionAspect dataPermissionAspect() {
|
||||
return new DataPermissionAspect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页插件,自动识别数据库类型
|
||||
*/
|
||||
public PaginationInnerInterceptor paginationInnerInterceptor() {
|
||||
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
|
||||
// 分页合理化
|
||||
paginationInnerInterceptor.setOverflow(true);
|
||||
return paginationInnerInterceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 乐观锁插件
|
||||
*/
|
||||
public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {
|
||||
return new OptimisticLockerInnerInterceptor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 元对象字段填充控制器
|
||||
*/
|
||||
@Bean
|
||||
public MetaObjectHandler metaObjectHandler() {
|
||||
return new InjectionMetaObjectHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用网卡信息绑定雪花生成器
|
||||
* 防止集群雪花ID重复
|
||||
*/
|
||||
@Bean
|
||||
public IdentifierGenerator idGenerator() {
|
||||
return new DefaultIdentifierGenerator(NetUtil.getLocalhost());
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常处理器
|
||||
*/
|
||||
@Bean
|
||||
public MybatisExceptionHandler mybatisExceptionHandler() {
|
||||
return new MybatisExceptionHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表对象处理器
|
||||
*/
|
||||
@Bean
|
||||
public PostInitTableInfoHandler postInitTableInfoHandler() {
|
||||
return new PlusPostInitTableInfoHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* PaginationInnerInterceptor 分页插件,自动识别数据库类型
|
||||
* https://baomidou.com/pages/97710a/
|
||||
* OptimisticLockerInnerInterceptor 乐观锁插件
|
||||
* https://baomidou.com/pages/0d93c0/
|
||||
* MetaObjectHandler 元对象字段填充控制器
|
||||
* https://baomidou.com/pages/4c6bcf/
|
||||
* ISqlInjector sql注入器
|
||||
* https://baomidou.com/pages/42ea4a/
|
||||
* BlockAttackInnerInterceptor 如果是对全表的删除或更新操作,就会终止该操作
|
||||
* https://baomidou.com/pages/f9a237/
|
||||
* IllegalSQLInnerInterceptor sql性能规范插件(垃圾SQL拦截)
|
||||
* IdentifierGenerator 自定义主键策略
|
||||
* https://baomidou.com/pages/568eb2/
|
||||
* TenantLineInnerInterceptor 多租户插件
|
||||
* https://baomidou.com/pages/aef2f2/
|
||||
* DynamicTableNameInnerInterceptor 动态表名插件
|
||||
* https://baomidou.com/pages/2a45ff/
|
||||
*/
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package com.fuyuanshen.common.mybatis.core.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Entity基类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 搜索值
|
||||
*/
|
||||
@JsonIgnore
|
||||
@TableField(exist = false)
|
||||
private String searchValue;
|
||||
|
||||
/**
|
||||
* 创建部门
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createDept;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@TableField(exist = false)
|
||||
private Map<String, Object> params = new HashMap<>();
|
||||
|
||||
}
|
@ -0,0 +1,334 @@
|
||||
package com.fuyuanshen.common.mybatis.core.mapper;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
import org.apache.ibatis.logging.Log;
|
||||
import org.apache.ibatis.logging.LogFactory;
|
||||
import com.fuyuanshen.common.core.utils.MapstructUtils;
|
||||
import com.fuyuanshen.common.core.utils.StreamUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 自定义 Mapper 接口, 实现 自定义扩展
|
||||
*
|
||||
* @param <T> table 泛型
|
||||
* @param <V> vo 泛型
|
||||
* @author Lion Li
|
||||
* @since 2021-05-13
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
|
||||
Log log = LogFactory.getLog(BaseMapperPlus.class);
|
||||
|
||||
/**
|
||||
* 获取当前实例对象关联的泛型类型 V 的 Class 对象
|
||||
*
|
||||
* @return 返回当前实例对象关联的泛型类型 V 的 Class 对象
|
||||
*/
|
||||
default Class<V> currentVoClass() {
|
||||
return (Class<V>) GenericTypeUtils.resolveTypeArguments(this.getClass(), BaseMapperPlus.class)[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前实例对象关联的泛型类型 T 的 Class 对象
|
||||
*
|
||||
* @return 返回当前实例对象关联的泛型类型 T 的 Class 对象
|
||||
*/
|
||||
default Class<T> currentModelClass() {
|
||||
return (Class<T>) GenericTypeUtils.resolveTypeArguments(this.getClass(), BaseMapperPlus.class)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的查询条件查询并返回结果列表
|
||||
*
|
||||
* @return 返回查询结果的列表
|
||||
*/
|
||||
default List<T> selectList() {
|
||||
return this.selectList(new QueryWrapper<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入实体对象集合
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @return 插入操作是否成功的布尔值
|
||||
*/
|
||||
default boolean insertBatch(Collection<T> entityList) {
|
||||
return Db.saveBatch(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量根据ID更新实体对象集合
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @return 更新操作是否成功的布尔值
|
||||
*/
|
||||
default boolean updateBatchById(Collection<T> entityList) {
|
||||
return Db.updateBatchById(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入或更新实体对象集合
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @return 插入或更新操作是否成功的布尔值
|
||||
*/
|
||||
default boolean insertOrUpdateBatch(Collection<T> entityList) {
|
||||
return Db.saveOrUpdateBatch(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入实体对象集合并指定批处理大小
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 批处理大小
|
||||
* @return 插入操作是否成功的布尔值
|
||||
*/
|
||||
default boolean insertBatch(Collection<T> entityList, int batchSize) {
|
||||
return Db.saveBatch(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量根据ID更新实体对象集合并指定批处理大小
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 批处理大小
|
||||
* @return 更新操作是否成功的布尔值
|
||||
*/
|
||||
default boolean updateBatchById(Collection<T> entityList, int batchSize) {
|
||||
return Db.updateBatchById(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入或更新实体对象集合并指定批处理大小
|
||||
*
|
||||
* @param entityList 实体对象集合
|
||||
* @param batchSize 批处理大小
|
||||
* @return 插入或更新操作是否成功的布尔值
|
||||
*/
|
||||
default boolean insertOrUpdateBatch(Collection<T> entityList, int batchSize) {
|
||||
return Db.saveOrUpdateBatch(entityList, batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询单个VO对象
|
||||
*
|
||||
* @param id 主键ID
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoById(Serializable id) {
|
||||
return selectVoById(id, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询单个VO对象并将其转换为指定的VO类
|
||||
*
|
||||
* @param id 主键ID
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的单个VO对象,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> C selectVoById(Serializable id, Class<C> voClass) {
|
||||
T obj = this.selectById(id);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
return MapstructUtils.convert(obj, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID集合批量查询VO对象列表
|
||||
*
|
||||
* @param idList 主键ID集合
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoByIds(Collection<? extends Serializable> idList) {
|
||||
return selectVoByIds(idList, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID集合批量查询实体对象列表,并将其转换为指定的VO对象列表
|
||||
*
|
||||
* @param idList 主键ID集合
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> List<C> selectVoByIds(Collection<? extends Serializable> idList, Class<C> voClass) {
|
||||
List<T> list = this.selectByIds(idList);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询条件Map查询VO对象列表
|
||||
*
|
||||
* @param map 查询条件Map
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoByMap(Map<String, Object> map) {
|
||||
return selectVoByMap(map, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询条件Map查询实体对象列表,并将其转换为指定的VO对象列表
|
||||
*
|
||||
* @param map 查询条件Map
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> List<C> selectVoByMap(Map<String, Object> map, Class<C> voClass) {
|
||||
List<T> list = this.selectByMap(map);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个VO对象
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoOne(Wrapper<T> wrapper) {
|
||||
return selectVoOne(wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个VO对象,并根据需要决定是否抛出异常
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param throwEx 是否抛出异常的标志
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoOne(Wrapper<T> wrapper, boolean throwEx) {
|
||||
return selectVoOne(wrapper, this.currentVoClass(), throwEx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个VO对象,并指定返回的VO对象的类型
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 返回的VO对象的Class对象
|
||||
* @param <C> 返回的VO对象的类型
|
||||
* @return 查询到的单个VO对象,经过类型转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) {
|
||||
return selectVoOne(wrapper, voClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个实体对象,并将其转换为指定的VO对象
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param throwEx 是否抛出异常的标志
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的单个VO对象,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass, boolean throwEx) {
|
||||
T obj = this.selectOne(wrapper, throwEx);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return null;
|
||||
}
|
||||
return MapstructUtils.convert(obj, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有VO对象列表
|
||||
*
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoList() {
|
||||
return selectVoList(new QueryWrapper<>(), this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询VO对象列表
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoList(Wrapper<T> wrapper) {
|
||||
return selectVoList(wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询实体对象列表,并将其转换为指定的VO对象列表
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @return 查询到的VO对象列表,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> List<C> selectVoList(Wrapper<T> wrapper, Class<C> voClass) {
|
||||
List<T> list = this.selectList(wrapper);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return MapstructUtils.convert(list, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件分页查询VO对象列表
|
||||
*
|
||||
* @param page 分页信息
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @return 查询到的VO对象分页列表
|
||||
*/
|
||||
default <P extends IPage<V>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper) {
|
||||
return selectVoPage(page, wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件分页查询实体对象列表,并将其转换为指定的VO对象分页列表
|
||||
*
|
||||
* @param page 分页信息
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
* @param <C> VO类的类型
|
||||
* @param <P> VO对象分页列表的类型
|
||||
* @return 查询到的VO对象分页列表,经过转换为指定的VO类后返回
|
||||
*/
|
||||
default <C, P extends IPage<C>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper, Class<C> voClass) {
|
||||
// 根据条件分页查询实体对象列表
|
||||
List<T> list = this.selectList(page, wrapper);
|
||||
// 创建一个新的VO对象分页列表,并设置分页信息
|
||||
IPage<C> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return (P) voPage;
|
||||
}
|
||||
voPage.setRecords(MapstructUtils.convert(list, voClass));
|
||||
return (P) voPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询符合条件的对象,并将其转换为指定类型的对象列表
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param mapper 转换函数,用于将查询到的对象转换为指定类型的对象
|
||||
* @param <C> 要转换的对象的类型
|
||||
* @return 查询到的符合条件的对象列表,经过转换为指定类型的对象后返回
|
||||
*/
|
||||
default <C> List<C> selectObjs(Wrapper<T> wrapper, Function<? super Object, C> mapper) {
|
||||
return StreamUtils.toList(this.selectObjs(wrapper), mapper);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
package com.fuyuanshen.common.mybatis.core.page;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import com.fuyuanshen.common.core.exception.ServiceException;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
import com.fuyuanshen.common.core.utils.sql.SqlUtil;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页查询实体类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
public class PageQuery implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 分页大小
|
||||
*/
|
||||
private Integer pageSize;
|
||||
|
||||
/**
|
||||
* 当前页数
|
||||
*/
|
||||
private Integer pageNum;
|
||||
|
||||
/**
|
||||
* 排序列
|
||||
*/
|
||||
private String orderByColumn;
|
||||
|
||||
/**
|
||||
* 排序的方向desc或者asc
|
||||
*/
|
||||
private String isAsc;
|
||||
|
||||
/**
|
||||
* 当前记录起始索引 默认值
|
||||
*/
|
||||
public static final int DEFAULT_PAGE_NUM = 1;
|
||||
|
||||
/**
|
||||
* 每页显示记录数 默认值 默认查全部
|
||||
*/
|
||||
public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* 构建分页对象
|
||||
*/
|
||||
public <T> Page<T> build() {
|
||||
Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
|
||||
Integer pageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
|
||||
if (pageNum <= 0) {
|
||||
pageNum = DEFAULT_PAGE_NUM;
|
||||
}
|
||||
Page<T> page = new Page<>(pageNum, pageSize);
|
||||
List<OrderItem> orderItems = buildOrderItem();
|
||||
if (CollUtil.isNotEmpty(orderItems)) {
|
||||
page.addOrder(orderItems);
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建排序
|
||||
*
|
||||
* 支持的用法如下:
|
||||
* {isAsc:"asc",orderByColumn:"id"} order by id asc
|
||||
* {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
|
||||
* {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc
|
||||
* {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc
|
||||
*/
|
||||
private List<OrderItem> buildOrderItem() {
|
||||
if (StringUtils.isBlank(orderByColumn) || StringUtils.isBlank(isAsc)) {
|
||||
return null;
|
||||
}
|
||||
String orderBy = SqlUtil.escapeOrderBySql(orderByColumn);
|
||||
orderBy = StringUtils.toUnderScoreCase(orderBy);
|
||||
|
||||
// 兼容前端排序类型
|
||||
isAsc = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"});
|
||||
|
||||
String[] orderByArr = orderBy.split(StringUtils.SEPARATOR);
|
||||
String[] isAscArr = isAsc.split(StringUtils.SEPARATOR);
|
||||
if (isAscArr.length != 1 && isAscArr.length != orderByArr.length) {
|
||||
throw new ServiceException("排序参数有误");
|
||||
}
|
||||
|
||||
List<OrderItem> list = new ArrayList<>();
|
||||
// 每个字段各自排序
|
||||
for (int i = 0; i < orderByArr.length; i++) {
|
||||
String orderByStr = orderByArr[i];
|
||||
String isAscStr = isAscArr.length == 1 ? isAscArr[0] : isAscArr[i];
|
||||
if ("asc".equals(isAscStr)) {
|
||||
list.add(OrderItem.asc(orderByStr));
|
||||
} else if ("desc".equals(isAscStr)) {
|
||||
list.add(OrderItem.desc(orderByStr));
|
||||
} else {
|
||||
throw new ServiceException("排序参数有误");
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Integer getFirstNum() {
|
||||
return (pageNum - 1) * pageSize;
|
||||
}
|
||||
|
||||
public PageQuery(Integer pageSize, Integer pageNum) {
|
||||
this.pageSize = pageSize;
|
||||
this.pageNum = pageNum;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package com.fuyuanshen.common.mybatis.core.page;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 表格分页数据对象
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class TableDataInfo<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private long total;
|
||||
|
||||
/**
|
||||
* 列表数据
|
||||
*/
|
||||
private List<T> rows;
|
||||
|
||||
/**
|
||||
* 消息状态码
|
||||
*/
|
||||
private int code;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*
|
||||
* @param list 列表数据
|
||||
* @param total 总记录数
|
||||
*/
|
||||
public TableDataInfo(List<T> list, long total) {
|
||||
this.rows = list;
|
||||
this.total = total;
|
||||
this.code = HttpStatus.HTTP_OK;
|
||||
this.msg = "查询成功";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分页对象构建表格分页数据对象
|
||||
*/
|
||||
public static <T> TableDataInfo<T> build(IPage<T> page) {
|
||||
TableDataInfo<T> rspData = new TableDataInfo<>();
|
||||
rspData.setCode(HttpStatus.HTTP_OK);
|
||||
rspData.setMsg("查询成功");
|
||||
rspData.setRows(page.getRecords());
|
||||
rspData.setTotal(page.getTotal());
|
||||
return rspData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据列表构建表格分页数据对象
|
||||
*/
|
||||
public static <T> TableDataInfo<T> build(List<T> list) {
|
||||
TableDataInfo<T> rspData = new TableDataInfo<>();
|
||||
rspData.setCode(HttpStatus.HTTP_OK);
|
||||
rspData.setMsg("查询成功");
|
||||
rspData.setRows(list);
|
||||
rspData.setTotal(list.size());
|
||||
return rspData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建表格分页数据对象
|
||||
*/
|
||||
public static <T> TableDataInfo<T> build() {
|
||||
TableDataInfo<T> rspData = new TableDataInfo<>();
|
||||
rspData.setCode(HttpStatus.HTTP_OK);
|
||||
rspData.setMsg("查询成功");
|
||||
return rspData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据原始数据列表和分页参数,构建表格分页数据对象(用于假分页)
|
||||
*
|
||||
* @param list 原始数据列表(全部数据)
|
||||
* @param page 分页参数对象(包含当前页码、每页大小等)
|
||||
* @return 构造好的分页结果 TableDataInfo<T>
|
||||
*/
|
||||
public static <T> TableDataInfo<T> build(List<T> list, IPage<T> page) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return TableDataInfo.build();
|
||||
}
|
||||
List<T> pageList = CollUtil.page((int) page.getCurrent() - 1, (int) page.getSize(), list);
|
||||
return new TableDataInfo<>(pageList, list.size());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.fuyuanshen.common.mybatis.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* 数据库类型
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DataBaseType {
|
||||
|
||||
/**
|
||||
* MySQL
|
||||
*/
|
||||
MY_SQL("MySQL"),
|
||||
|
||||
/**
|
||||
* Oracle
|
||||
*/
|
||||
ORACLE("Oracle"),
|
||||
|
||||
/**
|
||||
* PostgreSQL
|
||||
*/
|
||||
POSTGRE_SQL("PostgreSQL"),
|
||||
|
||||
/**
|
||||
* SQL Server
|
||||
*/
|
||||
SQL_SERVER("Microsoft SQL Server");
|
||||
|
||||
/**
|
||||
* 数据库类型
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
/**
|
||||
* 根据数据库产品名称查找对应的数据库类型
|
||||
*
|
||||
* @param databaseProductName 数据库产品名称
|
||||
* @return 对应的数据库类型枚举值,如果未找到则返回 null
|
||||
*/
|
||||
public static DataBaseType find(String databaseProductName) {
|
||||
if (StringUtils.isBlank(databaseProductName)) {
|
||||
return null;
|
||||
}
|
||||
for (DataBaseType type : values()) {
|
||||
if (type.getType().equals(databaseProductName)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.fuyuanshen.common.mybatis.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import com.fuyuanshen.common.core.domain.model.LoginUser;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
import com.fuyuanshen.common.mybatis.helper.DataPermissionHelper;
|
||||
|
||||
/**
|
||||
* 数据权限类型枚举
|
||||
* <p>
|
||||
* 支持使用 SpEL 模板表达式定义 SQL 查询条件
|
||||
* 内置数据:
|
||||
* - {@code user}: 当前登录用户信息,参考 {@link LoginUser}
|
||||
* 内置服务:
|
||||
* - {@code sdss}: 系统数据权限服务,参考 ISysDataScopeService
|
||||
* 如需扩展数据,可以通过 {@link DataPermissionHelper} 进行操作
|
||||
* 如需扩展服务,可以通过 ISysDataScopeService 自行编写
|
||||
* </p>
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DataScopeType {
|
||||
|
||||
/**
|
||||
* 全部数据权限
|
||||
*/
|
||||
ALL("1", "", ""),
|
||||
|
||||
/**
|
||||
* 自定数据权限
|
||||
*/
|
||||
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", " 1 = 0 "),
|
||||
|
||||
/**
|
||||
* 部门数据权限
|
||||
*/
|
||||
DEPT("3", " #{#deptName} = #{#user.deptId} ", " 1 = 0 "),
|
||||
|
||||
/**
|
||||
* 部门及以下数据权限
|
||||
*/
|
||||
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", " 1 = 0 "),
|
||||
|
||||
/**
|
||||
* 仅本人数据权限
|
||||
*/
|
||||
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 "),
|
||||
|
||||
/**
|
||||
* 部门及以下或本人数据权限
|
||||
*/
|
||||
DEPT_AND_CHILD_OR_SELF("6", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} ) OR #{#userName} = #{#user.userId} ", " 1 = 0 ");
|
||||
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* SpEL 模板表达式,用于构建 SQL 查询条件
|
||||
*/
|
||||
private final String sqlTemplate;
|
||||
|
||||
/**
|
||||
* 如果不满足 {@code sqlTemplate} 的条件,则使用此默认 SQL 表达式
|
||||
*/
|
||||
private final String elseSql;
|
||||
|
||||
/**
|
||||
* 根据枚举代码查找对应的枚举值
|
||||
*
|
||||
* @param code 枚举代码
|
||||
* @return 对应的枚举值,如果未找到则返回 null
|
||||
*/
|
||||
public static DataScopeType findCode(String code) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return null;
|
||||
}
|
||||
for (DataScopeType type : values()) {
|
||||
if (type.getCode().equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package com.fuyuanshen.common.mybatis.handler;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import com.fuyuanshen.common.core.domain.model.LoginUser;
|
||||
import com.fuyuanshen.common.core.exception.ServiceException;
|
||||
import com.fuyuanshen.common.core.utils.ObjectUtils;
|
||||
import com.fuyuanshen.common.mybatis.core.domain.BaseEntity;
|
||||
import com.fuyuanshen.common.satoken.utils.LoginHelper;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MP注入处理器
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2021/4/25
|
||||
*/
|
||||
@Slf4j
|
||||
public class InjectionMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 插入填充方法,用于在插入数据时自动填充实体对象中的创建时间、更新时间、创建人、更新人等信息
|
||||
*
|
||||
* @param metaObject 元对象,用于获取原始对象并进行填充
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
try {
|
||||
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
|
||||
// 获取当前时间作为创建时间和更新时间,如果创建时间不为空,则使用创建时间,否则使用当前时间
|
||||
Date current = ObjectUtils.notNull(baseEntity.getCreateTime(), new Date());
|
||||
baseEntity.setCreateTime(current);
|
||||
baseEntity.setUpdateTime(current);
|
||||
|
||||
// 如果创建人为空,则填充当前登录用户的信息
|
||||
if (ObjectUtil.isNull(baseEntity.getCreateBy())) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (ObjectUtil.isNotNull(loginUser)) {
|
||||
Long userId = loginUser.getUserId();
|
||||
// 填充创建人、更新人和创建部门信息
|
||||
baseEntity.setCreateBy(userId);
|
||||
baseEntity.setUpdateBy(userId);
|
||||
baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Date date = new Date();
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, date);
|
||||
this.strictInsertFill(metaObject, "updateTime", Date.class, date);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新填充方法,用于在更新数据时自动填充实体对象中的更新时间和更新人信息
|
||||
*
|
||||
* @param metaObject 元对象,用于获取原始对象并进行填充
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
try {
|
||||
if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity baseEntity) {
|
||||
// 获取当前时间作为更新时间,无论原始对象中的更新时间是否为空都填充
|
||||
Date current = new Date();
|
||||
baseEntity.setUpdateTime(current);
|
||||
|
||||
// 获取当前登录用户的ID,并填充更新人信息
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (ObjectUtil.isNotNull(userId)) {
|
||||
baseEntity.setUpdateBy(userId);
|
||||
}
|
||||
} else {
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*
|
||||
* @return 当前登录用户的信息,如果用户未登录则返回 null
|
||||
*/
|
||||
private LoginUser getLoginUser() {
|
||||
LoginUser loginUser;
|
||||
try {
|
||||
loginUser = LoginHelper.getLoginUser();
|
||||
} catch (Exception e) {
|
||||
log.warn("自动注入警告 => 用户未登录");
|
||||
return null;
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package com.fuyuanshen.common.mybatis.handler;
|
||||
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.fuyuanshen.common.core.domain.R;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
import org.mybatis.spring.MyBatisSystemException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* Mybatis异常处理器
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class MybatisExceptionHandler {
|
||||
|
||||
/**
|
||||
* 主键或UNIQUE索引,数据重复异常
|
||||
*/
|
||||
@ExceptionHandler(DuplicateKeyException.class)
|
||||
public R<Void> handleDuplicateKeyException(DuplicateKeyException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求地址'{}',数据库中已存在记录'{}'", requestURI, e.getMessage());
|
||||
return R.fail(HttpStatus.HTTP_CONFLICT, "数据库中已存在该记录,请联系管理员确认");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mybatis系统异常 通用处理
|
||||
*/
|
||||
@ExceptionHandler(MyBatisSystemException.class)
|
||||
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
String message = e.getMessage();
|
||||
if (StringUtils.contains(message, "CannotFindDataSourceException")) {
|
||||
log.error("请求地址'{}', 未找到数据源", requestURI);
|
||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
|
||||
}
|
||||
log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
|
||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, message);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,357 @@
|
||||
package com.fuyuanshen.common.mybatis.handler;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import org.apache.ibatis.io.Resources;
|
||||
import com.fuyuanshen.common.core.domain.dto.RoleDTO;
|
||||
import com.fuyuanshen.common.core.domain.model.LoginUser;
|
||||
import com.fuyuanshen.common.core.exception.ServiceException;
|
||||
import com.fuyuanshen.common.core.utils.SpringUtils;
|
||||
import com.fuyuanshen.common.core.utils.StreamUtils;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
import com.fuyuanshen.common.mybatis.annotation.DataColumn;
|
||||
import com.fuyuanshen.common.mybatis.annotation.DataPermission;
|
||||
import com.fuyuanshen.common.mybatis.enums.DataScopeType;
|
||||
import com.fuyuanshen.common.mybatis.helper.DataPermissionHelper;
|
||||
import com.fuyuanshen.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.core.type.ClassMetadata;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.expression.*;
|
||||
import org.springframework.expression.common.TemplateParserContext;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 数据权限过滤
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PlusDataPermissionHandler {
|
||||
|
||||
/**
|
||||
* 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行)
|
||||
*/
|
||||
private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* spel 解析器
|
||||
*/
|
||||
private final ExpressionParser parser = new SpelExpressionParser();
|
||||
private final ParserContext parserContext = new TemplateParserContext();
|
||||
/**
|
||||
* bean解析器 用于处理 spel 表达式中对 bean 的调用
|
||||
*/
|
||||
private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
|
||||
|
||||
/**
|
||||
* 构造方法,扫描指定包下的 Mapper 类并初始化缓存
|
||||
*
|
||||
* @param mapperPackage Mapper 类所在的包路径
|
||||
*/
|
||||
public PlusDataPermissionHandler(String mapperPackage) {
|
||||
scanMapperClasses(mapperPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据过滤条件的 SQL 片段
|
||||
*
|
||||
* @param where 原始的查询条件表达式
|
||||
* @param mappedStatementId Mapper 方法的 ID
|
||||
* @param isSelect 是否为查询语句
|
||||
* @return 数据过滤条件的 SQL 片段
|
||||
*/
|
||||
public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
|
||||
try {
|
||||
// 获取数据权限配置
|
||||
DataPermission dataPermission = getDataPermission(mappedStatementId);
|
||||
// 获取当前登录用户信息
|
||||
LoginUser currentUser = DataPermissionHelper.getVariable("user");
|
||||
if (ObjectUtil.isNull(currentUser)) {
|
||||
currentUser = LoginHelper.getLoginUser();
|
||||
DataPermissionHelper.setVariable("user", currentUser);
|
||||
}
|
||||
// 如果是超级管理员或租户管理员,则不过滤数据
|
||||
if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
|
||||
return where;
|
||||
}
|
||||
// 构造数据过滤条件的 SQL 片段
|
||||
String dataFilterSql = buildDataFilter(dataPermission, isSelect);
|
||||
if (StringUtils.isBlank(dataFilterSql)) {
|
||||
return where;
|
||||
}
|
||||
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
|
||||
// 数据权限使用单独的括号 防止与其他条件冲突
|
||||
ParenthesedExpressionList<Expression> parenthesis = new ParenthesedExpressionList<>(expression);
|
||||
if (ObjectUtil.isNotNull(where)) {
|
||||
return new AndExpression(where, parenthesis);
|
||||
} else {
|
||||
return parenthesis;
|
||||
}
|
||||
} catch (JSQLParserException e) {
|
||||
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
|
||||
} finally {
|
||||
DataPermissionHelper.removePermission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建数据过滤条件的 SQL 语句
|
||||
*
|
||||
* @param dataPermission 数据权限注解
|
||||
* @param isSelect 标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
|
||||
* @return 构建的数据过滤条件的 SQL 语句
|
||||
* @throws ServiceException 如果角色的数据范围异常或者 key 与 value 的长度不匹配,则抛出 ServiceException 异常
|
||||
*/
|
||||
private String buildDataFilter(DataPermission dataPermission, boolean isSelect) {
|
||||
// 更新或删除需满足所有条件
|
||||
String joinStr = isSelect ? " OR " : " AND ";
|
||||
if (StringUtils.isNotBlank(dataPermission.joinStr())) {
|
||||
joinStr = " " + dataPermission.joinStr() + " ";
|
||||
}
|
||||
LoginUser user = DataPermissionHelper.getVariable("user");
|
||||
Object defaultValue = "-1";
|
||||
NullSafeStandardEvaluationContext context = new NullSafeStandardEvaluationContext(defaultValue);
|
||||
context.addPropertyAccessor(new NullSafePropertyAccessor(context.getPropertyAccessors().get(0), defaultValue));
|
||||
context.setBeanResolver(beanResolver);
|
||||
DataPermissionHelper.getContext().forEach(context::setVariable);
|
||||
Set<String> conditions = new HashSet<>();
|
||||
// 优先设置变量
|
||||
List<String> keys = new ArrayList<>();
|
||||
Map<DataColumn, Boolean> ignoreMap = new HashMap<>();
|
||||
for (DataColumn dataColumn : dataPermission.value()) {
|
||||
if (dataColumn.key().length != dataColumn.value().length) {
|
||||
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
|
||||
}
|
||||
// 包含权限标识符 这直接跳过
|
||||
if (StringUtils.isNotBlank(dataColumn.permission()) &&
|
||||
CollUtil.contains(user.getMenuPermission(), dataColumn.permission())
|
||||
) {
|
||||
ignoreMap.put(dataColumn, Boolean.TRUE);
|
||||
continue;
|
||||
}
|
||||
// 设置注解变量 key 为表达式变量 value 为变量值
|
||||
for (int i = 0; i < dataColumn.key().length; i++) {
|
||||
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
|
||||
}
|
||||
keys.addAll(Arrays.stream(dataColumn.key()).map(key -> "#" + key).toList());
|
||||
}
|
||||
|
||||
for (RoleDTO role : user.getRoles()) {
|
||||
user.setRoleId(role.getRoleId());
|
||||
// 获取角色权限泛型
|
||||
DataScopeType type = DataScopeType.findCode(role.getDataScope());
|
||||
if (ObjectUtil.isNull(type)) {
|
||||
throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
|
||||
}
|
||||
// 全部数据权限直接返回
|
||||
if (type == DataScopeType.ALL) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
boolean isSuccess = false;
|
||||
for (DataColumn dataColumn : dataPermission.value()) {
|
||||
// 包含权限标识符 这直接跳过
|
||||
if (ignoreMap.containsKey(dataColumn)) {
|
||||
// 修复多角色与权限标识符共用问题 https://gitee.com/dromara/fys-Vue-Plus/issues/IB4CS4
|
||||
conditions.add(joinStr + " 1 = 1 ");
|
||||
isSuccess = true;
|
||||
continue;
|
||||
}
|
||||
// 不包含 key 变量 则不处理
|
||||
if (!StringUtils.containsAny(type.getSqlTemplate(), keys.toArray(String[]::new))) {
|
||||
continue;
|
||||
}
|
||||
// 当前注解不满足模板 不处理
|
||||
if (!StringUtils.containsAny(type.getSqlTemplate(), dataColumn.key())) {
|
||||
continue;
|
||||
}
|
||||
// 忽略数据权限 防止spel表达式内有其他sql查询导致死循环调用
|
||||
String sql = DataPermissionHelper.ignore(() ->
|
||||
parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class)
|
||||
);
|
||||
// 解析sql模板并填充
|
||||
conditions.add(joinStr + sql);
|
||||
isSuccess = true;
|
||||
}
|
||||
// 未处理成功则填充兜底方案
|
||||
if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
|
||||
conditions.add(joinStr + type.getElseSql());
|
||||
}
|
||||
}
|
||||
|
||||
if (CollUtil.isNotEmpty(conditions)) {
|
||||
String sql = StreamUtils.join(conditions, Function.identity(), "");
|
||||
return sql.substring(joinStr.length());
|
||||
}
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描指定包下的 Mapper 类,并查找其中带有特定注解的方法或类
|
||||
*
|
||||
* @param mapperPackage Mapper 类所在的包路径
|
||||
*/
|
||||
private void scanMapperClasses(String mapperPackage) {
|
||||
// 创建资源解析器和元数据读取工厂
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
|
||||
// 将 Mapper 包路径按分隔符拆分为数组
|
||||
String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
|
||||
String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
|
||||
try {
|
||||
for (String packagePattern : packagePatternArray) {
|
||||
// 将包路径转换为资源路径
|
||||
String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
|
||||
// 获取指定路径下的所有 .class 文件资源
|
||||
Resource[] resources = resolver.getResources(classpath + path + "/*.class");
|
||||
for (Resource resource : resources) {
|
||||
// 获取资源的类元数据
|
||||
ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
|
||||
// 获取资源对应的类对象
|
||||
Class<?> clazz = Resources.classForName(classMetadata.getClassName());
|
||||
// 查找类中的特定注解
|
||||
findAnnotation(clazz);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("初始化数据安全缓存时出错:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定的类中查找特定的注解 DataPermission,并将带有这个注解的方法或类存储到 dataPermissionCacheMap 中
|
||||
*
|
||||
* @param clazz 要查找的类
|
||||
*/
|
||||
private void findAnnotation(Class<?> clazz) {
|
||||
DataPermission dataPermission;
|
||||
for (Method method : clazz.getMethods()) {
|
||||
if (method.isDefault() || method.isVarArgs()) {
|
||||
continue;
|
||||
}
|
||||
String mappedStatementId = clazz.getName() + "." + method.getName();
|
||||
if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
|
||||
dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
|
||||
dataPermissionCacheMap.put(mappedStatementId, dataPermission);
|
||||
}
|
||||
}
|
||||
if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
|
||||
dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
|
||||
dataPermissionCacheMap.put(clazz.getName(), dataPermission);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象
|
||||
*
|
||||
* @param mapperId 映射语句 ID
|
||||
* @return DataPermission 注解对象,如果不存在则返回 null
|
||||
*/
|
||||
public DataPermission getDataPermission(String mapperId) {
|
||||
// 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象
|
||||
if (DataPermissionHelper.getPermission() != null) {
|
||||
return DataPermissionHelper.getPermission();
|
||||
}
|
||||
// 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象
|
||||
if (dataPermissionCacheMap.containsKey(mapperId)) {
|
||||
return dataPermissionCacheMap.get(mapperId);
|
||||
}
|
||||
// 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象,则尝试使用类名作为键查找
|
||||
String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
|
||||
if (dataPermissionCacheMap.containsKey(clazzName)) {
|
||||
return dataPermissionCacheMap.get(clazzName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象
|
||||
*
|
||||
* @param mapperId 映射语句 ID
|
||||
* @return 如果找到对应的 DataPermission 注解对象,则返回 false;否则返回 true
|
||||
*/
|
||||
public boolean invalid(String mapperId) {
|
||||
return getDataPermission(mapperId) == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有null变量找不到的变量返回默认值
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
private static class NullSafeStandardEvaluationContext extends StandardEvaluationContext {
|
||||
|
||||
private final Object defaultValue;
|
||||
|
||||
@Override
|
||||
public Object lookupVariable(String name) {
|
||||
Object obj = super.lookupVariable(name);
|
||||
// 如果读取到的值是 null,则返回默认值
|
||||
if (obj == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 对所有null变量找不到的变量返回默认值 委托模式 将不需要处理的方法委托给原处理器
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
private static class NullSafePropertyAccessor implements PropertyAccessor {
|
||||
|
||||
private final PropertyAccessor delegate;
|
||||
private final Object defaultValue;
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSpecificTargetClasses() {
|
||||
return delegate.getSpecificTargetClasses();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
return delegate.canRead(context, target, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
TypedValue value = delegate.read(context, target, name);
|
||||
// 如果读取到的值是 null,则返回默认值
|
||||
if (value.getValue() == null) {
|
||||
return new TypedValue(defaultValue);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
|
||||
return delegate.canWrite(context, target, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
|
||||
delegate.write(context, target, name, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.fuyuanshen.common.mybatis.handler;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import com.baomidou.mybatisplus.core.handlers.PostInitTableInfoHandler;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import com.fuyuanshen.common.core.utils.SpringUtils;
|
||||
import com.fuyuanshen.common.core.utils.reflect.ReflectUtils;
|
||||
|
||||
/**
|
||||
* 修改表信息初始化方式
|
||||
* 目前用于全局修改是否使用逻辑删除
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class PlusPostInitTableInfoHandler implements PostInitTableInfoHandler {
|
||||
|
||||
@Override
|
||||
public void postTableInfo(TableInfo tableInfo, Configuration configuration) {
|
||||
String flag = SpringUtils.getProperty("mybatis-plus.enableLogicDelete", "true");
|
||||
// 只有关闭时 统一设置false 为true时mp自动判断不处理
|
||||
if (!Convert.toBool(flag)) {
|
||||
ReflectUtils.setFieldValue(tableInfo, "withLogicDelete", false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.fuyuanshen.common.mybatis.helper;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import com.fuyuanshen.common.core.exception.ServiceException;
|
||||
import com.fuyuanshen.common.core.utils.SpringUtils;
|
||||
import com.fuyuanshen.common.mybatis.enums.DataBaseType;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据库助手
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class DataBaseHelper {
|
||||
|
||||
private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
|
||||
|
||||
/**
|
||||
* 获取当前数据库类型
|
||||
*/
|
||||
public static DataBaseType getDataBaseType() {
|
||||
DataSource dataSource = DS.determineDataSource();
|
||||
try (Connection conn = dataSource.getConnection()) {
|
||||
DatabaseMetaData metaData = conn.getMetaData();
|
||||
String databaseProductName = metaData.getDatabaseProductName();
|
||||
return DataBaseType.find(databaseProductName);
|
||||
} catch (SQLException e) {
|
||||
throw new ServiceException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isMySql() {
|
||||
return DataBaseType.MY_SQL == getDataBaseType();
|
||||
}
|
||||
|
||||
public static boolean isOracle() {
|
||||
return DataBaseType.ORACLE == getDataBaseType();
|
||||
}
|
||||
|
||||
public static boolean isPostgerSql() {
|
||||
return DataBaseType.POSTGRE_SQL == getDataBaseType();
|
||||
}
|
||||
|
||||
public static boolean isSqlServer() {
|
||||
return DataBaseType.SQL_SERVER == getDataBaseType();
|
||||
}
|
||||
|
||||
public static String findInSet(Object var1, String var2) {
|
||||
DataBaseType dataBasyType = getDataBaseType();
|
||||
String var = Convert.toStr(var1);
|
||||
if (dataBasyType == DataBaseType.SQL_SERVER) {
|
||||
// charindex(',100,' , ',0,100,101,') <> 0
|
||||
return "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
|
||||
} else if (dataBasyType == DataBaseType.POSTGRE_SQL) {
|
||||
// (select strpos(',0,100,101,' , ',100,')) <> 0
|
||||
return "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
|
||||
} else if (dataBasyType == DataBaseType.ORACLE) {
|
||||
// instr(',0,100,101,' , ',100,') <> 0
|
||||
return "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
|
||||
}
|
||||
// find_in_set(100 , '0,100,101')
|
||||
return "find_in_set('%s' , %s) <> 0".formatted(var, var2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前加载的数据库名
|
||||
*/
|
||||
public static List<String> getDataSourceNameList() {
|
||||
return new ArrayList<>(DS.getDataSources().keySet());
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
package com.fuyuanshen.common.mybatis.helper;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaStorage;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
|
||||
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import com.fuyuanshen.common.core.utils.reflect.ReflectUtils;
|
||||
import com.fuyuanshen.common.mybatis.annotation.DataPermission;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 数据权限助手
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("unchecked cast")
|
||||
public class DataPermissionHelper {
|
||||
|
||||
private static final String DATA_PERMISSION_KEY = "data:permission";
|
||||
|
||||
private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
|
||||
|
||||
private static final ThreadLocal<DataPermission> PERMISSION_CACHE = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 获取当前执行mapper权限注解
|
||||
*
|
||||
* @return 返回当前执行mapper权限注解
|
||||
*/
|
||||
public static DataPermission getPermission() {
|
||||
return PERMISSION_CACHE.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前执行mapper权限注解
|
||||
*
|
||||
* @param dataPermission 数据权限注解
|
||||
*/
|
||||
public static void setPermission(DataPermission dataPermission) {
|
||||
PERMISSION_CACHE.set(dataPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前执行mapper权限注解
|
||||
*/
|
||||
public static void removePermission() {
|
||||
PERMISSION_CACHE.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从上下文中获取指定键的变量值,并将其转换为指定的类型
|
||||
*
|
||||
* @param key 变量的键
|
||||
* @param <T> 变量值的类型
|
||||
* @return 指定键的变量值,如果不存在则返回 null
|
||||
*/
|
||||
public static <T> T getVariable(String key) {
|
||||
Map<String, Object> context = getContext();
|
||||
return (T) context.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上下文中设置指定键的变量值
|
||||
*
|
||||
* @param key 要设置的变量的键
|
||||
* @param value 要设置的变量值
|
||||
*/
|
||||
public static void setVariable(String key, Object value) {
|
||||
Map<String, Object> context = getContext();
|
||||
context.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据权限上下文
|
||||
*
|
||||
* @return 存储在SaStorage中的Map对象,用于存储数据权限相关的上下文信息
|
||||
* @throws NullPointerException 如果数据权限上下文类型异常,则抛出NullPointerException
|
||||
*/
|
||||
public static Map<String, Object> getContext() {
|
||||
SaStorage saStorage = SaHolder.getStorage();
|
||||
Object attribute = saStorage.get(DATA_PERMISSION_KEY);
|
||||
if (ObjectUtil.isNull(attribute)) {
|
||||
saStorage.set(DATA_PERMISSION_KEY, new HashMap<>());
|
||||
attribute = saStorage.get(DATA_PERMISSION_KEY);
|
||||
}
|
||||
if (attribute instanceof Map map) {
|
||||
return map;
|
||||
}
|
||||
throw new NullPointerException("data permission context type exception");
|
||||
}
|
||||
|
||||
private static IgnoreStrategy getIgnoreStrategy() {
|
||||
Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
|
||||
if (ignoreStrategyLocal instanceof ThreadLocal<?> IGNORE_STRATEGY_LOCAL) {
|
||||
if (IGNORE_STRATEGY_LOCAL.get() instanceof IgnoreStrategy ignoreStrategy) {
|
||||
return ignoreStrategy;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
|
||||
*/
|
||||
public static void enableIgnore() {
|
||||
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
|
||||
if (ObjectUtil.isNull(ignoreStrategy)) {
|
||||
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
|
||||
} else {
|
||||
ignoreStrategy.setDataPermission(true);
|
||||
}
|
||||
Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
|
||||
reentrantStack.push(reentrantStack.size() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭忽略数据权限
|
||||
*/
|
||||
public static void disableIgnore() {
|
||||
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
|
||||
if (ObjectUtil.isNotNull(ignoreStrategy)) {
|
||||
boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
|
||||
&& !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
|
||||
&& !Boolean.TRUE.equals(ignoreStrategy.getIllegalSql())
|
||||
&& !Boolean.TRUE.equals(ignoreStrategy.getTenantLine())
|
||||
&& CollectionUtil.isEmpty(ignoreStrategy.getOthers());
|
||||
Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
|
||||
boolean empty = reentrantStack.isEmpty() || reentrantStack.pop() == 1;
|
||||
if (noOtherIgnoreStrategy && empty) {
|
||||
InterceptorIgnoreHelper.clearIgnoreStrategy();
|
||||
} else if (empty) {
|
||||
ignoreStrategy.setDataPermission(false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在忽略数据权限中执行
|
||||
*
|
||||
* @param handle 处理执行方法
|
||||
*/
|
||||
public static void ignore(Runnable handle) {
|
||||
enableIgnore();
|
||||
try {
|
||||
handle.run();
|
||||
} finally {
|
||||
disableIgnore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在忽略数据权限中执行
|
||||
*
|
||||
* @param handle 处理执行方法
|
||||
*/
|
||||
public static <T> T ignore(Supplier<T> handle) {
|
||||
enableIgnore();
|
||||
try {
|
||||
return handle.get();
|
||||
} finally {
|
||||
disableIgnore();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
package com.fuyuanshen.common.mybatis.interceptor;
|
||||
|
||||
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.delete.Delete;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import net.sf.jsqlparser.statement.select.SetOperationList;
|
||||
import net.sf.jsqlparser.statement.update.Update;
|
||||
import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlCommandType;
|
||||
import org.apache.ibatis.session.ResultHandler;
|
||||
import org.apache.ibatis.session.RowBounds;
|
||||
import com.fuyuanshen.common.mybatis.handler.PlusDataPermissionHandler;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据权限拦截器
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 3.5.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
|
||||
|
||||
private final PlusDataPermissionHandler dataPermissionHandler;
|
||||
|
||||
/**
|
||||
* 构造函数,初始化 PlusDataPermissionHandler 实例
|
||||
*
|
||||
* @param mapperPackage 扫描的映射器包
|
||||
*/
|
||||
public PlusDataPermissionInterceptor(String mapperPackage) {
|
||||
this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在执行查询之前,检查并处理数据权限相关逻辑
|
||||
*
|
||||
* @param executor MyBatis 执行器对象
|
||||
* @param ms 映射语句对象
|
||||
* @param parameter 方法参数
|
||||
* @param rowBounds 分页对象
|
||||
* @param resultHandler 结果处理器
|
||||
* @param boundSql 绑定的 SQL 对象
|
||||
* @throws SQLException 如果发生 SQL 异常
|
||||
*/
|
||||
@Override
|
||||
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
|
||||
// 检查是否需要忽略数据权限处理
|
||||
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
// 检查是否缺少有效的数据权限注解
|
||||
if (dataPermissionHandler.invalid(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
// 解析 sql 分配对应方法
|
||||
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
|
||||
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在准备 SQL 语句之前,检查并处理更新和删除操作的数据权限相关逻辑
|
||||
*
|
||||
* @param sh MyBatis StatementHandler 对象
|
||||
* @param connection 数据库连接对象
|
||||
* @param transactionTimeout 事务超时时间
|
||||
*/
|
||||
@Override
|
||||
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
|
||||
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
|
||||
MappedStatement ms = mpSh.mappedStatement();
|
||||
// 获取 SQL 命令类型(增、删、改、查)
|
||||
SqlCommandType sct = ms.getSqlCommandType();
|
||||
|
||||
// 只处理更新和删除操作的 SQL 语句
|
||||
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
|
||||
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
// 检查是否缺少有效的数据权限注解
|
||||
if (dataPermissionHandler.invalid(ms.getId())) {
|
||||
return;
|
||||
}
|
||||
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
|
||||
mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SELECT 查询语句中的 WHERE 条件
|
||||
*
|
||||
* @param select SELECT 查询对象
|
||||
* @param index 查询语句的索引
|
||||
* @param sql 查询语句
|
||||
* @param obj WHERE 条件参数
|
||||
*/
|
||||
@Override
|
||||
protected void processSelect(Select select, int index, String sql, Object obj) {
|
||||
if (select instanceof PlainSelect) {
|
||||
this.setWhere((PlainSelect) select, (String) obj);
|
||||
} else if (select instanceof SetOperationList setOperationList) {
|
||||
List<Select> selectBodyList = setOperationList.getSelects();
|
||||
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 UPDATE 语句中的 WHERE 条件
|
||||
*
|
||||
* @param update UPDATE 查询对象
|
||||
* @param index 查询语句的索引
|
||||
* @param sql 查询语句
|
||||
* @param obj WHERE 条件参数
|
||||
*/
|
||||
@Override
|
||||
protected void processUpdate(Update update, int index, String sql, Object obj) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
|
||||
if (null != sqlSegment) {
|
||||
update.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 DELETE 语句中的 WHERE 条件
|
||||
*
|
||||
* @param delete DELETE 查询对象
|
||||
* @param index 查询语句的索引
|
||||
* @param sql 查询语句
|
||||
* @param obj WHERE 条件参数
|
||||
*/
|
||||
@Override
|
||||
protected void processDelete(Delete delete, int index, String sql, Object obj) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
|
||||
if (null != sqlSegment) {
|
||||
delete.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 SELECT 语句的 WHERE 条件
|
||||
*
|
||||
* @param plainSelect SELECT 查询对象
|
||||
* @param mappedStatementId 映射语句的 ID
|
||||
*/
|
||||
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
|
||||
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
|
||||
if (null != sqlSegment) {
|
||||
plainSelect.setWhere(sqlSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建表达式,用于处理表的数据权限
|
||||
*
|
||||
* @param table 表对象
|
||||
* @param where WHERE 条件表达式
|
||||
* @param whereSegment WHERE 条件片段
|
||||
* @return 构建的表达式
|
||||
*/
|
||||
@Override
|
||||
public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
|
||||
// 只有新版数据权限处理器才会执行到这里
|
||||
final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
|
||||
return handler.getSqlSegment(table, where, whereSegment);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
com.fuyuanshen.common.mybatis.config.MybatisPlusConfig
|
@ -0,0 +1,33 @@
|
||||
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
mybatis-plus:
|
||||
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
|
||||
checkConfigLocation: false
|
||||
configuration:
|
||||
# 自动驼峰命名规则(camel case)映射
|
||||
mapUnderscoreToCamelCase: true
|
||||
# MyBatis 自动映射策略
|
||||
# NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射
|
||||
autoMappingBehavior: FULL
|
||||
# MyBatis 自动映射时未知列或未知属性处理策
|
||||
# NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息
|
||||
autoMappingUnknownColumnBehavior: NONE
|
||||
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||
global-config:
|
||||
# 是否打印 Logo banner
|
||||
banner: true
|
||||
dbConfig:
|
||||
# 主键类型
|
||||
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
|
||||
idType: ASSIGN_ID
|
||||
# 逻辑已删除值(可按需求随意修改)
|
||||
logicDeleteValue: 1
|
||||
# 逻辑未删除值
|
||||
logicNotDeleteValue: 0
|
||||
insertStrategy: NOT_NULL
|
||||
updateStrategy: NOT_NULL
|
||||
whereStrategy: NOT_NULL
|
@ -0,0 +1,20 @@
|
||||
# p6spy 性能分析插件配置文件
|
||||
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
|
||||
# 自定义日志打印
|
||||
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
|
||||
#日志输出到控制台
|
||||
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
|
||||
# 使用日志系统记录 sql
|
||||
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
|
||||
# 取消JDBC URL前缀
|
||||
useprefix=true
|
||||
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
|
||||
excludecategories=info,debug,result,commit,resultset
|
||||
# 日期格式
|
||||
dateformat=yyyy-MM-dd HH:mm:ss
|
||||
# SQL语句打印时间格式
|
||||
databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss
|
||||
# 是否过滤 Log
|
||||
filter=true
|
||||
# 过滤 Log 时所排除的 sql 关键字,以逗号分隔
|
||||
exclude=
|
Reference in New Issue
Block a user