权限申请审批流程设计
目前Do平台下各类资源比较多(像任性、驾驶舱、烽火、数据集市等等),对应的各种角色也比较多,需要对do平台的每个用户进行权限控制。Do平台功能权限申请之前都是通过发送邮件的方式提交申请,对于审批人来说,工作较为繁琐,容易出错。为方便审批人实现一键通过或者一键驳回的审批功能,同时也为使用户权限申请的流程规范化,设计此权限申请产品。下面主要介绍权限申请中审批流程的设计。
下面是权限申请的业务流程图:
主要有以下几个模块:
1)用户提交申请:
用户通过do平台权限申请入口页面提交申请,用户可以根据自己的角色,选择不同类型的申请(代理商、RD、非RD)。用户提交的申请内容会以快照的方式保存,快照的方式便于申请内容需求扩展,同时减少前后端逻辑交互,减小开发交流成本。
2)创建审批流程
用户提交申请之后,会根据用户提交的申请内容,动态的创建审批流程,后面内容会详细介绍。
3)审批人审批
审批人可以通过do平台权限申请页面(我的审批)、workflow、HI等三种方式进行审批。workflow是运维部提供的一个工作流服务,通过接入workflow,可以复用邮件提醒、HI审批等功能,丰富用户体验。如果审批人通过workflow、HI方式审批,我们无法立即得到该审批信息(因为workflow不会回调任何接口),所以我们通过CT任务的方式定时扫描申请单状态信息,同步到自己系统。
审批流程如下:
1)存储设计
由于每个节点只有一个审批人,因此在设计工作流时,建了两张mysql表:auth_apply(存储用户提交的申请,一条记录代表一个申请)、auth_approve(存储审批流程,一条记录代表一个审批节点,auth_apply表中的一个申请对应该表中多个审批节点)。这样的设计能够完全满足现有需求,但是这并不是一个标准的工作流设计(没有考虑一个节点多个审批人情况),这也为后续功能扩展带来了麻烦。
2)代码框架
由于代理商、RD、非RD申请大体的审批流程是一样的,但是具体到审批流程的每一步可能又不一样,为此采用“模板方法模式”来设计。
Service_Data_ApplySave为抽象基类,模板方法createApplyAndApprove()给出了整个审批流程业务逻辑的骨架,包括一些列抽象操作:genAuditors()生成审批人(包括workflow的审批人)、createApplyWorkFlow()调用workflow接口生成申请单、saveApply()保存申请内容、createApprove()创建本地审批流。Service_Data_AgentApplySave、Service_Data_NordApplySave、Service_Data_RdApplySave等各子类实现相应的抽象方法即可。
3)接入workflow
由于workflow在创建审批流程时,必须提前配置好流程模板,而且不同类型的审批流程需要创建不同的流程模板,所以需要根据业务逻辑推断可能出现的审批流程情况,提前手动创建好流程模板(这种方式极不灵活,也促使后续迭代想办法尽量减少创建流程模板的操作)。
权限申请产品到现在为止经历了两次迭代,而这两次迭代都对代码进行了重构。
1)需求:
1. 在最终审批人中增加多个人审批, 有其中一人审批通过并授权即可
2. 如果申请的ad-hoc表中,有托管库中的表,需要将托管表的所有人的上级加入到审批流程
2)需求分析:
要满足上面两个需求,就必须实现一个节点支持多个审批人,同时节点也要区分操作方式(“与”和“或”,“与”表示该节点所有人都通过,则该节点通过;“或”表示该节点任意一人通过,则该节点通过)。
3)设计方案:
对审批流程进行了简单的重构:增加auth_approve_user表(用于存储每个节点的审批人,一条记录对应一个审批人,一个节点可能对应多条记录),同时auth_approve表增加mode字段,用于判断该节点操作方式。由于增加了一张mysql表,为了兼容老的数据(用户已经提交的单子),就必须在新代码上线前将老的数据按一定的逻辑导入新表中,为了保证导入老数据后,在代码上线前不再有新数据进入,就必须让线上服务在这个上线过程中(导入老数据、代码上线)不可用,这就是上文所说的“麻烦”,不过这个“麻烦”可以接受,因此没有专门去开发平滑迁移的方案。
由于需求2需要在审批流程增加一个节点,该节点是“与”操作节点,workflow不支持动态添加审批人,因此必须去修改流程模板。这里就涉及到并发问题(在创建审批流程时,不能同时有多个进程修改该“与”节点审批人),为此增加了锁机制(下文介绍实现方法),来避免这种并发问题。
抽象类Service_Data_ApplySave增加获取锁和释放锁的方法(使用redis来标记某个流程模板是否正在被修改),Service_Data_RdApplySave类增加相关业务方法,如下图:
4)redis实现锁机制方法:
1.进程A获取锁:$get = hincrby($key, $processId, 1); 如果$get等于1,则A成功获取锁,A操作完毕后释放锁:$get = hincrby($key, $processId, -1);;
2.进程B获取锁:$get = hincrby($key, $processId, 1); 发现$get大于1,说明锁已被占用,B获取锁失败,同时进行回滚操作:$get = hincrby($key, $processId, -1);
3.进程B每隔一段时间重新进行获取锁的操作,直到成功获取。
该锁机制实现方法比较简单,强依赖redis服务,其他优缺点就不在此讨论了。
1)需求:
1.特殊审批流程增加VP审批节点
2.审批委托功能:A可委托B帮其审批
2)需求分析:
上面两个需求都要增加workflow流程模板,同时修改相关代码。我们已经根据之前的需求增加了很多个workflow流程模板了,而这次的需求又要增加流程模板。可以看到,由于接入workflow的方式极不灵活,当涉及到流程修改时,都要大篇幅的修改代码逻辑,导致开发成本比较高。
3)设计方案:
通过如上分析,决定在这次迭代时,对创建审批流程的代码进行底层的重构,通过这次重构,希望能够在以后功能扩展时,尽量减少创建workflow流程模板的操作,尽量只增加业务代码而不是修改与workflow相关的代码甚至底层逻辑。重构后的代码框架如下:
此次重构主要是对创建审批流程进行细化、解耦(与接入workflow解耦),主要思想就是通过将流程步骤细化,将接入workflow从业务逻辑拆解出来,从而达到解耦目的,如下图所示:
将genAuditors()方法拆解成genMyAuditors()、ifModifyToOneState()、repaceProxyAuditor()、genWorkflowAuditors()、selectWorkflowTpl()等方法(图中绿色),createApplyWorkFlow()方法拆解成getUpdateProcessLock()、updateWorkflowTplAuditors()、createApplyWorkFlow()、freeUpdateProcessLock()等方法(图中蓝色),其中红色虚线框内的方法是生成审批人的步骤,黑色虚线框内的方法是接入workflow的步骤。
重构后,添加workflow流程模板不在依赖业务逻辑,而是根据审批流程节点个数和类型(“与”和“或”)来添加模板,如下图所示:
4)重构后的优点:
1.实现业务逻辑与接入workflow解耦
用户提交的申请,可能是不同类型的(代理商、RD、非RD),也可能是特殊申请,还有可能涉及tuoguan库表的申请等等,因此会有不同的业务逻辑来处理。重构前,是根据业务逻辑来选择workflow的流程模板,这样的话,生成审批人和接入workflow是耦合在一起的(重构前的genAuditors()里包含了生成workflow的审批人genWorkflowAuditors()和选择workflow流程模板selectWorkflowTpl()),他们都是业务逻辑的一部分;重构后,根据业务逻辑来生成审批人,然后根据生成的审批人来选择workflow的流程模板,这样就把接入workflow从业务逻辑中拆分出来,从而实现解耦,如下图所示。
2.减小功能扩展及维护成本
实现接入workflow与业务逻辑解耦,会给以后审批流程功能修改带来极大便利:只需关注业务逻辑变化,增加或修改生成审批人的逻辑,而不需要去修改接入workflow的逻辑,功能扩展更加灵活,从而减小新功能开发及维护成本。
5)重构之后存在的缺点:
1.性能影响
重构之后,每次接入workflow时,都要去修改选好的workflow流程模板的每个节点的审批人(因为一个流程模板可能被多个业务逻辑使用到),而这在重构前是不需要的(只需要修改部分模板部分节点审批人),这对性能是有影响的,不过考虑到“提交申请”接口的访问量并不大,因此是可以接受的。
2.额外增加锁机制
为了避免两个进程同时修改同一个workflow流程模板,就必须要额外去增加锁机制解决并发问题(锁机制同样使用redis实现,方法同上)。