1、前言

最近公司重新开发老的oa系统,这边选择后端使用net core 前端使用vue2 重新开发。
在梳理需求时,发现很多菜单的权限判断,都是根据登录用户的职级、或者一些自定义的业务来判断。如果按照传统的RBAC(角色权限管理)来设计,也可以实现,比如根据职级形成多个角色,然后将人员定期同步到对应的角色下。但是这样做的后果是,随着系统业务逻辑的迭代,如后期可能根据其访问来源IP,内外网,是否试用期等等来判断其权限。这样的后果就是角色爆炸,难以维护,而且把人员往角色同步的工作也会随之增加。
所以我在架构设计时,考虑使用ABAC,根据属性来控制权限。这样,我仅需要关注和维护属性,以及属性的判断比较即可。而且这样设计带来的好处就是,在企业内部会比RBAC更适合企业的实际需求。

1.1后端代码实现要点

首先要把ABAC的概念理解并用编码的方式将其实现。
ABAC的实现核心在于基于属性组装成表达式语言,有使用过mybatis的同学都知道以下方式拼接成的sql语句


<if test=' testString != null and testString == "A" '>
   AND 表字段 = #{testString}
</if>
 

<if test=" testString != null and testString == 'A'.toString() ">
  AND 表字段 = #{testString}
</if>

ABAC的实现思路与此类似,根据不同属性,组装不同的表达式,通过表达式来得出有无权限。
实现的方式也有很多,JAVA中可以选择Spring Expression Language:SpEL来实现,也可通过规则引擎。
.net中也可选择规则引擎、Expression表达式树
本文中选择通过Expression表达式树 来实现

1.2、前端实现难点

在本系统设计及实现时,如果说前后端是分离的,那么就要要求前后端人员对ABAC有着相同的理解。
前端如何传递表达式参数?如何展示表达式树,如 age >1 && ( name != ‘张三’ || email != ‘lisi’ ) ,前端该如何形象且易懂的展示给用户,这也是难点所在。
表达式与子表达式如何展示,提示一下:利用vue组件的递归引用

2、系统授权分类

这里简要指出一般系统中授权的三级分类

3、RBAC(Role-Based Access Control)基础概念

基于角色的访问控制,用户绑定角色,角色关联功能,用户只有权访问自己角色所关联的功能。
是IT系统中使用较多的权限管控模型,也是比较简单易懂的一种模型

4、ABAC(Attribute-Based Access Control)基础概念

ABAC是基于属性的访问控制,相较于RBAC粒度更细。再判断用户是否有权限访问某一资源时,是通过其各种属性实时计算而来的。

ABAC中的属性又可细分为:
1、环境属性:访问来源内外网、事件、场景 。
– 如企业内某些系统、功能在未来仅支持内网访问
2、用户属性:职别、职等、部门、籍贯、直间接、签核权
– 实时计算用户属性的好处是,针对频繁的组织、人员异动,只需要修改对应资源的权限策略,无需关注组织人员异动
3、资源属性:资源状态、资源创建时间、资源热度等
– 以API接口资源为例。庞杂的业务系统,一定会衍生出较多的API接口。但随着业务的演变,有些API会被弃用但并未下架,而这些接口又可以访问到一些数据。在后期规划API平台中,就可以根据API的热度,限定在本次访问之前,指定时间内没有被访问过的API禁止访问。

5、RBAC与ABAC对比

5.1、实现方式

5.1.1 RBAC

RBAC实现方式相对较简单,主要需要考虑以下几个方面:
角色定义:定义不同的角色,根据不同的职责和权限进行划分。
角色分配:将用户分配到相应的角色中。
权限分配:将不同的权限分配给不同的角色,以实现权限控制。

5.1.2 ABAC

ABAC的实现过程相对复杂,需要考虑以下几个方面:
属性设计:设计适当的属性标识用户、资源、环境等。
访问策略:基于属性来制定访问策略。
实现机制:引入多个组件来实现属性访问控制,如规则引擎等。

5.2、适用场景

5.2.1 RBAC

适用于规模较小、角色划分较为静态的场景,比如一些中小型企业、少量管理员对应着少量功能的系统等,它的优点在于结构清晰,易于管理。但是如果角色不够细分,就不能对不同的用户进行详细的权限控制,也不太适用于复杂多层次的业务场景。

5.2.2 ABAC

适用于复杂多层次、动态变化的场景,它的优点在于非常灵活,可以应对不同的用户、不同的业务需求,更加符合实际场景的需求。但是ABAC需要对各种属性进行建模,实现更加复杂,需要的技术支持和投入也更大。

6、核心功能梳理及表结构设计

管理侧权限管理流程图:

用户侧登录鉴权流程图:

6.0、属性管理

ABAC中所有权限判定都是围绕属性来做判断,故将现有的属性抽象化,并进行管理,是整个权限管理的数据基础

6.0.1 Sys_Dev_Param (基础参数表)
create table Sys_Dev_Param
(
  Id           bigint        not null
  constraint PK_Sys_Dev_Param_Id
  primary key,
  param_name   nvarchar(200) not null,
  param_value  nvarchar(500) not null,
  param_desc   nvarchar(255) not null,
  value_type   nvarchar(255) not null,
  attr_type    nvarchar(255) not null,
  CreateTime   datetime,
  UpdateTime   datetime,
  CreateEmpNo  nvarchar(255),
  UpdateEmpNo  nvarchar(255),
  CreateEmp    nvarchar(255),
  UpdateEmp    nvarchar(255),
  CreateDeptNo nvarchar(255),
  UpdateDeptNo nvarchar(255),
  ExtJson      nvarchar(max),
  IsDelete     bit
)
go

exec sp_addextendedproperty 'MS_Description', N'基础参数表', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param'
go

exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN', 'Id'
go

exec sp_addextendedproperty 'MS_Description', N'显示名称', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'param_name'
go

exec sp_addextendedproperty 'MS_Description', N'参数key', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'param_value'
go

exec sp_addextendedproperty 'MS_Description', N'参数描述', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'param_desc'
go

exec sp_addextendedproperty 'MS_Description', N'参数值类型 如:string、int、datetime、list', 'SCHEMA', 'dbo', 'TABLE',
     'Sys_Dev_Param', 'COLUMN', 'value_type'
go

