开篇前的废话:工作流是我们在做互联网应用开发时经常需要用到的一种技术,复杂的工作流我们基本是借助一些开源的 工作流项目来做,比如 ccflow等,但是有时候,我们只需要实现一些简单的工作流流程,这时候用 ccflow等就显得杀鸡用牛刀了,这时候我们就得自己写一个简单的工作流的流程了,一个简单的工作流的实现,如果没有自己动手做过,单凭看别人的博客是很难理解的,我就曾在这个问题上掉进大坑。下面把我对简单工作流的实现简单的记录一下。

说明:因为工作流是涉及一个任务请求在多个人之间流转的业务流程,所以我们本篇博客的实现需要立足在一个至少有三个用户的项目基础上的,这里我不会从最基础的部分开始做,那样将耗费太多时间,过程也很麻烦。我将在我手上一个现成的项目上实现这个流程,所以你是个比我还菜的菜鸟,可能会看懵,如果我没让你看明白,请不要骂我,因为我也很菜。我会尽量把过程写的清楚明白一些,尽可能让看的人都能看明白。

  • 业务描述

本篇我将写一个简单的工作流流程,用来实现一个公司员工的请假流程,简单来说,可以用下图来描述:

这是一个简单且常用的一个工作流程,需要三个用户,分别扮演三种角色,普通员工、部门经理和总经理。

  • 数据库设计

工作流的数据库设计主要涉及 4 张表,其中一张是用来存储我们的请假申请的内容(比如:请假时长、请假原因、收假时间等),另外三张表则主要实现工作流程的核心;

如图所示:数据库的另外三张表分别为 流程实例表,流程节点表,流程流转记录表,

它们的作用分别是:

流程节点表:该表定义一个流程有几个节点,每个节点在流程中的位置如何,他的前一个节点是谁,后一个节点是谁,该节点由谁来操作等等;比如,一个流程从发起到完成审批共需要经手3个人,则就有3个节点。示例:

这个流程从发起到审批完成有3个人经手,所以在这里添加三个节点,注明每个节点之间的关系;后期,如果某个节点的人审批过了,则通过查找这张表,来寻找它的上一节点或下一节点,然后通过改变节点的值,使流程向下一个节点流转;

流程实例表:流程实例表是工作流的核心,在请假单提交时,同步新建一个 流程实例 ,这个流程实例表就相当于是每个节点之间的一个纽带,上一个人操作过后,就把当前节点的编号由当前节点改为下一节点,就这样,每个人操作过后,就改一个该表中的节点编号,这样就可以实现流程在每个节点之间传递、移动;

流程流转记录表:每个人对流程进行操作后,同步在该表中创建一个操作记录,记录是谁操作的,操作结果如何等等;

以下列出数据实体:

Request.cs

namespace Modules.Wflow
{
/// <summary>
/// 请假申请
/// </summary>
[Table("LeaveRequest")]
public class LeaveRequest
{
[Key]
public Guid Id { get; set; } /// <summary>
/// 请假原因
/// </summary>
[MaxLength()]
public string Reason { get; set; } /// <summary>
/// 请假时长
/// </summary>
public string Duration { get; set; } /// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
}
}

Node.cs

namespace Modules.Wflow
{
/// <summary>
/// 节点表
/// </summary>
[Table("WF_Node")]
public class Node
{
public Node()
{
IsDelete = false;
} [Key]
public Guid Id { get; set; } /// <summary>
/// 节点编号
/// </summary>
[MaxLength()]
[Required]
public string NodeSN { get; set; } /// <summary>
/// 节点名称
/// </summary>
[MaxLength()]
[Required]
public string NodeName { get; set; } ///// <summary>
///// 流程id
///// </summary>
//public Guid? FlowId { get; set; } //[MaxLength(100)]
///// <summary>
///// 流程名称
///// </summary>
//public string FlowName { get; set; } /// <summary>
/// 执行人Id
/// </summary>
public Guid? OperatorId { get; set; } /// <summary>
///执行人名称
/// </summary>
public string Operator { get; set; } /// <summary>
/// 下一节点编号
/// </summary>
public string NextNodeSN { get; set; } /// <summary>
/// 上一节点编号(退回节点)
/// </summary>
public string LastNodeSN { get; set; } public DateTime CreateTime { get; set; } /// <summary>
/// 是否删除
/// </summary>
public bool IsDelete { get; set; }
}
}

