👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料:
👉这是一个或许对你有用的开源项目
国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。
功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:
  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn

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
publicclassDataScopeHandlerimplementsDataPermissionHandler
{


    Map<String, ExpressStrategy> expressStrategyMap = 
new
 HashMap<>();


@PostConstruct
publicvoidinit()
{

        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
)

thrownew
 IllegalArgumentException(
"错误的表达式:"
 + rule.getExpression());


            newWhere = expressStrategy.apply(rule, newWhere);

        }


return
 oldWhere == 
null
 ? newWhere : 
new
 AndExpression(oldWhere, 
new
 Parenthesis(newWhere));

    }

}

使用策略模式 ExpressStrategy

publicinterfaceExpressStrategy
{


Expression apply(RuleDto rule, Expression where)
;


default Object getValue(RuleDto rule)
{

if
 (rule.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())) {

return
 rule.getResult();

        } 
elseif
 (rule.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())) {

return
 rule.getValue1();

        } 
else
 {

thrownew
 IllegalArgumentException(
"错误的提供类型"
);

        }

    }


default Column getColumn(RuleDto rule)
{

        String sql = 
""
.equals(rule.getTableAlias()) || rule.getTableAlias() == 
null
 ? rule.getColumnName() : rule.getTableAlias() + 
"."
 + rule.getColumnName();

returnnew
 Column(sql);

    }


defaultbooleanisOr(String spliceType)
{

if
 (!spliceType.equals(SpliceTypeEnum.AND.toString()) && !spliceType.equals(SpliceTypeEnum.OR.toString())) {

thrownew
 IllegalArgumentException(
"错误的拼接类型:"
 + spliceType);

        }

return
 spliceType.equals(SpliceTypeEnum.OR.toString());

    }


}

其中一种策略 EqStrategyImpl

这里只列举其中一种情况,我们处理 = 操作
publicclassEqStrategyImplimplementsExpressStrategy
{


@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
publicclassMyBatisPlusConfig
{


@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
@Slf
4j

@Component
publicclassDataScopeAspect
{


@Autowired
private
 MarkService dataScopeService;


// 通过ThreadLocal记录权限相关的属性值
publicstatic
 ThreadLocal<DataScopeParam> threadLocal = 
new
 ThreadLocal<>();


publicstatic DataScopeParam getDataScopeParam()
{

return
 threadLocal.get();

    }


// 方法切点
@Pointcut
(
"@annotation(com.gitee.whzzone.common.annotation.DataScope)"
)

publicvoidmethodPointCut()
{

    }


@After
(
"methodPointCut()"
)

publicvoidclearThreadLocal()
{

        threadLocal.remove();

        log.debug(
"threadLocal.remove()"
);

    }


@Before
(
"methodPointCut()"
)

publicvoiddoBefore(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();

thrownew
 RuntimeException(
"数据权限 method 切面错误:"
 + e.getMessage());

        }


    }


@Data
publicstaticclassDataScopeParam
{

private
 DataScopeInfo dataScopeInfo;

    }


}

解析

根据注解的值,能拿到一个mark,根据这个标记可以查询到对应的rules,则可以开始进行解析
private DataScopeInfo execRuleHandler(List<Rule> rules)
{

if
 (CollectionUtil.isEmpty(rules))

returnnull
;


    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);


        } 
elseif
 (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)

thrownew
 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) {

thrownew
 RuntimeException(
"配置了不存在的方法"
);

            } 
catch
 (ClassNotFoundException e) {

thrownew
 RuntimeException(
"配置了不存在的类"
);

            } 
catch
 (Exception e) {

                e.printStackTrace();

thrownew
 RuntimeException(
"其他错误:"
 + e.getMessage());

            }


        } 
else
thrownew
 RuntimeException(
"错误的提供类型"
);

    }

    DataScopeInfo dataScopeInfo = 
new
 DataScopeInfo();

    dataScopeInfo.setRuleList(ruleList);

return
 dataScopeInfo;

}

4 例子1 查看订单金额大于100且小于500的订单

规则配置

  1. 新增一个标记,可以理解成一个接口标识
  1. 这个接口下所有的规则
  1. 查看订单金额大于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}),从而实现这个权限控制
  1. 给角色的这个订单列表接口配置查看订单金额大于100且小于500的订单这个规则,那么这个角色只能查看范围内的订单数据了。

代码

controller

@Api
(tags = 
"订单相关"
)

@RestController
@RequestMapping
(
"order"
)

publicclassOrderControllerextendsEntityController<OrderOrderServiceOrderDtoOrderQuery
{

// 通用的增删改查不用写,父类已实现
}

service

publicinterfaceOrderServiceextendsEntityService<OrderOrderDtoOrderQuery
{

// 通用的增删改查不用写,父类已实现

/**

     * 查询订单范围内的 orderIds

     * 
@param
 begin 订单金额开始

     * 
@param
 end 订单金额结束

     * 
@return
     */

List<Long> limitAmountBetween(BigDecimal begin, BigDecimal end)
;

}

impl

@Service
publicclassOrderServiceImplextendsEntityServiceImpl<OrderMapperOrderOrderDtoOrderQueryimplementsOrderService
{


@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))

returnnew
 ArrayList<>();


return
 list.stream().map(BaseEntity::getId).collect(Collectors.toList());

    }

}