exec sp_addextendedproperty 'MS_Description',
     N'参数类型:1:用户基础属性;2:用户扩展属性;3:部门基础属性;4:部门扩展属性;5:环境属性;6:自定义属性 ', 'SCHEMA', 'dbo',
     'TABLE', 'Sys_Dev_Param', 'COLUMN', 'attr_type'
go

exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'CreateTime'
go

exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'UpdateTime'
go

exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'CreateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'UpdateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'CreateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'UpdateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'CreateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'UpdateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN',
     'ExtJson'
go

exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param', 'COLUMN', 'IsDelete'
go
6.0.2 Sys_Dev_Param_API (基础参数表-API参数表)
create table Sys_Dev_Param_API
(
  Id             bigint         not null
  constraint PK_Sys_Dev_Param_API_Id
  primary key,
  param_id       bigint         not null,
  request_url    nvarchar(255)  not null,
  request_method nvarchar(255)  not null,
  request_param  nvarchar(4000) not null,
  reponse_type   nvarchar(255)  not null,
  CreateTime     datetime,
  UpdateTime     datetime,
  CreateEmpNo    nvarchar(255),
  UpdateEmpNo    nvarchar(255),
  CreateEmp      nvarchar(255),
  UpdateEmp      nvarchar(255),
  CreateDeptNo   nvarchar(255),
  UpdateDeptNo   nvarchar(255),
  ExtJson        nvarchar(max),
  IsDelete       bit
)
go

exec sp_addextendedproperty 'MS_Description', N'基础参数-API参数表', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API'
go

exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN', 'Id'
go

exec sp_addextendedproperty 'MS_Description', N'参数ID', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'param_id'
go

exec sp_addextendedproperty 'MS_Description', N'API请求地址', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'request_url'
go

exec sp_addextendedproperty 'MS_Description', N'API请求方法', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'request_method'
go

exec sp_addextendedproperty 'MS_Description', N'API请求参数', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'request_param'
go

exec sp_addextendedproperty 'MS_Description', N'API请求返回类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API',
     'COLUMN', 'reponse_type'
go

exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'CreateTime'
go

exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'UpdateTime'
go

exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'CreateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'UpdateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'CreateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'UpdateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'CreateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'UpdateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'ExtJson'
go

exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Param_API', 'COLUMN',
     'IsDelete'
go

6.1、策略/规则管理

所有基于属性的判断条件都应当归属在规则或策略下
前端设计应当支持策略的复制功能,避免多个菜单,每个都要配置

6.1.1 Sys_Dev_Rule (策略/规则表)

数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)

-- auto-generated definition
create table Sys_Dev_Rule
(
  Id           bigint        not null
  constraint PK_Sys_Dev_Rule_Id
  primary key,
  rule_name    nvarchar(500) not null,
  rule_desc    nvarchar(500) not null,
  CreateTime   datetime,
  UpdateTime   datetime,
  CreateEmpNo  nvarchar(255),
  UpdateEmpNo  nvarchar(255),
  CreateEmp    nvarchar(255),
  UpdateEmp    nvarchar(255),
  CreateDeptNo nvarchar(255),
  UpdateDeptNo nvarchar(255),
  ExtJson      nvarchar(max),
  IsDelete     bit
)
go

exec sp_addextendedproperty 'MS_Description', N'策略/规则', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule'
go

exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'Id'
go

exec sp_addextendedproperty 'MS_Description', N'规则名称', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
     'rule_name'
go

exec sp_addextendedproperty 'MS_Description', N'规则描述', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
     'rule_desc'
go

exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
     'CreateTime'
go

exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
     'UpdateTime'
go

exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
     'CreateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
     'UpdateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'CreateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'UpdateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
     'CreateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN',
     'UpdateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'ExtJson'
go

exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule', 'COLUMN', 'IsDelete'
go

6.2、条件管理

条件管理的唯一目的是获得返回的True/False
什么是条件?
如:EmpNo == ‘001’ 返回true
我们要做的就是把条件抽象化成结构化数据

6.1.1 Sys_Dev_Rule_Cond(条件表)

数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)

-- auto-generated definition
create table Sys_Dev_Rule_Cond
(
  Id               bigint        not null
  constraint PK_Sys_Dev_Rule_Cond_Id
  primary key,
  cond_group_id    bigint        not null,
  left_type        nvarchar(255) not null,
  left_value       nvarchar(255) not null,
  left_value_type  nvarchar(255) not null,
  symbol           nvarchar(255) not null,
  right_type       nvarchar(255),
  right_value_type nvarchar(255),
  right_value      nvarchar(255) not null,
  CreateTime       datetime,
  UpdateTime       datetime,
  CreateEmpNo      nvarchar(255),
  UpdateEmpNo      nvarchar(255),
  CreateEmp        nvarchar(255),
  UpdateEmp        nvarchar(255),
  CreateDeptNo     nvarchar(255),
  UpdateDeptNo     nvarchar(255),
  ExtJson          nvarchar(max),
  IsDelete         bit
)
go

exec sp_addextendedproperty 'MS_Description', N'条件表', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond'
go

exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN', 'Id'
go

exec sp_addextendedproperty 'MS_Description', N'条件归属的条件组ID', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond',
     'COLUMN', 'cond_group_id'
go

exec sp_addextendedproperty 'MS_Description', N'属性来源类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'left_type'
go

exec sp_addextendedproperty 'MS_Description', N'左值', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'left_value'
go

exec sp_addextendedproperty 'MS_Description', N'左值值类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'left_value_type'
go

exec sp_addextendedproperty 'MS_Description', N'关系', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN', 'symbol'
go

exec sp_addextendedproperty 'MS_Description', N'右值类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'right_type'
go

exec sp_addextendedproperty 'MS_Description', N'右值值类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'right_value_type'
go

exec sp_addextendedproperty 'MS_Description', N'右值', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'right_value'
go

exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'CreateTime'
go

exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'UpdateTime'
go

exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'CreateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'UpdateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'CreateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'UpdateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'CreateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'UpdateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'ExtJson'
go

exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond', 'COLUMN',
     'IsDelete'
go

6.2、条件组管理

条件组管理是将多个条件组合在一起,作为一个操作单元,返回一个true或false

条件组的管理要实现,多个条件之间的逻辑关系,且、或
如:EmpNo == ‘001’ || EmpNo == ‘002’
条件组应当能够体现 两个条件的或关系