FlowInstance .cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace Modules.Wflow
{
/// <summary>
/// 流程实例表
/// </summary>
[Table("WF_FlowInstance")]
public class FlowInstance
{
[Key]
public Guid Id { get; set; } /// <summary>
/// 当前节点
/// </summary>
[MaxLength()]
[Required]
public string NodeSN { get; set; } /// <summary>
/// 节点名称
/// </summary>
[MaxLength()]
public string NodeName { get; set; } /// <summary>
/// 流状态
/// </summary>
[MaxLength()]
public string WFStatus { get; set; } /// <summary>
/// 流程发起人userId
/// </summary>
public Guid? StarterId { get; set; } /// <summary>
/// 流程发起人姓名
/// </summary>
public string Starter { get; set; } /// <summary>
/// 当前操作人userId
/// </summary>
public Guid? OperatorId { get; set; } /// <summary>
/// 当前操作人姓名
/// </summary>
public string Operator { get; set; } /// <summary>
/// 待办人Id
/// </summary>
public Guid? ToDoerId { get; set; } /// <summary>
/// 待办人名称
/// </summary>
public string ToDoer { get; set; } /// <summary>
/// 已操作人
/// </summary>
[MaxLength()]
public string Operated { get; set; } /// <summary>
/// 流程创建时间
/// </summary>
public DateTime CreateTime { get; set; } /// <summary>
/// 流程更新时间
/// </summary>
public DateTime UpdateTime { get; set; } /// <summary>
/// 申请单Id
/// </summary>
public Guid? RequisitionId { get; set; } }
}

FlowRecord.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace Modules.Wflow
{
/// <summary>
/// 流程记录表
/// </summary>
[Table("WF_FlowRecord")]
public class FlowRecord
{
[Key]
public Guid Id { get; set; } /// <summary>
/// 流程实例Id
/// </summary>
public Guid? WorkId { get; set; } /// <summary>
/// 当前节点编号
/// </summary>
[MaxLength()]
public string CurrentNodeSN { get; set; } /// <summary>
/// 当前节点名称
/// </summary>
[MaxLength()]
public string CurrentNode { get; set; } /// <summary>
/// 操作人Id
/// </summary>
public Guid? OperatorId { get; set; } /// <summary>
/// 操作人名称
/// </summary>
[MaxLength()]
public string Operator { get; set; } /// <summary>
/// 更新时间
/// </summary>
public DateTime UpdateTime { get; set; } /// <summary>
/// 是否读取
/// </summary>
public bool IsRead { get; set; } /// <summary>
/// 是否通过
/// </summary>
public bool IsPass { get; set; } }
}
  • 工作流业务代码实现

业务代码主要以流程的起始节点和第二个节点为例进行说明。

首先,我在项目中添加了三个人员,分别是 张三(总经理)、李四(部门经理)、王五(普通员工)

然后,分别创建对应的控制器和前端的菜单等,如下图:

这里我主要针对新建申请和待办审批两个功能的实现进行编码:

(1)新建申请:

如图,这是我创建的一个请假单,当该请假提交后,会进行如下几项处理:

(1)保存请假单到数据库;

(2)创建工作流实例,给该实例的各项赋值;具体赋值为:

当前节点(NodeSN):赋值为该流程的第一个节点(101);

流程发起人(Starter):赋值为当前用户的用户名(王五);

当前操作人(Operator): 赋值为当前用户的用户名(王五);

待办人(ToDoer):赋值待办人(通过节点表查询待办人是谁)(此处应为李四)(下一个人会根据待办人的值来查找自己是否有待办事项);

请假单Id(RequisitionId):赋值为刚保存的请假单的id(刚提交的申请单ID);

(3)创建工作流操作记录,具体赋值为:

流程实例Id:赋值为(2)中创建的流程实例的Id;

当前处理人:同(2)中;

当前节点:同(2)中;

是否已读和通过:这个值在流程发起节点是不需要写的,或者写 true;

该部分代码如下:

 /// <summary>
