分享一种灵活的数据权限思路(AOP、反射、MyBatis拦截器)
👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
《项目实战(视频)》:从书中学,往事上“练” 《互联网高频面试题》:面朝简历学习,春暖花开 《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题 《精进 Java 学习指南》:系统学习,互联网主流技术栈 《必读 Java 源码专栏》:知其然,知其所以然
👉这是一个或许对你有用的开源项目
国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。
功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:
Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud 视频教程:https://doc.iocoder.cn
1 前言 2 需求 3 设计思路 4 例子1 查看 订单金额大于100且小于500
的订单5 例子2 查看 收货人地址
模糊查询钦南区
的订单6 当然,一键代码生成,一句代码都不用写即可,实现单表的增删改查 7 项目地址 wonder-server: 一个有意思的权限管理系统[2]
1 前言
我一年java,在小公司,当前公司权限这块都没有成熟的方案,目前我知道权限分为功能权限和数据权限,我不知道数据权限这块大家是怎么解决的,但在实际项目中我遇到数据权限真的复杂,你永远不知道业主在这方面的需求是什么。
我也有去搜索在这方面是怎么做,但是我在gitee、github搜到的权限管理系统他们都是这么实现的:查看全部数据
、自定义数据权限
、本部门数据权限
、本部门及以下数据
、仅本人数据权限
,但是这种控制粒度完全不够的,所以就想自己实现一下。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro 视频教程:https://doc.iocoder.cn/video/
2 需求
需求一 有一个单位
和企业
的树,企业都是挂在某个单位下面的,企业是分类型的(餐饮企业
、经营企业
、生产企业
),业主需要单位的人限定某些单位只能看一个或他指定的某个类型的企业。现在指定角色A
只能查看餐饮
、经营
的企业
,那就只能使用查看自定义部门数据
这个,然后在10000家企业里面慢慢勾选符合的企业,这样可以是可以,但是我觉得这样做不太妥。
估计有人说:那你把三种类型的企业分组,餐饮企业挂在餐饮分组下,其他同理。然后用自定义数据权限
选中那两个不就可以了吗? 可以是可以,但是我不是业主,业主要求了那些企业必须挂在哪些单位
下,在页面显示的树也不能显示什么餐饮企业分组
、生产企业
... 说到底,除非你有办法改变业主的想法。
需求二 类似订单吧,角色A
只能查看未支付
的订单,角色B
只能看交易金额在100~1000元
的订单。
用通用的那5种权限对这两个需求已经是束手无策了。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud 视频教程:https://doc.iocoder.cn/video/
3 设计思路
后来我看到一篇文章【数据权限就该这么实现(设计篇) [1]】,对我有很大的启发,从数据库字段下手,用规则来处理
我以这个文章的思路为基础,设计了这么一个关系
主要还是这张规则表,通过在页面配置好相关的规则来实现对某个字段
的控制
CREATE TABLE `sys_rule` (
`id` bigint NOT NULL,
`remark` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '备注',
`mark_id` bigint DEFAULT NULL,
`table_alias` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '表别名',
`column_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '数据库字段名',
`splice_type` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '拼接类型 SpliceTypeEnum',
`expression` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '表达式 ExpressionEnum',
`provide_type` tinyint DEFAULT NULL COMMENT 'ProvideTypeEnum 值提供类型,1-值,2-方法',
`value1` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '值1',
`value2` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '值2',
`class_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '全限定类名',
`method_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '方法名',
`formal_param` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '形参,分号隔开',
`actual_param` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '实参,分号隔开',
`create_time` datetime DEFAULT NULL,
`create_by` bigint DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`update_by` bigint DEFAULT NULL,
`deleted` bit(1) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='规则表';
整体思路就是通过页面来对特定的接口设置规则,如果提供类型是值
且@DataScope
注解用在方法上,那么默认机会在执行SQL前去拼接对应的数据权限。
如果提供类型是方法
且@DataScope
注解用在方法上,那么会根据你配置的方法名
、参数类型
去反射执行对应的方法,得到该规则能查看的所有idList,然后在执行SQL前去拼接对应的数据权限,这是默认的处理方式。如果@DataScope
注解使用在形参上或者使用Service提供的方法接口,那么需要开发者手动处理,返回什么那么是开发者自定义了。
所以字段你自己定,联表也没问题、反射执行什么方法、参数是什么、过程怎么样也是你自己定,灵活性很高(至少我是这么认为的,哈哈哈哈哈哈)
新建 DataScopeHandler
@Component
public class DataScopeHandler implements DataPermissionHandler {
Map<String, ExpressStrategy> expressStrategyMap = new HashMap<>();
@PostConstruct
public void init() {
expressStrategyMap.put(ExpressionEnum.EQ.toString(), new EqStrategyImpl());
expressStrategyMap.put(ExpressionEnum.NE.toString(), new NeStrategyImpl());
// ....其他情况
}
@Override
public Expression getSqlSegment(Expression oldWhere, String mappedStatementId) {
DataScopeAspect.DataScopeParam dataScopeParam = DataScopeAspect.getDataScopeParam();
// 没有规则就不限制
if (dataScopeParam == null || dataScopeParam.getDataScopeInfo() == null || CollectionUtil.isEmpty(dataScopeParam.getDataScopeInfo().getRuleList()) || SecurityUtil.isAdmin()) {
return oldWhere;
}
Expression newWhere = null;
DataScopeInfo dataScopeInfo = dataScopeParam.getDataScopeInfo();
List<RuleDto> ruleList = dataScopeInfo.getRuleList();
for (RuleDto rule : ruleList) {
ExpressStrategy expressStrategy = expressStrategyMap.get(rule.getExpression());
if (expressStrategy == null)
throw new IllegalArgumentException("错误的表达式:" + rule.getExpression());
newWhere = expressStrategy.apply(rule, newWhere);
}
return oldWhere == null ? newWhere : new AndExpression(oldWhere, new Parenthesis(newWhere));
}
}
使用策略模式 ExpressStrategy
public interface ExpressStrategy {
Expression apply(RuleDto rule, Expression where);
default Object getValue(RuleDto rule) {
if (rule.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())) {
return rule.getResult();
} else if (rule.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())) {
return rule.getValue1();
} else {
throw new IllegalArgumentException("错误的提供类型");
}
}
default Column getColumn(RuleDto rule) {
String sql = "".equals(rule.getTableAlias()) || rule.getTableAlias() == null ? rule.getColumnName() : rule.getTableAlias() + "." + rule.getColumnName();
return new Column(sql);
}
default boolean isOr(String spliceType) {
if (!spliceType.equals(SpliceTypeEnum.AND.toString()) && !spliceType.equals(SpliceTypeEnum.OR.toString())) {
throw new IllegalArgumentException("错误的拼接类型:" + spliceType);
}
return spliceType.equals(SpliceTypeEnum.OR.toString());
}
}
其中一种策略 EqStrategyImpl
这里只列举其中一种情况,我们处理 =
操作
public class EqStrategyImpl implements ExpressStrategy{
@Override
public Expression apply(RuleDto rule, Expression where) {
boolean or = isOr(rule.getSpliceType());
Column column = getColumn(rule);
Object value = getValue(rule);
StringValue valueExpression = new StringValue((String) value);
EqualsTo equalsTo = new EqualsTo(column, valueExpression);
if (or) {
where = where == null ? equalsTo : new OrExpression(where, equalsTo);
} else {
where = where == null ? equalsTo : new AndExpression(where, equalsTo);
}
return where;
}
}
注册 DataScopeHandler
@Configuration
public class MyBatisPlusConfig {
@Autowired
private DataScopeHandler dataScopeHandler;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加自定义的数据权限处理器
DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
dataPermissionInterceptor.setDataPermissionHandler(dataScopeHandler);
interceptor.addInnerInterceptor(dataPermissionInterceptor);
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
自定义注解@DataScope
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
/**
* 标记这是哪个接口,
*/
String value();
}
切面处理
@Aspect
@Slf4j
@Component
public class DataScopeAspect {
@Autowired
private MarkService dataScopeService;
// 通过ThreadLocal记录权限相关的属性值
public static ThreadLocal<DataScopeParam> threadLocal = new ThreadLocal<>();
public static DataScopeParam getDataScopeParam() {
return threadLocal.get();
}
// 方法切点
@Pointcut("@annotation(com.gitee.whzzone.common.annotation.DataScope)")
public void methodPointCut() {
}
@After("methodPointCut()")
public void clearThreadLocal() {
threadLocal.remove();
log.debug("threadLocal.remove()");
}
@Before("methodPointCut()")
public void doBefore(JoinPoint point) {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
// 获得注解
DataScope dataScope = method.getAnnotation(DataScope.class);
try {
if (dataScope != null && !SecurityUtil.isAdmin()) {
// 拿到注解的值
String scopeName = dataScope.value();
// 根据注解的值去解析
DataScopeInfo dataScopeInfo = dataScopeService.execRuleByName(scopeName);
DataScopeParam dataScopeParam = new DataScopeParam();
dataScopeParam.setDataScopeInfo(dataScopeInfo);
threadLocal.set(dataScopeParam);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("数据权限 method 切面错误:" + e.getMessage());
}
}
@Data
public static class DataScopeParam {
private DataScopeInfo dataScopeInfo;
}
}
解析
根据注解的值,能拿到一个mark
,根据这个标记可以查询到对应的rules
,则可以开始进行解析
private DataScopeInfo execRuleHandler(List<Rule> rules) {
if (CollectionUtil.isEmpty(rules))
return null;
List<RuleDto> ruleList = new ArrayList<>();
for (Rule rule : rules) {
RuleDto dto = new RuleDto();
BeanUtil.copyProperties(rule, dto);
if (rule.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())) {
ruleList.add(dto);
} else if (rule.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())) {
try {
Class<?>[] paramsTypes = null;
Object[] argValues = null;
if (StrUtil.isNotBlank(rule.getFormalParam()) && StrUtil.isNotBlank(rule.getActualParam())) {
// 获取形参数组
String[] formalArray = rule.getFormalParam().split(";");
// 获取实参数组
String[] actualArray = rule.getActualParam().split(";");
if (formalArray.length != actualArray.length)
throw new RuntimeException("形参数量与实参数量不符合");
// 转换形参为Class数组
paramsTypes = new Class<?>[formalArray.length];
for (int i = 0; i < formalArray.length; i++) {
paramsTypes[i] = Class.forName(formalArray[i].trim());
}
// 转换实参为Object数组
argValues = new Object[actualArray.length];
for (int i = 0; i < actualArray.length; i++) {
argValues[i] = JSONObject.parseObject(actualArray[i], paramsTypes[i]);
}
}
Class<?> clazz = Class.forName(rule.getClassName());
Object result;
Method targetMethod = clazz.getDeclaredMethod(rule.getMethodName(), paramsTypes);
if (Modifier.isStatic(targetMethod.getModifiers())) {
// 设置静态方法可访问
targetMethod.setAccessible(true);
// 执行静态方法
result = targetMethod.invoke(null, argValues);
} else {
try {
// 尝试从容器中获取实例
Object instance = context.getBean(Class.forName(rule.getClassName()));
Class<?> beanClazz = instance.getClass();
Method beanClazzMethod = beanClazz.getDeclaredMethod(rule.getMethodName(), paramsTypes);
// 执行方法
result = beanClazzMethod.invoke(instance, argValues);
} catch (NoSuchBeanDefinitionException e) {
// 创建类实例
Object obj = clazz.newInstance();
// 执行方法
result = targetMethod.invoke(obj, argValues);
}
}
dto.setResult(result);
ruleList.add(dto);
} catch (NoSuchMethodException e) {
throw new RuntimeException("配置了不存在的方法");
} catch (ClassNotFoundException e) {
throw new RuntimeException("配置了不存在的类");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("其他错误:" + e.getMessage());
}
} else
throw new RuntimeException("错误的提供类型");
}
DataScopeInfo dataScopeInfo = new DataScopeInfo();
dataScopeInfo.setRuleList(ruleList);
return dataScopeInfo;
}
4 例子1 查看订单金额大于100且小于500
的订单
规则配置
新增一个标记,可以理解成一个接口标识
这个接口下所有的规则
查看订单金额大于100且小于500的订单
的需求的具体配置,这个配置的目的是通过反射执行com.gitee.whzzone.admin.business.service.impl.OrderServiceImpl
这个类下的limitAmountBetween(BigDecimal, BigDecimal)
的方法,也就是执行limitAmountBetween(100, 500)
,返回符合条件的orderIds
,然后会在执行sql前去拼接select ... from order where ... and id in ({这里是返回的orderIds})
,从而实现这个权限控制
给角色的这个 订单列表接口
配置查看订单金额大于100且小于500的订单
这个规则,那么这个角色只能查看范围内的订单数据了。
代码
controller
@Api(tags = "订单相关")
@RestController
@RequestMapping("order")
public class OrderController extends EntityController<Order, OrderService, OrderDto, OrderQuery> {
// 通用的增删改查不用写,父类已实现
}
service
public interface OrderService extends EntityService<Order, OrderDto, OrderQuery> {
// 通用的增删改查不用写,父类已实现
/**
* 查询订单范围内的 orderIds
* @param begin 订单金额开始
* @param end 订单金额结束
* @return
*/
List<Long> limitAmountBetween(BigDecimal begin, BigDecimal end);
}
impl
@Service
public class OrderServiceImpl extends EntityServiceImpl<OrderMapper, Order, OrderDto, OrderQuery> implements OrderService {
@DataScope("order-list") // 使用在方法上,交给AOP默认处理,标记这个方法为订单列表查询
@Override // 重写父类列表查询
public List<OrderDto> list(OrderQuery query) {
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverName()), Order::getReceiverName, query.getReceiverName());
queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverPhone()), Order::getReceiverPhone, query.getReceiverPhone());
queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverAddress()), Order::getReceiverAddress, query.getReceiverAddress());
queryWrapper.eq(query.getOrderStatus() != null, Order::getOrderStatus, query.getOrderStatus());
return afterQueryHandler(list(queryWrapper));
}
// 具体实现
@Override
public List<Long> limitAmountBetween(BigDecimal begin, BigDecimal end) {
LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.between(Order::getOrderAmount, begin, end);
List<Order> list = list(queryWrapper);
if (CollectionUtil.isEmpty(list))
return new ArrayList<>();
return list.stream().map(BaseEntity::getId).collect(Collectors.toList());
}
}
这样就实现了查看订单金额大于100且小于500的订单
的需求,其实这个需求用不着这么麻烦,被我复杂化了(演示一下),其实用例子2
的方式来实现。配两条规则:分别是order_amount > 100
和order_amount < 500
的规则,然后选择AND
连接就可以了。
5 例子2 查看收货人地址
模糊查询钦南区
的订单
规则配置
新增一个规则, 提供类型
为值
,单表查询可以不设置表别名,看图吧
配置角色在订单列表查询接口使用的规则
代码
在例子1
的基础上不用做任何改动,因为这个需求无需编写代码
这样就实现了这个简单的需求,这样处理后,就可以在sql执行前拼接对应的查询条件,从而实现数据权限
到这里以上前面说的两个例子就可以搞定了,这查看全部数据
、自定义数据权限
、本部门数据权限
、本部门及以下数据
、仅本人数据权限
五种权限在无形中实现了,针对你的用户id
字段、部门
id字段配几条对应的规则就可以。
6 当然,一键代码生成,一句代码都不用写即可,实现单表的增删改查
EntityController
public abstract class EntityController<T extends BaseEntity<T>, S extends EntityService<T, D, Q>, D extends EntityDto, Q extends EntityQuery> {
@Autowired
private S service;
@RequestLogger
@ApiOperation("获取")
@GetMapping("/get/{id}")
public Result<D> get(@PathVariable Long id){
T t = service.getById(id);
return Result.ok("操作成功", service.afterQueryHandler(t));
}
@RequestLogger
@ApiOperation("删除")
@GetMapping("/delete/{id}")
public Result<Boolean> delete(@PathVariable Long id){
return Result.ok("操作成功", service.removeById(id));
}
@RequestLogger
@ApiOperation("保存")
@PostMapping("save")
public Result<T> save(@Validated(CreateGroup.class) @RequestBody D d){
return Result.ok("操作成功", service.save(d));
}
@RequestLogger
@ApiOperation("更新")
@PostMapping("update")
public Result<Boolean> update(@Validated(UpdateGroup.class) @RequestBody D d){
return Result.ok("操作成功", service.updateById(d));
}
@RequestLogger
@ApiOperation("分页")
@PostMapping("page")
public Result<PageData<D>> page(@RequestBody Q q){
return Result.ok("操作成功", service.page(q));
}
@RequestLogger
@ApiOperation("列表")
@PostMapping("list")
public Result<List<D>> list(@RequestBody Q q){
return Result.ok("操作成功", service.list(q));
}
}
EntityService
public interface EntityService<T extends BaseEntity<T>, D extends EntityDto, Q extends EntityQuery> extends IService<T> {
T save(D d);
boolean updateById(D d);
@Override
T getById(Serializable id);
@Override
boolean removeById(T entity);
@Override
boolean removeById(Serializable id);
T afterSaveHandler(T t);
T afterUpdateHandler(T t);
D afterQueryHandler(T t);
List<D> afterQueryHandler(List<T> list);
void afterDeleteHandler(T t);
default Class<T> getTClass() {
return (Class<T>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 0);
}
default Class<D> getDClass() {
return (Class<D>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 1);
}
default Class<Q> getQClass() {
return (Class<Q>) ReflectionKit.getSuperClassGenericType(this.getClass(), EntityService.class, 2);
}
boolean isExist(Long id);
D beforeSaveOrUpdateHandler(D d);
D beforeSaveHandler(D d);
D beforeUpdateHandler(D d);
PageData<D> page(Q q);
QueryWrapper<T> queryWrapperHandler(Q q);
List<D> list(Q q);
}
EntityServiceImpl
public abstract class EntityServiceImpl<M extends BaseMapper<T>, T extends BaseEntity<T>, D extends EntityDto, Q extends EntityQuery> extends ServiceImpl<M, T> implements EntityService<T, D, Q> {
@Override
@Transactional(rollbackFor = Exception.class)
public T save(D d) {
try {
d = beforeSaveOrUpdateHandler(d);
d = beforeSaveHandler(d);
Class<T> dClass = getTClass();
T t = dClass.getDeclaredConstructor().newInstance();
BeanUtil.copyProperties(d, t);
boolean save = save(t);
if (!save) {
throw new RuntimeException("操作失败");
}
afterSaveHandler(t);
return t;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateById(D d) {
try {
d = beforeSaveOrUpdateHandler(d);
d = beforeUpdateHandler(d);
Class<D> dClass = getDClass();
Class<? super D> superclass = dClass.getSuperclass();
Field fieldId = superclass.getDeclaredField("id");
fieldId.setAccessible(true);
long id = (long) fieldId.get(d);
T t = getById(id);
if (t == null) {
throw new RuntimeException(StrUtil.format("【{}】不存在", id));
}
BeanUtil.copyProperties(d, t);
boolean b = super.updateById(t);
if (b) {
afterUpdateHandler(t);
}
return b;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Override
public T getById(Serializable id) {
if (id == null)
return null;
return super.getById(id);
}
@Override
public boolean removeById(T entity) {
return removeById(entity.getId());
}
@Override
public boolean removeById(Serializable id) {
if (id == null) {
throw new RuntimeException("id不能为空");
}
T t = getById(id);
boolean b = SqlHelper.retBool(getBaseMapper().deleteById(id));
if (b) {
afterDeleteHandler(t);
}
return b;
}
@Override
public T afterSaveHandler(T t) {
return t;
}
@Override
public T afterUpdateHandler(T t) {
return t;
}
@Override
public D afterQueryHandler(T t) {
Class<D> dClass = getDClass();
return BeanUtil.copyProperties(t, dClass);
}
@Override
public List<D> afterQueryHandler(List<T> list) {
List<D> dList = new ArrayList<>();
if (CollectionUtil.isEmpty(list)) {
return dList;
}
for (T t : list) {
D d = afterQueryHandler(t);
dList.add(d);
}
return dList;
}
@Override
public void afterDeleteHandler(T t) {
}
@Override
public boolean isExist(Long id) {
if (id == null)
throw new RuntimeException("id 为空");
long count = count(new QueryWrapper<T>().eq("id", id));
return count > 0;
}
@Override
public D beforeSaveOrUpdateHandler(D d) {
return d;
}
@Override
public D beforeSaveHandler(D d) {
return d;
}
@Override
public D beforeUpdateHandler(D d) {
return d;
}
@Override
public PageData<D> page(Q q) {
try {
QueryWrapper<T> queryWrapper = queryWrapperHandler(q);
IPage<T> page = new Page<>(q.getCurPage(), q.getPageSize());
page(page, queryWrapper);
List<D> dList = afterQueryHandler(page.getRecords());
return new PageData<>(dList, page.getTotal(), page.getPages());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Override
public QueryWrapper<T> queryWrapperHandler(Q q) {
try {
Class<? extends EntityQuery> qClass = q.getClass();
Field[] fields = qClass.getDeclaredFields();
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
Map<String, Field[]> betweenFieldMap = new HashMap<>();
// 处理@SelectColumn
SelectColumn selectColumn = qClass.getAnnotation(SelectColumn.class);
if (selectColumn != null && selectColumn.value() != null && selectColumn.value().length > 0) {
String[] strings = selectColumn.value();
for (int i = 0; i < strings.length; i++) {
strings[i] = StrUtil.toUnderlineCase(strings[i]);
}
queryWrapper.select(strings);
}
String sortColumn = "";
String sortOrder = "";
for (Field field : fields) {
// if (isBusinessField(field.getName())) {
field.setAccessible(true);
Object value = field.get(q);
// 判断该属性是否存在值
if (Objects.isNull(value) || String.valueOf(value).equals("null") || value.equals("")) {
continue;
}
// FIXME 存在bug,应该在判空前执行
// 是否存在注解@QuerySort
QuerySort querySort = field.getDeclaredAnnotation(QuerySort.class);
if (querySort != null) {
String paramValue = (String) field.get(q);
sortColumn = paramValue.isEmpty() ? querySort.value() : paramValue;
}
// 是否存在注解@QueryOrder
QueryOrder queryOrder = field.getDeclaredAnnotation(QueryOrder.class);
if (queryOrder != null) {
String paramValue = (String) field.get(q);
sortOrder = paramValue.isEmpty() ? queryOrder.value() : paramValue;
}
// 是否存在注解@Query
Query query = field.getDeclaredAnnotation(Query.class);
if (query == null) {
continue;
}
String columnName = StrUtil.isBlank(query.column()) ? StrUtil.toUnderlineCase(field.getName()) : query.column();
// TODO 待优化这坨屎山
if (query.expression().equals(ExpressionEnum.EQ)) {
queryWrapper.eq(columnName, value);
} else if (query.expression().equals(ExpressionEnum.NE)) {
queryWrapper.ne(columnName, value);
} else if (query.expression().equals(ExpressionEnum.LIKE)) {
queryWrapper.like(columnName, value);
} else if (query.expression().equals(ExpressionEnum.GT)) {
queryWrapper.gt(columnName, value);
} else if (query.expression().equals(ExpressionEnum.GE)) {
queryWrapper.ge(columnName, value);
} else if (query.expression().equals(ExpressionEnum.LT)) {
queryWrapper.lt(columnName, value);
} else if (query.expression().equals(ExpressionEnum.LE)) {
queryWrapper.le(columnName, value);
} else if (query.expression().equals(ExpressionEnum.IN)) {
queryWrapper.in(columnName, value);
} else if (query.expression().equals(ExpressionEnum.NOT_IN)) {
queryWrapper.notIn(columnName, value);
} else if (query.expression().equals(ExpressionEnum.IS_NULL)) {
queryWrapper.isNull(columnName);
} else if (query.expression().equals(ExpressionEnum.NOT_NULL)) {
queryWrapper.isNotNull(columnName);
} else if (query.expression().equals(ExpressionEnum.BETWEEN)) {
if (betweenFieldMap.containsKey(columnName)) {
Field[] f = betweenFieldMap.get(columnName);
Field[] tempList = new Field[2];
tempList[0] = f[0];
tempList[1] = field;
betweenFieldMap.put(columnName, tempList);
} else {
betweenFieldMap.put(columnName, new Field[]{field});
}
}
}
// }
Set<String> keySet = betweenFieldMap.keySet();
for (String key : keySet) {
// 已在编译时做了相关校验,在此无须做重复且耗时的校验
Field[] itemFieldList = betweenFieldMap.get(key);
if (itemFieldList.length != 2){
throw new IllegalArgumentException("查询参数数量对应异常");
}
Field field1 = itemFieldList[0];
Field field2 = itemFieldList[1];
Query query1 = field1.getDeclaredAnnotation(Query.class);
if (field1.get(q) instanceof Date) {
if (query1.left()) {
queryWrapper.apply("date_format(" + key + ",'%y%m%d') >= date_format({0},'%y%m%d')", field1.get(q));
queryWrapper.apply("date_format(" + key + ",'%y%m%d') <= date_format({0},'%y%m%d')", field2.get(q));
} else {
queryWrapper.apply("date_format(" + key + ",'%y%m%d') <= date_format({0},'%y%m%d')", field1.get(q));
queryWrapper.apply("date_format(" + key + ",'%y%m%d') >= date_format({0},'%y%m%d')", field2.get(q));
}
} else {
if (query1.left()) {
queryWrapper.between(key, field1.get(q), field2.get(q));
} else {
queryWrapper.between(key, field2.get(q), field1.get(q));
}
}
}
if (sortOrder.equalsIgnoreCase("desc")) {
queryWrapper.orderByDesc(StrUtil.isNotBlank(sortColumn), StrUtil.toUnderlineCase(sortColumn));
} else {
queryWrapper.orderByAsc(StrUtil.isNotBlank(sortColumn), StrUtil.toUnderlineCase(sortColumn));
}
return queryWrapper;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Override
public List<D> list(Q q) {
try {
QueryWrapper<T> queryWrapper = queryWrapperHandler(q);
return afterQueryHandler(list(queryWrapper));
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
}
这个怎么说呢,还是方便的,生成代码后就启动了,增删改查的接口就有了,如果自定义业务,重写controller、service实现就可以。
自定义注解
也设计了一些自定义注解来进行辅助查询
// 查询特定字段
@SelectColumn({"id", "create_time", "create_by", "update_time", "update_by", "deleted", "dict_name", "dict_code", "dict_type", "sort", "remark"})
@ApiModel(value = "DictQuery对象", description = "系统字典")
public class DictQuery extends EntityQuery {
@Query(expression = ExpressionEnum.LIKE) // dictName字段模糊查询
@ApiModelProperty("字典名称")
private String dictName;
@Query // dictCode字段精确查询
@ApiModelProperty("字典编码(唯一)")
private String dictCode;
@Query
@ApiModelProperty("字典类型,0-列表,1-树")
private Integer dictType;
@ApiModelProperty("排序")
private Integer sort;
@ApiModelProperty("备注")
private String remark;
// create_time 字段范围查询
@Query(column = "create_time", expression = ExpressionEnum.BETWEEN, left = true)
@ApiModelProperty("开始日期")
private Date beginDate;
// create_time 字段范围查询
@Query(column = "create_time", expression = ExpressionEnum.BETWEEN, left = false)
@ApiModelProperty("结束日期")
private Date endDate;
@QuerySort("sort") // 如果sortColumn为null,根据sort字段排序
@ApiModelProperty("排序字段")
private String sortColumn;
@QueryOrder // 如果sortOrder为null,默认asc
@ApiModelProperty("排序方式-asc/desc")
private String sortOrder;
}
还有很多可以完善的地方,忽略我的垃圾技术,本文主要是描述一下思路。有兴趣的可以移步到仓库看看,一个人维护好难啊,但是也不想放弃,写都写到这是吧,总感觉一个权限管理系统就要成了哈哈哈哈哈 有没有感兴趣的来一起维护维护,非常欢迎~
7 项目地址 wonder-server: 一个有意思的权限管理系统[2]
参考资料
[1]数据权限就该这么实现(设计篇): https://juejin.cn/post/7127810938161332232
[2]wonder-server: 一个有意思的权限管理系统: https://gitee.com/whzzone/wonder-server
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
微信扫码关注该文公众号作者