如:EmpNo == ‘001’ || (EmpNo == ‘002’ && DeptNo == ‘部门1’ )
由于涉及到子逻辑关系,这时新建条件组应当能够体现:
1、新建条件组应 包含 条件a:EmpNo == ‘001’
2、新建条件组应 包含条件组2, 条件组2包含条件b:EmpNo == ‘002’ 、条件c:Eamil == ‘cuizhexin’
3、新建条件组 应当是条件a 和条件组2的父级
4、条件a 和条件组2 应当是平级

如下图:
image.png

6.2.1 Sys_Dev_Cond_Group (条件组表)

数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)


create table Sys_Dev_Rule_Cond_Group
(
  Id           bigint        not null
  constraint PK_Sys_Dev_Rule_Cond_Group_Id
  primary key,
  rule_id      bigint        not null,
  logic_symbol nvarchar(255) not null,
  parent_id    bigint,
  CreateTime   datetime,
  UpdateTime   datetime,
  CreateEmpNo  nvarchar(255),
  UpdateEmpNo  nvarchar(255),
  CreateEmp    nvarchar(255),
  UpdateEmp    nvarchar(255),
  CreateDeptNo nvarchar(255),
  UpdateDeptNo nvarchar(255),
  ExtJson      nvarchar(max),
  IsDelete     bit
)
go

exec sp_addextendedproperty 'MS_Description', N'条件组', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group'
go

exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN', 'Id'
go

exec sp_addextendedproperty 'MS_Description', N'策略ID', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN',
     'rule_id'
go

exec sp_addextendedproperty 'MS_Description', N'逻辑操作符', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
     'COLUMN', 'logic_symbol'
go

exec sp_addextendedproperty 'MS_Description', N'父条件配置ID,为空时,应为顶级条件组', 'SCHEMA', 'dbo', 'TABLE',
     'Sys_Dev_Rule_Cond_Group', 'COLUMN', 'parent_id'
go

exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
     'COLUMN', 'CreateTime'
go

exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
     'COLUMN', 'UpdateTime'
go

exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
     'COLUMN', 'CreateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
     'COLUMN', 'UpdateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN',
     'CreateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN',
     'UpdateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
     'COLUMN', 'CreateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
     'COLUMN', 'UpdateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group',
     'COLUMN', 'ExtJson'
go

exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Rule_Cond_Group', 'COLUMN',
     'IsDelete'
go

6.4、资源管理

资源管理包含菜单及操作的管理,为树形结构

6.4.1 Sys_Dev_Resource (资源表)

数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)

-- auto-generated definition
create table Sys_Dev_Resource
(
  Id                 bigint        not null
  constraint PK_Sys_Dev_Resource_Id
  primary key,
  ParentId           bigint,
  Title              nvarchar(200) not null,
  Name               nvarchar(200),
  Code               nvarchar(200),
  Category           nvarchar(200) not null,
  Module             bigint,
  MenuType           nvarchar(200),
  Path               nvarchar(255),
  Component          nvarchar(200),
  Icon               nvarchar(200),
  Color              nvarchar(200),
  SortCode           int,
  HasTopBanner       bit,
  TopBannerComponent nvarchar(200),
  HasSiderBar        bit,
  Affix              bit,
  IsActiveMenu       bit,
  IsHide             bit,
  IsDisable          bit,
  CreateTime         datetime,
  UpdateTime         datetime,
  CreateEmpNo        nvarchar(255),
  UpdateEmpNo        nvarchar(255),
  CreateEmp          nvarchar(255),
  UpdateEmp          nvarchar(255),
  CreateDeptNo       nvarchar(255),
  UpdateDeptNo       nvarchar(255),
  ExtJson            nvarchar(max),
  IsDelete           bit
)
go

exec sp_addextendedproperty 'MS_Description', N'资源', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource'
go

exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Id'
go

exec sp_addextendedproperty 'MS_Description', N'父id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'ParentId'
go

exec sp_addextendedproperty 'MS_Description', N'标题', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Title'
go

exec sp_addextendedproperty 'MS_Description', N'别名', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Name'
go

exec sp_addextendedproperty 'MS_Description', N'编码', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Code'
go

exec sp_addextendedproperty 'MS_Description', N'分类', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'Category'
go

exec sp_addextendedproperty 'MS_Description', N'所属模块Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'Module'
go

exec sp_addextendedproperty 'MS_Description', N'菜单类型', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'MenuType'
go

exec sp_addextendedproperty 'MS_Description', N'路径', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Path'
go

exec sp_addextendedproperty 'MS_Description', N'组件', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'Component'
go

exec sp_addextendedproperty 'MS_Description', N'图标', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Icon'
go

exec sp_addextendedproperty 'MS_Description', N'颜色', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN', 'Color'
go

exec sp_addextendedproperty 'MS_Description', N'排序码', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'SortCode'
go

exec sp_addextendedproperty 'MS_Description', N'是否拥有头部Banner', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource',
     'COLUMN', 'HasTopBanner'
go

exec sp_addextendedproperty 'MS_Description', N'头部Banner组件地址', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource',
     'COLUMN', 'TopBannerComponent'
go

exec sp_addextendedproperty 'MS_Description', N'是否拥有侧边菜单', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource',
     'COLUMN', 'HasSiderBar'
go

exec sp_addextendedproperty 'MS_Description', N'是否首页', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'Affix'
go

exec sp_addextendedproperty 'MS_Description', N'是否默认激活', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'IsActiveMenu'
go

exec sp_addextendedproperty 'MS_Description', N'是否隐藏', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'IsHide'
go

exec sp_addextendedproperty 'MS_Description', N'是否禁用', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'IsDisable'
go

exec sp_addextendedproperty 'MS_Description', N'创建时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'CreateTime'
go

exec sp_addextendedproperty 'MS_Description', N'更新时间', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'UpdateTime'
go

exec sp_addextendedproperty 'MS_Description', N'创建者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'CreateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'UpdateEmpNo'
go

exec sp_addextendedproperty 'MS_Description', N'创建人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'CreateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'更新人', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'UpdateEmp'
go

exec sp_addextendedproperty 'MS_Description', N'创建者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'CreateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'修改者部门', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'UpdateDeptNo'
go

exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'ExtJson'
go

exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Resource', 'COLUMN',
     'IsDelete'
go

6.5、资源与策略关联管理

image.png

6.5.1 Sys_Dev_Res_Rule_Real (资源与策略关联表)

数据库设计(所有表均需继承公共表,包含ID、创建时间、是否删除等公共属性)