/// 保存申请单并提交工作流
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public ActionResult Save(LeaveRequest request)
{
try
{
request.CreateTime = DateTime.Now;
//1.保存请假单
_context.LeaveRequests.AddOrUpdate(request); //2.创建工作流
var flow = new FlowInstance { Id = Guid.NewGuid()};
//当前登录人员信息
var userInfo = GetEmployeeInfo(CacheUtil.LoginUser.Id);
//工作流当前节点
flow.NodeSN = _context.Nodes.FirstOrDefault(x => x.NodeName.Equals("发起申请")).NodeSN;
flow.NodeName = "发起申请";
//申请处理状态
flow.WFStatus = "已申请";
//申请人(流程发起人)
flow.StarterId = userInfo.Id;
flow.Starter = userInfo.FullName;
//当前操作者
flow.OperatorId = userInfo.Id;
flow.Operator = userInfo.FullName;
//下一个节点处理人
flow.ToDoerId = _context.Nodes.FirstOrDefault(x => x.NodeName.Equals("部门经理审批")).OperatorId;
flow.ToDoer = _context.Nodes.FirstOrDefault(x => x.NodeName.Equals("部门经理审批")).Operator;
//申请单ID
flow.RequisitionId = request.Id;
flow.UpdateTime = DateTime.Now;
flow.CreateTime = DateTime.Now;
//已操作过的人
flow.Operated = userInfo.Id.ToString();
_context.FlowInstances.AddOrUpdate(flow); //3.新建流操作记录
var flowRecord = new FlowRecord { Id = Guid.NewGuid() };
//流程实例Id
flowRecord.WorkId = flow.Id;
//当前处理人
flowRecord.Operator = userInfo.FullName;
flowRecord.OperatorId = userInfo.Id;
//当前节点
flowRecord.CurrentNodeSN = flow.NodeSN;
flowRecord.CurrentNode = flow.NodeName;
//是否已读
flowRecord.IsRead = true;
//是否通过
flowRecord.IsPass = true;
flowRecord.UpdateTime = DateTime.Now;
_context.FlowRecords.Add(flowRecord);
int saveResult = _context.SaveChanges();
if (saveResult > )
{
return Json(new
{
Result = true
});
}
else {
throw new Exception("提交失败");
}
}
catch (Exception exception)
{ return Json(new {
Result = false,
exception.Message
});
}
}

(2)获取待办审批

若按照刚才的操作进行的话,那么则会新建一个工作流实例,该实例存储的数据如下:

同时,在流程记录表中会得到一条记录数据,如下:

接下来,我们来看一下获取待办审批的步骤,首先看下效果:

登录李四的账号:

然后,说明一下获取待办审批的步骤,以及向下一节点流转的步骤:

(1)获取待办审批:根据工作流实例中的 待办人Id 来进行获取,若待办人为当前登录的用户,则获取这个待办事项;

  /// <summary>
/// 获取待办审批
/// </summary>
/// <returns></returns>
public ActionResult GetList(int page)
{
try
{
// var userInfo = GetEmployeeInfo(CacheUtil.LoginUser.Id);
var todo = _context.FlowInstances.Where(x => x.ToDoerId == UserInfo.Id).OrderByDescending(x => x.CreateTime); //此处获取待办人列表,根据待办人Id 等于 当前登录用户Id获取
int count = todo.Count();
var pagedList = todo.ToPage(page, count).ToList();
var todoList = pagedList.Select(x => new
{
x.Id,
x.Starter,//申请人
x.Operator, //上一操作人
UpdateTime = x.UpdateTime.Format("yyyy年MM月dd日 hh:mm"), //更新时间,
x.RequisitionId //对应申请单id
}).ToList();
return Json(new {
Data = todoList,
Result = true,
Count = count
});
}
catch (Exception exception)
{ return Json(new {
Result = false,
exception.Message
});
}
}

(2)若同意,则点击确定,执行相关操作,进行流程流转:

具体步骤为:

1>根据条件获取该工作流实例;

2>新增当前操作人记录:

依次记录 工作流实例Id、当前节点编号、当前操作人、是否通过等信息;

需要注意的是:先新增记录,然后判断记录是否保存成功,如果成功保存,才能执行 流实例 的状态转变操作;

3>改变流实例表的值:

当前操作人:赋值为当前用户;

节点:节点由当前节点变为下一节点;

待办人:根据节点表 获取 待办人 信息;

