通用审批流程快速开发系统案例分享
公司组织机构是一个树形架构,先前新加盟公司时都是总部直接添加在某个子公司下,因审计需要,要求通过下面公司申请,逐个角色处理来完成新公司的开通,开发任务最后落到我这里,时间紧,任务重,先前也没接触多少审批流程的开发,好在我们的系统是基于通用权限管理系统的底层来做的开发,角色,权限控制已没什么问题,而且底层也集成有一个审批流程引擎组件,只是先前没多少人使用过,通过与吉日嘎拉老师的沟通,大致了解了这个组件的思想,就像其它系统调用权限功能一样,我只需要完成业务功能的开发,实现审批流的接口即可,经过将近3周的开发,终于完成了新公司创建的审批流程。下面整理一下利用通用审批流程组件进行审批流开发的一些经验:
1、需求分析
根据全国省份划分,每个省份设有专人审批下级公司申请创建公司的单据,其中某些省份需要经过片区经理再审核,如下图
注:其中的"网点管理员"是由系统默认的各个公司的在所有系统中具有最大权限的人员,由其提交新开其下级公司的申请,片区文员是各个省份的负责审批该省所有公司提交新开公司的申请单据的人员。
从图上可以看到,新开一个公司根据告诉管理员所在省份的不同会有两个流程中的其中一个处理(流程入口条件),因此我们需要创建两个审批流。
在审核过程中,审核状态有待审、通过、退回、完成四种状态,这个是很好理解的。
2、创建审批流程
通过分析流程,在通用审批组件中创建对应的流程:两个审批流程。
已经创建好的审批流程,打开其中一个看看,如下图:
下图是其中一个流程的定义,可以看到流程与表、程序集、类之间的关系,在具体实现时要继承流程处理接口实现
审核步骤是需要先定义的:
定义审核步骤
审核步骤先创建好,创建审核流程时选择其中的几个审核步骤构成一个流程,创建流程过程中,可设置在流程处理过程中可编辑的字段,这一块我没使用,字段控制我直接在权限中处理了。
3、创建角色
根据上面的流程,我在系统中创建了对应的角色,因为直接是在通用权限系统中开发的,角色配置起来就非常容易了。
在子系统中创建的用于流程审批的角色
4、设置角色人员
上面的角色创建好以后,就需要向角色中添加人员了,根据提供的人员配置到各个角色中,操作也是很简单的
选中角色,点击成员,向角色中添加成员即可。
5、配置菜单权限
通过对整个处理流程的分析,定义了以上与审批流程有关系的权限菜单。注意其中将全部要审核的字段也作为菜单项控制了,这样在分配角色权限时处理方便,每个角色能够修改哪些字段,其中菜单的编号也进行了一些处理,按照实体的名字来命名,这样前后台判断权限时也很方便。
6、配置角色权限
角色及菜单创建完毕后,可以配置各个角色具有的菜单权限了,如下截图:
如上图,是配置片区审核具有的菜单权限,这样片区审核这个角色里的人进入系统就有了对应的权限,只需勾选上要分配的权限菜单即可。
===================================
至此,审批流程、角色、菜单、角色人员,角色菜单权限都配置完毕了,接下来开始具体审批业务功能的开发,可以看到,有了这个通用权限及审批流程的底层管理系统,我们只需关注业务功能即可。
===================================
7、流程处理前端
上图是公司管理员进入申请创建新公司的界面,可以看到,在界面上控制了哪些项目是必填的,哪些项目是没有权限填写的。填写完毕,可以在“申请进度查询”中看到流程进入哪个阶段。
上图是其中一个角色进入审核界面的显示内容,点击审核,就可以处理:通过或者退回
点击上面的提交或者退回,就可以对这个审批单据进行处理了
8、审批流程接口实现
底层中处理工作流的业务类
上图是开始申请的处理流程:将申请单据保存起来,启动审批流程,这是在数据库中存储的结果如下,可以看到当前申请的流程要走的步骤:按ACTIVITYID的顺序。
审批过程中的拒绝操作:不再保存修改记录,退回到上一步。
审批过程中通过审批的处理:可以保存当前人员对单据的修改,同时提交到下一步来处理,如果是最后一步,审核通过时,当前审核单据的状态将变为完成。
可以看到,在处理审批流程时,后台审核部分只需要调用底层接口即可,开发人员只需关注业务功能开发,下面把主要的底层接口提供出来,供参考:
8、部分后台代码
权限判断,实现前后端输入项的验证,前端如果有权限,对应的文本框处于可编辑状态,否则不可编辑,后端也会再次验证,有权限必须填写的都需要经过后端再次验证,这样安全问题就可以解决了。
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Net; using System.Text; using System.Web; using System.Web.Script.Serialization; namespace Infrastructure { using DotNet.Business; using DotNet.Model; using DotNet.Utilities; /// <summary> /// /// 修改纪录 /// /// 2016-03-02 版本:1.0 宋彪 修改文件。 /// /// <author> /// <name>宋彪</name> /// <date>2016-03-02</date> /// </author> /// </summary> public static class WorkFlowBaseUserInfoExt { /// <summary> /// 存储用户权限菜单的sessionKey /// </summary> private static string permissionKey = "UserPermissionListSession"; /// <summary> /// 获取存储在session中的用户权限菜单 /// </summary> /// <param name="userInfo"></param> /// <param name="userId"></param> /// <param name="systemCode"></param> /// <returns></returns> public static List<BaseModuleEntity> GetSessionPermissionList(this BaseUserInfo userInfo, string userId, string systemCode = "Base") { if (HttpContext.Current.Session[permissionKey] == null) { //List<BaseModuleEntity> list = PermissionUtilities.GetPermissionList(userInfo, userId, systemCode); List<BaseModuleEntity> returnResult = new List<BaseModuleEntity>(); try { string url = System.Configuration.ConfigurationManager.AppSettings["LogonService"] + "/PermissionService.ashx"; userInfo.Id = userId; userInfo.SystemCode = systemCode; // 忽略超级管理员 因为超级管理员有全部权限 userInfo.IsAdministrator = false; WebClient webClient = new WebClient(); NameValueCollection postValues = new NameValueCollection(); postValues.Add("Function", "GetPermissionList"); postValues.Add("UserInfo", userInfo.Serialize()); postValues.Add("SystemCode", systemCode); postValues.Add("fromCache", false.ToString()); // 向服务器发送POST数据 byte[] responseArray = webClient.UploadValues(url, postValues); string response = Encoding.UTF8.GetString(responseArray); if (!string.IsNullOrEmpty(response)) { JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer(); returnResult = javaScriptSerializer.Deserialize<List<BaseModuleEntity>>(response); returnResult = returnResult.OrderBy(t => t.SortCode).ToList(); HttpContext.Current.Session[permissionKey] = returnResult; } } catch (Exception ex) { Log.Write(ex.ToString()); } return returnResult; } return (List<BaseModuleEntity>)HttpContext.Current.Session[permissionKey]; } /// <summary> /// 移除session中的用户权限 /// </summary> /// <param name="userInfo"></param> /// <param name="userId"></param> /// <param name="systemCode"></param> public static void RemoveSessionPermissionList(this BaseUserInfo userInfo, string userId, string systemCode = "Base") { if (HttpContext.Current.Session[permissionKey] != null) { HttpContext.Current.Session.Remove(permissionKey); } } /// <summary> /// 判断用户是否有某个菜单的权限,从存储再session中的获取菜单 /// </summary> /// <returns></returns> public static bool IsAuthorizedByCode(this BaseUserInfo userInfo, string code, string systemCode = "Base") { if (string.IsNullOrWhiteSpace(code)) { return false; } List<BaseModuleEntity> list = GetSessionPermissionList(userInfo, userInfo.Id, systemCode); if (list != null && list.Any()) { return list.Any(t => string.Equals(t.Code, code, StringComparison.OrdinalIgnoreCase)); } return false; } /// <summary> /// 通过权限判断渲染用户在前端是否可以输入 /// </summary> /// <param name="userInfo"></param> /// <param name="code"></param> /// <param name="validateInfo"></param> /// <param name="errorMsg"></param> /// <param name="quiFormStyle"></param> /// <param name="modelField"></param> /// <returns></returns> public static string GetAttributesByCode(this BaseUserInfo userInfo, string code, string validateInfo, string errorMsg, string quiFormStyle = null,string modelField=null) { string result = string.Empty; string classInfo = string.Empty; if (!string.IsNullOrWhiteSpace(quiFormStyle)) { classInfo += quiFormStyle; } if (userInfo.IsAuthorizedByCode(code)) { if (!string.IsNullOrWhiteSpace(validateInfo)) { classInfo += " " + validateInfo; } result = " class=\"" + classInfo + "\" "; ; if (!string.IsNullOrWhiteSpace(errorMsg)) { result += " error=\"" + errorMsg + "\" "; } } else { result = " class=\"" + classInfo + "\" "; result += " disabled=\"disabled\" "; ; } if (!string.IsNullOrWhiteSpace(modelField)) { code = modelField; } return result += " name=\"" + code + "\" "; ; } /// <summary> /// 通过权限判断渲染用户在前端是否可以输入 /// </summary> /// <param name="userInfo"></param> /// <param name="code"></param> /// <param name="required"></param> /// <param name="errorMsg"></param> /// <returns></returns> public static string GetAttributesByCode(this BaseUserInfo userInfo, string code, bool required, string errorMsg, string modelField = null) { string result = string.Empty; if (userInfo.IsAuthorizedByCode(code)) { if (required) { result += " class=\"validate[required]\" "; } if (required && !string.IsNullOrWhiteSpace(errorMsg)) { result += " error=\"" + errorMsg + "\" "; } } else { result += " disabled=\"disabled\" "; ; } if (!string.IsNullOrWhiteSpace(modelField)) { code = modelField; } return result += " name=\"" + code + "\" "; ; } } }
前台调用权限判断实现输入项验证
<input <%:Html.Raw(Utils.UserInfo.GetAttributesByCode("siteAudit.UserRealName",true,"请输入真实姓名")) %> maxlength="20" type="text" style="width: 150px;" value="<%: siteAudit.UserRealName %>" />
如果没有权限,输入会增加disabled属性。
单据处理过程中调用的底层工作流的方法
//----------------------------------------------------------------- // All Rights Reserved , Copyright (C) 2016 , Hairihan TECH, Ltd. //----------------------------------------------------------------- using System; namespace DotNet.Business { using DotNet.IService; using DotNet.Model; using DotNet.Utilities; /// <summary> /// BaseWorkFlowCurrentManager /// 流程管理. /// /// 修改记录 /// /// 2012.04.04 版本:1.0 JiRiGaLa 。 /// /// <author> /// <name>JiRiGaLa</name> /// <date>2012.04.04</date> /// </author> /// </summary> public partial class BaseWorkFlowCurrentManager : BaseManager, IBaseManager { /// <summary> /// (点批量通过时)当批量审核通过时 /// </summary> /// <param name="currentIds">审批流当前主键数组</param> /// <param name="auditIdea">批示</param> /// <returns>成功失败</returns> public int AutoAuditPass(string[] currentIds, string auditIdea) { int result = 0; for (int i = 0; i < currentIds.Length; i++) { result += this.AutoAuditPass(currentIds[i], auditIdea); } return result; } /// <summary> /// 审批通过 /// </summary> /// <param name="currentId"></param> /// <param name="auditIdea"></param> /// <returns></returns> public int AutoAuditPass(string currentId, string auditIdea) { IWorkFlowManager workFlowManager = this.GetWorkFlowManager(currentId); return AutoAuditPass(workFlowManager, currentId, auditIdea); } /// <summary> /// (点通过时)当审核通过时 /// </summary> /// <param name="workFlowManager"></param> /// <param name="currentId">审批流当前主键</param> /// <param name="auditIdea">批示</param> /// <returns>成功失败</returns> public int AutoAuditPass(IWorkFlowManager workFlowManager, string currentId, string auditIdea) { int result = 0; // 这里要加锁,防止并发提交 // 这里用锁的机制,提高并发控制能力 lock (WorkFlowCurrentLock) { // using (TransactionScope transactionScope = new TransactionScope()) //{ //try //{ // 1. 先获得现在的状态?当前的工作流主键、当前的审核步骤主键? BaseWorkFlowCurrentEntity workFlowCurrentEntity = this.GetObject(currentId); // 只有待审核状态的,才可以通过,被退回的也可以重新提交 if (!(workFlowCurrentEntity.AuditStatus.Equals(AuditStatus.StartAudit.ToString()) || workFlowCurrentEntity.AuditStatus.Equals(AuditStatus.AuditPass.ToString()) || workFlowCurrentEntity.AuditStatus.Equals(AuditStatus.WaitForAudit.ToString()) || workFlowCurrentEntity.AuditStatus.Equals(AuditStatus.AuditReject.ToString()) )) { return result; } // 是不是给当前人审核的,或者当前人在委托的人? if (!string.IsNullOrEmpty(workFlowCurrentEntity.ToUserId)) { if (!(workFlowCurrentEntity.ToUserId.ToString().Equals(this.UserInfo.Id) || workFlowCurrentEntity.ToUserId.IndexOf(this.UserInfo.Id) >= 0 // || workFlowCurrentEntity.ToUserId.ToString().Equals(this.UserInfo.TargetUserId) )) { return result; } } // 获取下一步是谁审核。 BaseWorkFlowStepEntity workFlowStepEntity = this.GetNextWorkFlowStep(workFlowCurrentEntity); // 3. 进行下一步流转?转给角色?还是传给用户? if (workFlowStepEntity == null || workFlowStepEntity.Id == null) { // 4. 若没下一步了,那就得结束流程了?审核结束了 result = this.AuditComplete(workFlowManager, currentId, auditIdea); } else { // 审核进入下一步 // 当前是哪个步骤? // 4. 是否已经在工作流里了? // 5. 若已经在工作流里了,那就进行更新操作? if (!string.IsNullOrEmpty(workFlowStepEntity.AuditUserId)) { // 若是任意人可以审核的,需要进行一次人工选任的工作 if (workFlowStepEntity.AuditUserId.Equals("Anyone")) { return result; } } // 按用户审核,审核通过 result = AuditPass(workFlowManager, currentId, auditIdea, workFlowStepEntity); } //} //catch (System.Exception ex) //{ // 在本地记录异常 // FileUtil.WriteException(UserInfo, ex); //} //finally //{ //} // transactionScope.Complete(); //} } return result; } #region public int AuditPass(IWorkFlowManager workFlowManager, string currentId, string auditIdea, BaseWorkFlowStepEntity workFlowStepEntity) 审核通过 /// <summary> /// 审核通过 /// </summary> /// <param name="id">当前主键</param> /// <param name="auditIdea">批示</param> /// <returns>影响行数</returns> public int AuditPass(IWorkFlowManager workFlowManager, string currentId, string auditIdea, BaseWorkFlowStepEntity workFlowStepEntity) { int result = 0; // 进行更新操作 result = this.StepAuditPass(currentId, auditIdea, workFlowStepEntity); if (result == 0) { // 数据可能被删除 this.StatusCode = Status.ErrorDeleted.ToString(); } BaseWorkFlowCurrentEntity workFlowCurrentEntity = this.GetObject(currentId); // 发送提醒信息 if (workFlowManager != null) { if (!string.IsNullOrEmpty(workFlowStepEntity.AuditUserId)) { workFlowStepEntity.AuditDepartmentId = null; workFlowStepEntity.AuditRoleId = null; } workFlowManager.OnAutoAuditPass(workFlowCurrentEntity); workFlowManager.SendRemindMessage(workFlowCurrentEntity, AuditStatus.AuditPass, new string[] { workFlowCurrentEntity.CreateUserId, workFlowStepEntity.AuditUserId }, workFlowStepEntity.AuditDepartmentId, workFlowStepEntity.AuditRoleId); } this.StatusMessage = this.GetStateMessage(this.StatusCode); return result; } #endregion #region private int StepAuditPass(string currentId, string auditIdea, BaseWorkFlowStepEntity toStepEntity, BaseWorkFlowAuditInfo workFlowAuditInfo = null) 审核通过(不需要再发给别人了是完成审批了) /// <summary> /// 审核通过(不需要再发给别人了是完成审批了) /// </summary> /// <param name="currentId">当前主键</param> /// <param name="auditIdea">批示</param> /// <param name="toStepEntity">审核到第几步</param> /// <param name="workFlowAuditInfo">当前审核人信息</param> /// <returns>影响行数</returns> private int StepAuditPass(string currentId, string auditIdea, BaseWorkFlowStepEntity toStepEntity, BaseWorkFlowAuditInfo workFlowAuditInfo = null) { BaseWorkFlowCurrentEntity workFlowCurrentEntity = this.GetObject(currentId); // 初始化审核信息,这里是显示当前审核人,可以不是当前操作员的功能 if (workFlowAuditInfo == null) { workFlowAuditInfo = new BaseWorkFlowAuditInfo(this.UserInfo); workFlowAuditInfo.AuditIdea = auditIdea; workFlowAuditInfo.AuditDate = DateTime.Now; workFlowAuditInfo.AuditUserId = this.UserInfo.Id; workFlowAuditInfo.AuditUserRealName = this.UserInfo.RealName; workFlowAuditInfo.AuditStatus = AuditStatus.AuditPass.ToString(); workFlowAuditInfo.AuditStatusName = AuditStatus.AuditPass.ToDescription(); } else { workFlowAuditInfo.AuditIdea = auditIdea; } // 审核意见是什么? workFlowCurrentEntity.AuditIdea = workFlowAuditInfo.AuditIdea; // 1.记录当前的审核时间、审核人信息 // 什么时间审核的? workFlowCurrentEntity.AuditDate = workFlowAuditInfo.AuditDate; // 审核的用户是谁? workFlowCurrentEntity.AuditUserId = workFlowAuditInfo.AuditUserId; // 审核人的姓名是谁? workFlowCurrentEntity.AuditUserRealName = workFlowAuditInfo.AuditUserRealName; // 审核状态是什么? workFlowCurrentEntity.AuditStatus = workFlowAuditInfo.AuditStatus; // 审核状态备注是什么? workFlowCurrentEntity.AuditStatusName = workFlowAuditInfo.AuditStatusName; if (!string.IsNullOrEmpty(workFlowCurrentEntity.ActivityFullName)) { workFlowCurrentEntity.Description = string.Format("从{0}提交到{1}{2}", workFlowCurrentEntity.ActivityFullName, toStepEntity.FullName, !string.IsNullOrEmpty(toStepEntity.Description) ? "," + toStepEntity.Description : string.Empty); } workFlowCurrentEntity.ToUserId = toStepEntity.AuditUserId; workFlowCurrentEntity.ToUserRealName = toStepEntity.AuditUserRealName; workFlowCurrentEntity.ToRoleId = toStepEntity.AuditRoleId; workFlowCurrentEntity.ToRoleRealName = toStepEntity.AuditRoleRealName; workFlowCurrentEntity.ToDepartmentId = toStepEntity.AuditDepartmentId; workFlowCurrentEntity.ToDepartmentName = toStepEntity.AuditDepartmentName; // 2.记录审核日志 this.AddHistory(workFlowCurrentEntity); // 3.上一个审核结束了,新的审核又开始了,更新待审核情况 workFlowCurrentEntity.ActivityId = toStepEntity.ActivityId; workFlowCurrentEntity.ActivityCode = toStepEntity.Code; workFlowCurrentEntity.ActivityFullName = toStepEntity.FullName; workFlowCurrentEntity.ActivityType = toStepEntity.ActivityType; workFlowCurrentEntity.SortCode = toStepEntity.SortCode; return this.UpdateObject(workFlowCurrentEntity); } #endregion } }
审核需要实现的工作流接口
//----------------------------------------------------------------- // All Rights Reserved , Copyright (C) 2016 , Hairihan TECH, Ltd. //----------------------------------------------------------------- namespace DotNet.IService { using DotNet.Model; using DotNet.Utilities; /// <summary> /// IWorkFlowManager /// 可审批化的类接口定义 /// /// 修改记录 /// /// 2011.09.06 版本:1.0 JiRiGaLa 创建文件。 /// /// <author> /// <name>JiRiGaLa</name> /// <date>2011.09.06</date> /// </author> /// </summary> public interface IWorkFlowManager { string CurrentTableName { get; set; } IDbHelper GetDbHelper(); BaseUserInfo GetUserInfo(); void SetUserInfo(BaseUserInfo userInfo); /// <summary> /// 获取待审核单据的网址 /// </summary> /// <param name="currentId">工作流当前主键</param> /// <returns>获取网址</returns> string GetUrl(string currentId); /// <summary> /// 发送即时通讯提醒 /// </summary> /// <param name="workFlowCurrentEntity">当前审核流实体信息</param> /// <param name="auditStatus">审核状态</param> /// <param name="userIds">发送给用户主键数组</param> /// <param name="organizeId">发送给部门主键数组</param> /// <param name="roleId">发送给角色主键数组</param> /// <returns>影响行数</returns> int SendRemindMessage(BaseWorkFlowCurrentEntity entity, AuditStatus auditStatus, string[] userIds, string organizeId, string roleId); /// <summary> /// 当工作流开始启动前需要做的工作 /// </summary> /// <param name="workFlowAuditInfo">审核信息</param> /// <returns>成功失败</returns> bool BeforeAutoStatr(BaseWorkFlowAuditInfo workFlowAuditInfo); /// <summary> /// 当工作流开始启动之后需要做的工作 /// </summary> /// <param name="workFlowAuditInfo">审核信息</param> /// <returns>成功失败</returns> bool AfterAutoStatr(BaseWorkFlowAuditInfo workFlowAuditInfo); /// <summary> /// (点通过时)当审核通过时 /// </summary> /// <param name="entity">审批流当前信息</param> /// <returns>成功失败</returns> bool OnAutoAuditPass(BaseWorkFlowCurrentEntity entity); /// <summary> /// (点退回时)当审核退回时 /// </summary> /// <param name="entity">审批流当前信息</param> /// <returns>成功失败</returns> bool OnAutoAuditReject(BaseWorkFlowCurrentEntity entity); /// <summary> /// (点完成时)当审核完成时 /// </summary> /// <param name="entity">审批流当前信息</param> /// <returns>成功失败</returns> bool OnAutoAuditComplete(BaseWorkFlowCurrentEntity entity); // ====================================== // // 下面是用户自己提交单据审核时发生的事件 // // ====================================== // /// <summary> /// 废弃单据 /// (废弃单据时)当废弃审批流时需要做的事情 /// </summary> /// <param name="id">主键</param> /// <param name="auditIdea">批示</param> /// <returns>影响行数</returns> int AuditQuash(string id, string auditIdea); /// <summary> /// 批量废弃单据 /// (批量废弃单据时)当废弃审批流时需要做的事情 /// </summary> /// <param name="ids">主键数组</param> /// <param name="auditIdea">批示</param> /// <returns>影响行数</returns> int AuditQuash(string[] ids, string auditIdea); // ====================================== // // 下面是用户被审核单据被审核时发生的事件 // // ====================================== // /// <summary> /// (点退回时)当审核退回时 /// </summary> /// <param name="entity">当前审批流程</param> /// <returns>成功失败</returns> bool OnAuditReject(BaseWorkFlowCurrentEntity entity); /// <summary> /// 废弃单据 /// (废弃单据时)当废弃审批流时需要做的事情 /// </summary> /// <param name="entity">当前审批流程</param> /// <returns>影响行数</returns> bool OnAuditQuash(BaseWorkFlowCurrentEntity entity); /// <summary> /// 流程完成时 /// 结束审核时,需要回调写入到表里,调用相应的事件 /// 若成功可以执行完成的处理 /// </summary> /// <param name="entity">当前审批流程</param> /// <returns>成功失败</returns> bool OnAuditComplete(BaseWorkFlowCurrentEntity entity); // (退回到某一节点时) 被退回到某个节点 // 当有人评论时的功能实现 /// <summary> /// 重置单据 /// (单据发生错误时)紧急情况下实用 /// </summary> /// <param name="entity">当前审批流程</param> /// <returns>影响行数</returns> bool OnReset(BaseWorkFlowCurrentEntity entity); } }
通过这次公司新开审批流程的开发,让我比较全面的了解了审批业务流程开发的思路,了解到了其核心的实现原理,如果没有权限底层及审批流组件的支持,这么短时间,没有任何经验的情况下,完成审批功能的开发是不可想象的,今后我会逐步完善这个审批组件,实现B/S化,通过界面拖动完成审批流程的创建。