-- auto-generated definition
create table Sys_Dev_Res_Rule_Real
(
  Id       bigint not null
  constraint PK_Sys_Dev_Res_Rule_Real_Id
  primary key,
  res_id   bigint not null,
  rule_id  bigint not null,
  ExtJson  nvarchar(max),
  IsDelete bit
)
go

exec sp_addextendedproperty 'MS_Description', N'资源策略关联表', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real'
go

exec sp_addextendedproperty 'MS_Description', 'Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN', 'Id'
go

exec sp_addextendedproperty 'MS_Description', N'资源Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN',
     'res_id'
go

exec sp_addextendedproperty 'MS_Description', N'策略Id', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN',
     'rule_id'
go

exec sp_addextendedproperty 'MS_Description', N'扩展信息', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN',
     'ExtJson'
go

exec sp_addextendedproperty 'MS_Description', N'软删除', 'SCHEMA', 'dbo', 'TABLE', 'Sys_Dev_Res_Rule_Real', 'COLUMN',
     'IsDelete'
go

7、.net 核心代码实现

7.1 获取当前登录用户信息

也是用户基础属性的数据来源,通过UserManager.属性 获取到当前登录用户的动态信息。这里只介绍概念,具体实现视业务而定

/// <summary>
/// 当前登录用户信息
/// </summary>
public class UserManager
{
    /// <summary>
    /// 当前用户EmpNo
    /// </summary>
    public static string EmpNo => App.User?.FindFirst(ClaimConst.EmpNo)?.Value;

    /// <summary>
    /// 当前用户账号
    /// </summary>
    public static string Email => App.User?.FindFirst(ClaimConst.Email)?.Value;

    /// <summary>
    /// 当前用户名称
    /// </summary>
    public static string EmpName => App.User?.FindFirst(ClaimConst.Name)?.Value;

    /// <summary>
    /// 机构ID
    /// </summary>
    public static string DeptNo => App.User?.FindFirst(ClaimConst.DeptNo)?.Value;

    /// <summary>
    /// 签核权限等级
    /// </summary>
    public static string ApproveLevel => App.User?.FindFirst(ClaimConst.ApproveLevel)?.Value;

    /// <summary>
    /// OrgCode
    /// </summary>
    public static string OrgCode => App.User?.FindFirst(ClaimConst.OrgCode)?.Value;