节点之间的流转其实主要涉及的就是待办人这个值的转换,根据下表,可以清楚看到这个转换:

以上三项最为重要,其他一些需要更新的值再次不列出。

然后看一下代码:

 /// <summary>
/// 同意审批
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ActionResult Agree(Guid id)
{
try
{
var flow = _context.FlowInstances.FirstOrDefault(x => x.RequisitionId == id);//根据申请单号获取flow实例
//记录当前人员操作记录
var flowRecord = new FlowRecord { Id = Guid.NewGuid() };
flowRecord.WorkId = flow.Id;
flowRecord.CurrentNode = flow.NodeName;
flowRecord.CurrentNodeSN = _context.Nodes.FirstOrDefault( x => x.NodeName.Equals(flow.NodeName)).NodeSN;
flowRecord.Operator = UserInfo.FullName;
flowRecord.OperatorId = UserInfo.Id;
flowRecord.UpdateTime = DateTime.Now;
flowRecord.IsRead = true;
flowRecord.IsPass = true;
_context.FlowRecords.Add(flowRecord);
int saveResult = _context.SaveChanges();
if (saveResult > )
{

              //改变流实例的状态,使之流向下一节点
              flow.OperatorId = UserInfo.Id;
              flow.Operator = UserInfo.FullName;
              flow.NodeSN = _context.Nodes.FirstOrDefault(x => x.NodeSN.Equals(flow.NodeSN)).NextNodeSN; //当前操作节点的编号变为下一节点的编号
              var nextNode = _context.Nodes.FirstOrDefault(x => x.NodeSN.Equals(flow.NodeSN)).NextNodeSN;
              flow.NodeName = _context.Nodes.FirstOrDefault( x => x.NodeSN.Equals(flow.NodeSN)).NodeName;
              flow.WFStatus = "已同意";
              flow.ToDoerId = _context.Nodes.FirstOrDefault(x => x.NodeSN.Equals(nextNode)).OperatorId;
              flow.ToDoer = _context.Nodes.FirstOrDefault(x => x.NodeSN.Equals(nextNode)).Operator;
              flow.UpdateTime = DateTime.Now;
              flow.Operated = flow.Operated + '@' + UserInfo.Id.ToString();
              _context.FlowInstances.AddOrUpdate(flow);

int i = _context.SaveChanges();
if (i > )
{
return Json(new
{
Result = true
});
}
else
{
throw new Exception("提交失败");
} } else
{
throw new Exception("提交失败"); }
}
catch (Exception exception)
{ return Json(new {
Result = false,
exception.Message
});
}
}

看一下数据库:

实例表:

记录表:

然后看一下现象:

仍登录李四账号,发现,李四的待办事项里已经没有数据了;

然后,登录张三的账号,查看其待办事项:

发现刚才的流程已经流转到张三的账号里了。

总结一下:工作流的实现思路,只是干巴巴的看的话,觉得挺复杂,挺难以捉摸的,但是实际操作一遍,发现也并不难,关键就在于上面三张表的设计以及流程流转时,各个值的状态应该如何转变,由此我也得到一点感悟,看一千遍不如动手做一遍,在学习的过程中,实践真的很重要,切忌纸上谈兵,脱离现实。

以上代码尚不完善,也没有经过测试,可能存在一些bug,重点看思路,我很菜,写的也很混乱,觉得写的不好的,请多多指出我的缺点,非常感谢!

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan

.net mvc中一种简单的工作流的设计的更多相关文章

  1. asp.net mvc 中 一种简单的 URL 重写

    asp.net mvc 中 一种简单的 URL 重写 Intro 在项目中想增加一个公告的功能,但是又不想直接用默认带的那种路由,感觉好low逼,想弄成那种伪静态化的路由 (别问我为什么不直接静态化, ...

  2. iOS开发UI篇—iOS开发中三种简单的动画设置

    iOS开发UI篇—iOS开发中三种简单的动画设置 [在ios开发中,动画是廉价的] 一.首尾式动画 代码示例: // beginAnimations表示此后的代码要“参与到”动画中 [UIView b ...

  3. MVC中几种常用ActionResult

    一.定义 MVC中ActionResult是Action的返回结果.ActionResult 有多个派生类,每个子类功能均不同,并不是所有的子类都需要返回视图View,有些直接返回流,有些返回字符串等 ...

  4. [转]MVC中几种常用ActionResult

    本文转自:http://www.cnblogs.com/xielong/p/5940535.html 一.定义 MVC中ActionResult是Action的返回结果.ActionResult 有多 ...

  5. MVC中几种常用的ActionResult

    一.定义 MVC中ActionResult是Action的返回结果.ActionResult 有多个派生类,每个子类功能均不同,并不是所有的子类都需要返回视图View,有些直接返回流,有些返回字符串等 ...

  6. asp.net -mvc框架复习(5)-ASP.NET MVC中的视图简单使用

    1.视图分类 ASPX视图(现在讲解) Razor视图(后面讲解) ASPX 视图: 2.@page指令 作用:页面的声明 要求:必须放在第一行,常用指令属性如下: 3.服务器端内嵌语法 小脚本:在A ...

  7. ASP.NET Core MVC 中两种路由的简单配置

    1.全局约定路由 这种方式配置优先级比较低,如果控制器或者方法上标记了特性路由那么优先走特性路由. 当建立好一个mvc项目里,路由都是默认配置好的. 如果建立的是空项目那么需要手动配置: 1.需要在C ...

  8. Java 项目中一种简单的动态修改配置即时生效的方式 WatchService

    这种方式仅适合于比较小的项目,例如只有一两台服务器,而且配置文件是可以直接修改的.例如 Spring mvc 以 war 包的形式部署,可以直接修改resources 中的配置文件.如果是 Sprin ...

  9. @Spring MVC 中几种获取request和response的方式

    1.最简单方式:处理方法入参 例如: @RequestMapping("/test") @ResponseBody public void saveTest(HttpServlet ...

随机推荐

  1. 性能測试JMeter趟的坑之JMeter的bug:TPS周期性波动问题

    先说下问题: 我在做性能測试时,使用JMeter搞了100个并发,以100TPS的压力压測十分钟,但压力一直出现波动.并且出现波动时JMeter十分卡,例如以下图: 周期性TPS波动 各种猜測: 所以 ...

  2. UI_Target/action 设计模式

    RootView.m 中 UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; button.frame = CGRectM ...

  3. 【转载】一分钟了解两阶段提交2PC(运营MM也懂了)

    上一期分享了"一分钟了解mongoDB"[回复"mongo"阅读],本期将分享分布式事务的一种实现方式2PC. 一.概念 二阶段提交2PC(Two phase ...

  4. 【转载】Http协议与TCP协议简单理解

    在C#编写代码,很多时候会遇到Http协议或者TCP协议,这里做一个简单的理解.TCP协议对应于传输层,而HTTP协议对应于应用层,从本质上来说,二者没有可比性.Http协议是建立在TCP协议基础之上 ...

  5. Arcgis Engine(ae)接口详解(3):featureClass的feature编辑和删除

    //由于测试数据不完善,featureClass在此要只设null值,真实功能要设实际的值 IFeatureClass featureClass = null; //获取某个字段的索引,后面取字段值用 ...

  6. Cannot instantiate the type AppiumDriver,AppiumDriver升级引发的问题

    转自:http://blog.csdn.net/zhubaitian/article/details/39717889 1. 问题描述和起因 在使用Appium1.7.0及其以下版本的时候,我们可以直 ...

  7. Hadoop 0.20.2+Ubuntu13.04配置和WordCount測试

    事实上这篇博客写的有些晚了.之前做过一些总结后来学校的事给忘了,这几天想又一次拿来玩玩发现有的东西记不住了.翻博客发现居然没有.好吧,所以赶紧写一份留着自己用吧.这东西网上有非常多,只是也不是全然适用 ...

  8. UIPanGestureRecognizer上下左右滑动方向推断算法

    CGFloat const gestureMinimumTranslation = 20.0; typedef enum :NSInteger { kCameraMoveDirectionNone, ...

  9. java的gradle项目的基本配置

    plugins { id 'org.springframework.boot' version '2.1.4.RELEASE' id 'java' } apply plugin: 'io.spring ...

  10. WinDbg调试高内存的.Net进程Dump

    WinDbg的学习路径,艰难曲折,多次研究进展不多,今日有所进展,记录下来. 微软官方帮助文档非常全面:https://msdn.microsoft.com/zh-cn/library/windows ...