这样就实现了查看订单金额大于100且小于500的订单的需求,其实这个需求用不着这么麻烦,被我复杂化了(演示一下),其实用例子2的方式来实现。配两条规则:分别是order_amount > 100order_amount < 500的规则,然后选择AND连接就可以了。

5 例子2 查看收货人地址模糊查询钦南区的订单

规则配置

  1. 新增一个规则,提供类型,单表查询可以不设置表别名,看图吧
  1. 配置角色在订单列表查询接口使用的规则

代码

例子1的基础上不用做任何改动,因为这个需求无需编写代码
这样就实现了这个简单的需求,这样处理后,就可以在sql执行前拼接对应的查询条件,从而实现数据权限
到这里以上前面说的两个例子就可以搞定了,这查看全部数据自定义数据权限本部门数据权限本部门及以下数据仅本人数据权限五种权限在无形中实现了,针对你的用户id字段、部门id字段配几条对应的规则就可以。

6 当然,一键代码生成,一句代码都不用写即可,实现单表的增删改查

EntityController

publicabstractclassEntityController<TextendsBaseEntity<T>, SextendsEntityService<TDQ>, DextendsEntityDtoQextendsEntityQuery
{

@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

publicinterfaceEntityService<TextendsBaseEntity<T>, DextendsEntityDtoQextendsEntityQueryextendsIService<T
{


save(D d)
;


booleanupdateById(D d)
;


@Override
getById(Serializable id)
;


@Override
booleanremoveById(T entity)
;


@Override
booleanremoveById(Serializable id)
;


afterSaveHandler(T t)
;


afterUpdateHandler(T t)
;


afterQueryHandler(T t)
;


List<D> afterQueryHandler(List<T> list)
;


voidafterDeleteHandler(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)
;

    }


booleanisExist(Long id)
;


beforeSaveOrUpdateHandler(D d)
;


beforeSaveHandler(D d)
;


beforeUpdateHandler(D d)
;


PageData<D> page(Q q)
;


QueryWrapper<T> queryWrapperHandler(Q q)
;


List<D> list(Q q)
;


}

EntityServiceImpl

publicabstractclassEntityServiceImpl<MextendsBaseMapper<T>, TextendsBaseEntity<T>, DextendsEntityDtoQextendsEntityQueryextendsServiceImpl<MTimplementsEntityService<TDQ
{


@Override
@Transactional
(rollbackFor = Exception
.
class
)

publicTsave
(
Dd
{

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) {

thrownew
 RuntimeException(
"操作失败"
);

            }

            afterSaveHandler(t);

return
 t;

        } 
catch
 (Exception e) {

            e.printStackTrace();

thrownew
 RuntimeException(e.getMessage());

        }

    }


@Override
@Transactional
(rollbackFor = Exception
.
class
)

publicbooleanupdateById
(
Dd
{

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
) {

thrownew
 RuntimeException(StrUtil.format(
"【{}】不存在"
, id));

            }


            BeanUtil.copyProperties(d, t);

boolean
 b = 
super
.updateById(t);

if
 (b) {

                afterUpdateHandler(t);

            }

return
 b;

        } 
catch
 (Exception e) {

            e.printStackTrace();

thrownew
 RuntimeException(e.getMessage());

        }

    }


@Override
public T getById(Serializable id)
{

if
 (id == 
null
)

returnnull
;

returnsuper
.getById(id);

    }


@Override
publicbooleanremoveById(T entity)
{

return
 removeById(entity.getId());

    }


@Override
publicbooleanremoveById(Serializable id)
{

if
 (id == 
null
) {

thrownew
 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
publicvoidafterDeleteHandler(T t)
{


    }


@Override
publicbooleanisExist(Long id)
{

if
 (id == 
null
)

thrownew
 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());


returnnew
 PageData<>(dList, page.getTotal(), page.getPages());


        } 
catch
 (Exception e) {

            e.printStackTrace();

thrownew
 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);

                } 
elseif
 (query.expression().equals(ExpressionEnum.NE)) {

                    queryWrapper.ne(columnName, value);

                } 
elseif
 (query.expression().equals(ExpressionEnum.LIKE)) {

                    queryWrapper.like(columnName, value);

                } 
elseif
 (query.expression().equals(ExpressionEnum.GT)) {

                    queryWrapper.gt(columnName, value);

                } 
elseif
 (query.expression().equals(ExpressionEnum.GE)) {

                    queryWrapper.ge(columnName, value);

                } 
elseif
 (query.expression().equals(ExpressionEnum.LT)) {

                    queryWrapper.lt(columnName, value);

                } 
elseif
 (query.expression().equals(ExpressionEnum.LE)) {

                    queryWrapper.le(columnName, value);

                } 
elseif
 (query.expression().equals(ExpressionEnum.IN)) {

                    queryWrapper.in(columnName, value);

                } 
elseif
 (query.expression().equals(ExpressionEnum.NOT_IN)) {

                    queryWrapper.notIn(columnName, value);

                } 
elseif
 (query.expression().equals(ExpressionEnum.IS_NULL)) {

                    queryWrapper.isNull(columnName);

                } 
elseif
 (query.expression().equals(ExpressionEnum.NOT_NULL)) {

                    queryWrapper.isNotNull(columnName);

                } 
elseif
 (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
){

thrownew
 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();

thrownew
 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();

thrownew
 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 = 
"系统字典"
)

publicclassDictQueryextendsEntityQuery
{


@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

欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,长按”或“扫描”下方二维码噢
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
继续阅读
阅读原文