    /// <summary>
    /// Extend 扩展字段
    /// </summary>
    public static string ExtJson => App.User?.FindFirst(ClaimConst.ExtJson)?.Value;
}
7.2 新增策略
/// <summary>
/// 新增策略
/// </summary>
/// <param name="rule"></param>
/// <exception cref="Exception"></exception>
public void AddRule(DevRuleDto rule)
{
    try
    {
        // 1、开启事务
        repository.BeginTran();

        // 2、新增策略
        var devRule = repository.InsertReturnEntity(new SysDevRule
                                                    {
                                                        RuleName = rule.RuleName,
                                                        RuleDesc = rule.RuleDesc,
                                                    }.ToCreate());

        // 3、递归新增条件组
        CreateSubConditionGroups(rule.RuleCondGroup, devRule.Id);

        // 4、提交事务
        repository.CommitTran();
    }
    catch (Exception ex)
    {
        repository.RollbackTran();
        throw new Exception("新增策略出错,请联系管理员", ex);
    }
}
7.3 递归新增条件组
/// <summary>
/// 递归新增条件组
/// </summary>
/// <param name="group"></param>
/// <param name="ruleId"></param>
private void CreateSubConditionGroups(DevRuleCondGroupDto group, long ruleId)
{
    // 如果group为空,则直接返回
    if (group == null)
    {
        return;
    }

    // 插入SysDevRuleCondGroup记录
    var condGroupId = repository.Change<SysDevRuleCondGroup>().InsertReturnEntity(
        new SysDevRuleCondGroup
        {
            LogicSymbol = group.LogicSymbol,
            RuleId = ruleId,
            ParentId = group.ParentId,
        }.ToCreate()).Id;

    // 插入SysDevRuleCond记录
    foreach (var condDto in group.ConditionList)
    {
        repository.Change<SysDevRuleCond>().Insert(new SysDevRuleCond
                                                   {
                                                       CondGroupId = condGroupId,
                                                       LeftType = condDto.LeftType,
                                                       LeftValue = condDto.LeftValue,
                                                       LeftValueType = condDto.LeftValueType,
                                                       Symbol = condDto.Symbol,
                                                       RightType = condDto.RightType,
                                                       RightValueType = condDto.RightValueType,
                                                       RightValue = condDto.RightValue,
                                                   }.ToCreate());
    }

    // 递归插入子条件组
    foreach (var subGroup in group.ConditionGroupList.Where(subGroup => subGroup != null))
    {
        subGroup.ParentId = condGroupId;
        CreateSubConditionGroups(subGroup, ruleId);
    }
}
7.4 递归组装Expression
/// <summary>
/// 获取根节点下的所有表达式
/// </summary>
/// <param name="ruleCondGroup"></param>
/// <returns></returns>
public Expression GetAllExpression(SysDevRuleCondGroup ruleCondGroup)
{
    // 初始化条件列表表达式为true常量
    Expression condListBody = Expression.Constant(true);

    var condExpressions = new List<Expression>();

    // 遍历所有条件,目前仅支持部分类型
    foreach (var cond in ruleCondGroup.ConditionList)
    {
        condExpressions.Add(GetExpression(cond));
    }

    // 根据逻辑符号合并条件表达式
    condListBody = MergeExpressions(ruleCondGroup.LogicSymbol, condListBody, condExpressions);

    // 递归处理子条件组并合并到当前条件表达式
    if (ruleCondGroup.ConditionGroupList != null)
    {
        foreach (var subGroup in ruleCondGroup.ConditionGroupList)
        {
            var subGroupExpression = GetAllExpression(subGroup);
            condListBody = MergeExpressions(ruleCondGroup.LogicSymbol, condListBody,
                                            new List<Expression> { subGroupExpression });
        }
    }

    return condListBody;
}
7.5 单个条件 组装表达式
/// <summary>
/// 单个条件 组装表达式
/// </summary>
/// <param name="cond"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
private Expression GetExpression(SysDevRuleCond cond)
{
    switch (cond.LeftType)
    {
            // 用户基础属性
        case DevParamConst.EmpBaseAttr:
            return GetEmpBaseExpression(cond);
            // 用户组织基础属性
        case DevParamConst.DeptBaseAttr:
            // 可以添加异常处理或日志记录
            break;
            // 环境属性
        case DevParamConst.EnvAttr:
            // 可以添加异常处理或日志记录
            break;
            // 自定义属性
        case DevParamConst.CustomAttr:
            // 可以添加异常处理或日志记录
            break;
        default:
            throw new ArgumentException($"Unsupported condition type: {cond.LeftType}");
    }

    return Expression.Constant(true); // 如果不支持该条件类型,默认返回true
}
7.6 根据逻辑符号合并条件表达式
private Expression MergeExpressions(string logicSymbol, Expression existingExpr,
                                    IEnumerable<Expression> expressions)
{
    return logicSymbol switch
    {
            "AND" => expressions.Aggregate(existingExpr, Expression.AndAlso),
            "OR" => expressions.Aggregate(existingExpr, Expression.OrElse),
            _ => throw new ArgumentException($"Unsupported logic symbol: {logicSymbol}")
            };
}
7.7 构造属性表达式具体实现
public Expression GetEmpBaseExpression(SysDevRuleCond cond)
{
    // 判断UserManager中是否有该属性
    if (typeof(UserManager).GetProperties().All(x =>
                                                !string.Equals(x.Name, cond.LeftValue, StringComparison.CurrentCultureIgnoreCase)))
    {
        return null;
    }

    Expression? constant = null;

    //构造表达式左侧
    var left = Expression.Constant(typeof(UserManager).GetProperties().First(x =>
                                                                             string.Equals(x.Name, cond.LeftValue, StringComparison.CurrentCultureIgnoreCase)).GetValue(null));
    //构造表达式右侧
    ConstantExpression? right = null;
    ConstantExpression? rightStart = null;
    ConstantExpression? rightEnd = null;
    var requestPart = (cond.RightValue + "?empNo={empNo}").SetTemplates(new
                                                                        {
                                                                            empNo = UserManager.EmpNo,
                                                                        });

    switch (cond.RightValueType)
    {
        case DevParamConst.DataType_STRING:
            right = Expression.Constant(cond.RightValue);
            break;
        case DevParamConst.DataType_LIST:
            right = Expression.Constant(cond.RightValue.Split(";"));
            break;
        case DevParamConst.DataType_NUMBER:
            right = Expression.Constant(int.Parse(cond.RightValue));
            break;
        case DevParamConst.DataType_DATETIME:
            right = Expression.Constant(DateTime.Parse(cond.RightValue));
            break;
        case DevParamConst.DataType_DATETIME_RANGE:
            rightStart = Expression.Constant(DateTime.Parse(cond.RightValue.Split(";")[0]));
            rightEnd = Expression.Constant(DateTime.Parse(cond.RightValue.Split(";")[0]));
            break;
        case DevParamConst.DataType_API_STRING:
            try
            {
                var result = requestPart.GetAsAsync<string>().Result;
                right = Expression.Constant(result);
                break;
            }
            catch (Exception e)
            {
                break;
            }
        case DevParamConst.DataType_API_NUMBER:
            try
            {
                var result = requestPart.GetAsAsync<int>().Result;
                right = Expression.Constant(result);
                break;
            }
            catch (Exception e)
            {
                break;
            }
        case DevParamConst.DataType_API_DATETIME:
            try
            {
                var result = requestPart.GetAsAsync<DateTime>().Result;
                right = Expression.Constant(result);
                break;
            }
            catch (Exception e)
            {
                break;
            }
        case DevParamConst.DataType_API_DATETIME_RANGE:
            try
            {
                var result = requestPart.GetAsAsync<List<DateTime>>().Result;
                rightStart = Expression.Constant(result[0]);
                rightEnd = Expression.Constant(result[1]);
                break;
            }
            catch (Exception e)
            {
                break;
            }
        case DevParamConst.DataType_API_LIST_STRING:
            try
            {
                var result = requestPart.GetAsAsync<List<string>>().Result;
                right = Expression.Constant(result);
                break;
            }
            catch (Exception e)
            {
                break;
            }
        case DevParamConst.DataType_API_LIST_NUMBER:
            try
            {
                var result = requestPart.GetAsAsync<List<int>>().Result;
                right = Expression.Constant(result);
                break;
            }
            catch (Exception e)
            {
                break;
            }
    }

    //构造运算表达式
    constant = cond.Symbol switch
    {
            DevParamConst.Symbol_EQUAL => Expression.Equal(left, right),
            DevParamConst.Symbol_NOT_EQUAL => Expression.NotEqual(left, right),
            DevParamConst.Symbol_GT => Expression.GreaterThan(left, right),
            DevParamConst.Symbol_GT_EQUAL => Expression.GreaterThanOrEqual(left, right),
            DevParamConst.Symbol_LT => Expression.LessThan(left, right),
            DevParamConst.Symbol_LT_EQUAL => Expression.LessThanOrEqual(left, right),
            DevParamConst.Symbol_CONTAIN => cond.RightValueType switch
            {
                    DevParamConst.DataType_STRING => Expression.Call(left,
                                                                     typeof(string).GetMethod("Contains", new[] { typeof(string) }), right),
                    DevParamConst.DataType_LIST => Expression.Call(left,
                                                                   typeof(List<string>).GetMethod("Contains", new[] { typeof(List<string>) }), right),
                    _ => constant
                    },
            DevParamConst.Symbol_NOT_CONTAIN => cond.RightValueType switch
            {
                    DevParamConst.DataType_STRING => Expression.Not(Expression.Call(left,
                                                                                    typeof(string).GetMethod("Contain", new[] { typeof(string) }), right)),
                    DevParamConst.DataType_LIST => Expression.Not(Expression.Call(left,
                                                                                  typeof(List<string>).GetMethod("Contain", new[] { typeof(List<string>) }), right)),
                    _ => constant
                    },
            DevParamConst.Symbol_IN => cond.RightValueType switch
            {
                    DevParamConst.DataType_STRING => Expression.Call(right,
                                                                     typeof(string).GetMethod("Contains", new[] { typeof(string) }), left),
                    DevParamConst.DataType_LIST => Expression.Call(right,
                                                                   typeof(List<string>).GetMethod("Contains", new[] { typeof(List<string>) }), left),
                    _ => constant
                    },
            DevParamConst.Symbol_NOT_IN => cond.RightValueType switch
            {
                    DevParamConst.DataType_STRING => Expression.Not(Expression.Call(right,
                                                                                    typeof(string).GetMethod("Contain", new[] { typeof(string) }), left)),
                    DevParamConst.DataType_LIST => Expression.Not(Expression.Call(right,
                                                                                  typeof(List<string>).GetMethod("Contain", new[] { typeof(List<string>) }), left)),
                    _ => constant
                    },
            DevParamConst.Symbol_BETWEEN => Expression.AndAlso(Expression.GreaterThanOrEqual(left, rightStart),
                                                               Expression.LessThanOrEqual(left, rightEnd)),
            _ => constant
            };

    return constant;
}


public List<Expression> GetEmpExtendExpression(SysDevRuleCond cond)
{
    // todo 由于开发进度要求,暂时不做实现,等API管理平台时,如有需求再做实现
    return null;
}

public List<Expression> GetDeptBaseExpression(SysDevRuleCond cond)
{
    return null;
}

public List<Expression> GetDeptExtendExpression(SysDevRuleCond cond)
{
    return null;
}

public List<Expression> GetEnvExpression(SysDevRuleCond cond)
{
    return null;
}

public List<Expression> GetCustomExpression(SysDevRuleCond cond)
{
    return null;
}

8、vue核心代码

8.1 条件组

如下图,条件配置为当前页面核心。通过vue组件实现,递归调用。
image.png

<template>
  <div>
    <el-row ref="configRef" :gutter="5" style="padding-left: 10px">
      <!--   1、左侧花括号区域,及逻辑关系    -->
      <el-col
        v-if="calcLeftBracketShow()"
        class="leftBracketCol"
        :span="1"
        style=""
        >
        <el-row class="topBracket" :style="{ height: bracketHeight }"></el-row>
        <el-row>
          <div class="middleBracket">
            <span v-if="ruleConfig.logicSymbol == 'AND'">且</span>
            <span v-else>或</span>
            <i class="el-icon-d-caret symbolIcon" @click="changeLogicSymbol"></i>
          </div>
        </el-row>
        <el-row class="bottomBracket" :style="{ height: bracketHeight }">
        </el-row>
      </el-col>
      <!--   2、右侧条件区域     -->
      <el-col ref="conditionCol" :span="calcLeftBracketShow() ? 23 : 24">
        <el-row v-for="(item, index) in ruleConfig.conditionList"
          :key="index" style="margin-top: 15px; padding-left: 15px" :gutter="5">
          <!--   2.1、属性分类     -->
          <el-col :span="6">
            <el-select
              @change="changeAttrType($event,item)"
              size="small"
              style="width: 100%"
              v-model="item.leftType"
              clearable
              placeholder="请选择属性分类"
              >
              <el-option
                v-for="dict in dict.type.AttrType"
                :key="dict.value"
                :label="dict.label"
                :value="dict.value"
                />
            </el-select>
          </el-col>
          <!--    2.2、属性     -->
          <el-col :span="5">
            <el-select
              :disabled="null == item.leftType || '' === item.leftType"
              size="small"
              style="width: 100%"
              clearable
              v-model="item.leftValue"
              @change="changeLeftValueAndType($event,item)"
              placeholder="请选择属性"
              >
              <el-option
                v-for="dict in attrOptions[item.leftType]"
                :key="dict.paramValue"
                :label="dict.paramName"
                :value="dict.paramValue"
                />
            </el-select>
          </el-col>
          <!--    2.3、运算关系     -->
          <el-col :span="3">

            <el-select
              :disabled="null == item.leftValueType || '' === item.leftValueType"
              size="small"
              style="width: 100%"
              v-model="item.symbol"
              placeholder="请选择关系"
              @change="changeSymbol($event,item)"
              >
              <el-option
                v-for="dict in dict.type[item.leftValueType+'_SYMBOL']"
                :key="dict.value"
                :label="dict.label"
                :value="dict.value"
                />
            </el-select>
          </el-col>
          <!--    2.4、关系右值类型     -->
          <el-col :span="4">

            <el-select
              :disabled="null == item.symbol || '' === item.symbol"
              size="small"
                style="width: 100%"
                v-model="item.rightValueType"
                placeholder="选择值类型"
                @change="changeRightValueType($event,item)"
            >
              <el-option label="字符串" value="STRING"></el-option>
              <el-option label="数字" value="NUMBER"></el-option>
              <el-option label="日期" value="DATETIME"></el-option>
              <el-option label="日期区间" value="DATETIME_RANGE"></el-option>
              <el-option label="LIST" value="LIST"></el-option>
              <el-option label="BOOLEAN" value="BOOLEAN"></el-option>
              <el-option label="API返回:STRING" value="API_STRING"></el-option>
              <el-option label="API返回:NUMBER" value="API_NUMBER"></el-option>
              <el-option label="API返回:BOOLEAN" value="API_BOOLEAN"></el-option>
              <el-option label="API返回:LIST<STRING>" value="API_LIST<STRING>"></el-option>
              <el-option label="API返回:LIST<NUMBER>" value="API_LIST<NUMBER>"></el-option>
            </el-select>
          </el-col>
          <!--    2.5、当右值类型为空时,默认disable    -->
          <el-col v-if="null == item.rightValueType || '' === item.rightValueType" :span="5">
            <el-input size="small" disabled></el-input>
          </el-col>
          <!--    2.6、右值类型为STATIC固定值时   -->
          <el-col
              v-if="'STRING' === item.rightValueType || 'NUMBER' === item.rightValueType || 'DATETIME' === item.rightValueType || 'DATETIME_RANGE' === item.rightValueType || 'LIST' === item.rightValueType || 'BOOLEAN' === item.rightValueType "
              :span="5">
            <el-input v-if="item.leftValueType === 'STRING'" size="small" v-model:value="item.rightValue"
                      placeholder="请输入字符串值" @input="changeInput"></el-input>
            <el-input-number v-if="item.leftValueType === 'NUMBER'" size="small" v-model="item.rightValue"
                             placeholder="请输入数字" @input="changeInput"></el-input-number>
            <el-date-picker
                v-if="item.leftValueType === 'DATETIME' && item.rightValueType === 'DATETIME'"
                size="small"
                v-model="item.rightValue"
                type="datetime"
                placeholder="选择日期时间"
                @change="changeInput"
            />
            <el-input v-if="item.leftValueType === 'LIST'" size="small" v-model="item.rightValue"
                      placeholder="多个值请用英文分号分隔" @input="changeInput"></el-input>
            <el-select
                v-if="item.leftValueType === 'BOOLEAN' || item.leftValueType === 'API'"
                size="small"
                style="width: 100%"
                v-model="item.logicSymbol"
                placeholder="请选择"
                @change="changeInput"
            >
              <el-option label="true" value="true"></el-option>
              <el-option label="false" value="false"></el-option>
            </el-select>
            <el-input v-if="null == item.leftValueType  || item.leftValueType === '' " size="small"
                      disabled></el-input>

          </el-col>
          <!--    2.7、右值类型为API返回时   -->
          <el-col
              v-if="'API_STRING' === item.rightValueType || 'API_NUMBER' === item.rightValueType || 'API_BOOLEAN' === item.rightValueType || 'API_LIST<STRING>' === item.rightValueType || 'API_LIST<NUMBER>' === item.rightValueType "
              :span="5">

            <el-input size="small" v-model="item.rightValue"
                      placeholder="请输入GET方式API地址" @input="changeInput"></el-input>
          </el-col>
          <el-col :span="1" style="text-align: center">
            <el-tooltip
                class="item"
                effect="dark"
                content="删除条件"
                placement="top"
            >
              <i
                  @mouseenter="remove_enter($event)"
                  @mouseleave="remove_leave($event)"
                  @click="remove_click($event, index)"
                  class="el-icon-remove-outline removeCond"
              ></i>
            </el-tooltip>
          </el-col>
        </el-row>

        <!--      2.8、子条件组,通过递归组件嵌套    -->
        <el-row
            v-for="(item, index) in ruleConfig.conditionGroupList"
            :key="'conditionGroupList'+index"
            type="flex"
            align="middle"
            style="
            display: flex;
            flex: 1 1;
            margin-top: 15px;
            padding-left: 15px;
            border-radius: 6px;
          "
        >
          <!--      2.8.1、递归组件外border   -->
          <el-col
              v-if="item.conditionList.length > 0"
              :span="23"
              style="border: 1px solid rgba(28, 28, 35, 0.08)"
          >
            <rule-config-comp
                :key="index"
                :ruleConfigData="item"
            ></rule-config-comp>
          </el-col>
          <!--      2.8.2、递归组件删除按钮  -->
          <el-col
              v-if="item.conditionList.length > 0"
              :span="1"
              style="
              display: inline-block;
              text-align: center;
              vertical-align: middle;
            "
          >
            <el-tooltip
                class="item"
                effect="dark"
                content="删除条件组"
                placement="top"
            >
              <i
                  class="el-icon-remove-outline removeCond"
                  @mouseenter="remove_enter($event)"
                  @mouseleave="remove_leave($event)"
                  @click="remove_group_click($event, index)"
              ></i>
            </el-tooltip>
          </el-col>
        </el-row>
        <!--      3、本层组件的新增按钮  -->
        <el-row style="margin-top: 10px">
          <el-button type="text" @click="addCond">新增条件</el-button>
          <el-divider direction="vertical"></el-divider>
          <el-dropdown @command="handleCommand" size="small" trigger="click">
            <span class="el-dropdown-link">
              <el-button type="text">
                <i class="el-icon-caret-bottom"></i>
              </el-button>
            </span>
            <el-dropdown-menu slot="dropdown" style="margin: 0 !important">
              <el-dropdown-item command="addGroup">添加条件组</el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
        </el-row>
      </el-col>
    </el-row>
  </div>
</template>
<script>
import {GetParamDictGroupByAttrType} from "@/api/system/dev/param";

export default {
  name: "RuleConfigComp",
  dicts: ["AttrType", "STRING_SYMBOL", "NUMBER_SYMBOL", "DATETIME_SYMBOL", "LIST_SYMBOL", "BOOLEAN_SYMBOL"],
  props: {
    ruleConfigData: {
      type: Object,
    },
  },
  watch: {
    // 监听父组件传过来的ruleConfigData,计算左侧花括号的高度
    ruleConfigData: {
      deep: true,
      immediate: true,
      handler(val) {
        this.ruleConfig = val;
        this.$nextTick(() => {
          this.calcLeftBracketHeight();
        });
      },
    },
  },
  data() {
    return {
      // 属性下拉框选项集合
      attrOptions: {},
      // right value下拉框选项集合
      rightValueBoolOptions: [{
        label: "true",
        value: "true",
      }, {
        label: "false",
        value: "false",
      }],
      bracketHeight: "",
      bracketShow: false,
      ruleConfig: null,
      formLabelWidth: "100px",
      form: {
        name: null,
        type: null,
        description: null,
      },
      rules: {
        name: [
          {required: true, message: "请输入策略名称", trigger: "change"},
        ],
        type: [
          {required: true, message: "请选择策略类型", trigger: "change"},
        ],
        description: [
          {required: true, message: "请输入策略描述", trigger: "change"},
        ],
        leftType: [
          {required: true, message: "请选择属性分类", trigger: "change"},
        ],
        leftValue: [
          {required: true, message: "请选择属性", trigger: "change"},
        ],
        symbol: [
          {required: true, message: "请选择属性", trigger: "change"},
        ],
        leftValueType: [
          {required: true, message: "请选择运算关系", trigger: "change"},
        ],
        rightValueType: [
          {required: true, message: "选择值类型", trigger: "change"},
        ],
        rightValue: [
          {required: true, message: "请输入值", trigger: "blur"},
        ],
      },
    };
  },
  created() {
    this.calcLeftBracketHeight();
    this.initParamDict();
  },
  methods: {
    //
    initParamDict() {
      var _this = this
      GetParamDictGroupByAttrType().then((res) => {
        if (res.code === 200) {
          var dict = res.data
          _this.attrOptions = dict

        }
      })
    },
    // 1、leftType 条件属性来源  change事件
    changeAttrType(val, item) {
      var _this = this
      // 将属性值已选中置空
      item.leftValue = ''
      item.leftValueType = ''
      _this.$forceUpdate();
    },
    // 2、属性值 change事件
    changeLeftValueAndType(val, item) {
      var _this = this
      item.leftValue = val
      if ('' === val) {
        item.leftValueType = ''
      } else {
        item.leftValueType = _.find(_this.attrOptions[item.leftType], ['paramValue', val]).valueType
      }
      item.symbol = ''
      _this.$forceUpdate()
    },
    // 3、关系 change事件
    changeSymbol(val, item) {
      item.symbol = val
      item.rightValueType = ''
      this.$forceUpdate()
    },
    // 4、右值类型 change事件
    changeRightValueType(val, item) {
      item.rightValueType = val
      item.rightValue = ''
      this.$forceUpdate()
    },
    changeInput() {
      this.$forceUpdate()
    },
    // 计算左侧括号是否显示
    calcLeftBracketShow: function () {
      var _this = this;
      if (_this.ruleConfig.conditionList.length > 1) {
        _this.bracketShow = true;
        return _this.bracketShow;
      }
      if (_this.ruleConfig.conditionList.length < 1) {
        _this.bracketShow = false;
        return _this.bracketShow;
      }
      if (_this.ruleConfig.conditionList.length === 1 && null == _this.ruleConfig.conditionGroupList) {
        _this.bracketShow = false;
        return _this.bracketShow;
      }
      if (_this.ruleConfig.conditionList.length === 1 && _this.ruleConfig.conditionGroupList.length === 0) {
        _this.bracketShow = false;
        return _this.bracketShow;
      }
      if (_this.ruleConfig.conditionList.length === 1 && _this.ruleConfig.conditionGroupList.length > 0) {
        _this.bracketShow = true;
        return _this.bracketShow;
      }
      return _this.bracketShow;
    },
    // 计算左侧括号的高度
    calcLeftBracketHeight: function () {
      var _this = this;
      _this.$nextTick(() => {
        // 获取右侧条件 col的高度
        let height = _this.$refs.conditionCol.$el.clientHeight;
        // 减去一些margin和关系运算的高度
        height = height - 48 - 15 - 15 - 32 - 10;
        // 除以2
        height = height / 2;
        _this.bracketHeight = height + "px";
      });
    },
    handleCommand(cmditem) {
      switch (cmditem) {
        case "addGroup":
          this.addGroup();
          break;
      }
    },
    addCond() {
      //  往当前层级的条件组,也就是conditionList中新增一组条件
      if (null == this.ruleConfig.conditionList) {
        this.ruleConfig.conditionList = [];
      }
      this.ruleConfig.conditionList.push({
        leftValue: null,
        leftType: null,
        leftValueType: null,
        symbol: null,
        rightValue: null,
        rightValueType: null,
      });
      this.calcLeftBracketShow();
    },
    addGroup() {
      if (
          null == this.ruleConfig.conditionList ||
          this.ruleConfig.conditionList.length == 0
      ) {
        this.addCond();
      }
      if (null == this.ruleConfig.conditionGroupList) {
        this.ruleConfig.conditionGroupList = [];
      }
      this.ruleConfig.conditionGroupList.push({
        conditionList: [
          {
            leftValue: null,
            leftType: null,
            leftValueType: null,
            symbol: null,
            rightValue: null,
            rightValueType: null,
          },
        ],
        logicSymbol: "AND",
        conditionGroupList: [],
      });
      this.calcLeftBracketShow();
    },
    remove_click(e, index) {
      // 判断当前是否存在条件组,如果存在,则条件必须保留一条不能删除
      if (
          this.ruleConfig.conditionList.length <= 1 &&
          this.ruleConfig.conditionGroupList.length > 0
      ) {
        // 提示用户,至少需要保留一条
        this.$message({
          type: "warning",
          message: "至少需要保留一条条件!",
        });
        return false;
      }
      this._.pullAt(this.ruleConfig.conditionList, index);
      this.$set(this.ruleConfig.conditionList, this.ruleConfig.conditionList);
      this.$forceUpdate();
      this.calcLeftBracketHeight();
    },
    remove_group_click(e, index) {
      this._.pullAt(this.ruleConfig.conditionGroupList, index);
      this.$set(
          this.ruleConfig.conditionGroupList,
          this.ruleConfig.conditionGroupList
      );
      this.$forceUpdate();
      this.calcLeftBracketHeight();
    },
    changeLogicSymbol() {
      if (this.ruleConfig.logicSymbol === "AND") {
        this.ruleConfig.logicSymbol = "OR";
      } else if (this.ruleConfig.logicSymbol === "OR") {
        this.ruleConfig.logicSymbol = "AND";
      }
    },
    remove_enter(e) {
      var parentElement = e.currentTarget.parentElement.parentElement;
      // 给父元素添加class
      parentElement.classList.add("styles_danger");
    },
    remove_leave(e) {
      var parentElement = e.currentTarget.parentElement.parentElement;
      // 给父元素添加class
      if (parentElement.classList.contains("styles_danger")) {
        parentElement.classList.remove("styles_danger");
      }
    },
  },
};
</script>
<style scoped>
/deep/ .box-card {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-size: 14px;
  background: #fff;
  border: none;

  .el-card__body {
    padding: 0;
  }

  margin-bottom: 20px;
}

/deep/ .el-card__header {
  color: #293350;
  border-bottom: none;
  padding: 0;
  margin-bottom: 10px;
}

.form-row {
  display: flex;
  flex-wrap: wrap;
}

.el-dropdown-link {
  cursor: pointer;
  color: #1890ff;
}

.topBracket {
  border-top-left-radius: 8px;
  border: 1px solid rgba(28, 28, 35, 0.08);
  margin-top: 15px;
  border-right: none;
  border-bottom: none;
  width: 24px;
}

.middleBracket {
  align-items: center;
  border-radius: 4px;
  display: flex;
  flex-direction: row;
  height: 32px;
  justify-content: space-around;
  margin-right: 2px;
  width: 42px;
  padding: 8px 4px;
}

.bottomBracket {
  border-bottom-left-radius: 8px;
  border: 1px solid rgba(28, 28, 35, 0.08);
  border-right: none;
  width: 24px;
  border-top: none;
}

.leftBracketCol {
  align-items: flex-end;
  display: flex;
  flex-direction: column;
  margin-top: 15px;
  height: 100%;
}

.symbolIcon {
  cursor: pointer;

  &:hover {
    color: #1890ff;
  }
}

.removeCond {
  color: #606266;
  margin: 0;
  font-size: 30px;
  vertical-align: middle;

  &:hover {
    color: #f56c6c;
  }
}

.styles_danger {
  input {
    background: #fff1ec !important;
    border-color: transparent !important;
  }
}
</style>
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