前言

ABP目前已经是很成熟的开发框架了,它提供了很多我们日常开发所必须的功能,并且很方便扩展,让我们能更专注于业务的开发。但是ABP官方并没有给我们实现工作流。

在.net core环境下的开源工作流引擎很少,其中WorkflowCore是一款轻量级工作流引擎,对于小型工作流和责任链类型的需求开发很适合,但只能通过后台编码或者json的方式定义工作流程,看了源码后觉得扩展性还是挺好的,至少能满足我的需求,于是选择对它下手。

jsPlumb是一个开源的比较强大的绘图组件,这里不多介绍,我就是用它实现一个简单的流程设计器。

花了差不多一个月的时间,把这三者结合到一起实现一个简单而强大的工作流模块。

目录

  1.  ABP模块实现WorkflowCore持久化存储接口(IPersistenceProvider)
  2. ABP中AbpWorkflow和AbpStepBody的自定义注册
  3. 设计器实现
  4. 设计器提交的流程数据转换成WorkflowCore支持的Json数据结构
  5. 总结

1.ABP模块实现WorkflowCore持久化存储接口(IPersistenceProvider)

这里我参考了WorkflowCore.Persistence.EntityFramework 持久化项目的实现方式 用ABP的方式实现了WorkflowCore的持久化。这样做有两个好处:

1.让工作流能支持ABP的多租户和全局数据过滤功能

2.数据库操作能使用统一的数据上下文,方便事务提交和回滚。

  • ABP实现的流程Workflow持久化存储所必须的实体类,其中PersistedWorkflowDefinition是用来持久化存储流程定义(在Workflow中流程定义在内存中)如下图:

  • 实现IPersistenceProvider接口

  1 public interface IAbpPersistenceProvider : IPersistenceProvider
2 {
3 Task<PersistedWorkflow> GetPersistedWorkflow(Guid id);
4
5 Task<PersistedExecutionPointer> GetPersistedExecutionPointer(string id);
6 Task<PersistedWorkflowDefinition> GetPersistedWorkflowDefinition(string id, int version);
7 }
8
9
10 public class AbpPersistenceProvider : DomainService, IAbpPersistenceProvider
11 {
12 protected readonly IRepository<PersistedEvent, Guid> _eventRepository;
13 protected readonly IRepository<PersistedExecutionPointer, string> _executionPointerRepository;
14 protected readonly IRepository<PersistedWorkflow, Guid> _workflowRepository;
15 protected readonly IRepository<PersistedWorkflowDefinition, string > _workflowDefinitionRepository;
16 protected readonly IRepository<PersistedSubscription, Guid> _eventSubscriptionRepository;
17 protected readonly IRepository<PersistedExecutionError, Guid> _executionErrorRepository;
18 protected readonly IGuidGenerator _guidGenerator;
19 protected readonly IAsyncQueryableExecuter _asyncQueryableExecuter;
20 public IAbpSession AbpSession { get; set; }
21
22
23 public AbpPersistenceProvider(IRepository<PersistedEvent, Guid> eventRepository, IRepository<PersistedExecutionPointer, string> executionPointerRepository, IRepository<PersistedWorkflow, Guid> workflowRepository, IRepository<PersistedSubscription, Guid> eventSubscriptionRepository, IGuidGenerator guidGenerator, IAsyncQueryableExecuter asyncQueryableExecuter, IRepository<PersistedExecutionError, Guid> executionErrorRepository, IRepository<PersistedWorkflowDefinition, string > workflowDefinitionRepository)
24 {
25
26 _eventRepository = eventRepository;
27 _executionPointerRepository = executionPointerRepository;
28 _workflowRepository = workflowRepository;
29 _eventSubscriptionRepository = eventSubscriptionRepository;
30 _guidGenerator = guidGenerator;
31 _asyncQueryableExecuter = asyncQueryableExecuter;
32 _executionErrorRepository = executionErrorRepository;
33 _workflowDefinitionRepository = workflowDefinitionRepository;
34
35
36 }
37 [UnitOfWork]
38 public virtual async Task<string> CreateEventSubscription(EventSubscription subscription)
39 {
40
41 subscription.Id = _guidGenerator.Create().ToString();
42 var persistable = subscription.ToPersistable();
43 await _eventSubscriptionRepository.InsertAsync(persistable);
44 return subscription.Id;
45 }
46 [UnitOfWork]
47 public virtual async Task<string> CreateNewWorkflow(WorkflowInstance workflow)
48 {
49 workflow.Id = _guidGenerator.Create().ToString();
50 var persistable = workflow.ToPersistable();
51 if (AbpSession.UserId.HasValue)
52 {
53 var userCache = AbpSession.GetCurrentUser();
54 persistable.CreateUserIdentityName = userCache.FullName;
55 }
56 await _workflowRepository.InsertAsync(persistable);
57 return workflow.Id;
58 }
59 [UnitOfWork]
60 public virtual async Task<IEnumerable<string>> GetRunnableInstances(DateTime asAt)
61 {
62 var now = asAt.ToUniversalTime().Ticks;
63
64 var query = _workflowRepository.GetAll().Where(x => x.NextExecution.HasValue && (x.NextExecution <= now) && (x.Status == WorkflowStatus.Runnable))
65 .Select(x => x.Id);
66 var raw = await _asyncQueryableExecuter.ToListAsync(query);
67
68 return raw.Select(s => s.ToString()).ToList();
69 }
70 [UnitOfWork]
71 public virtual async Task<IEnumerable<WorkflowInstance>> GetWorkflowInstances(WorkflowStatus? status, string type, DateTime? createdFrom, DateTime? createdTo, int skip, int take)
72 {
73
74 IQueryable<PersistedWorkflow> query = _workflowRepository.GetAll()
75 .Include(wf => wf.ExecutionPointers)
76 .ThenInclude(ep => ep.ExtensionAttributes)
77 .Include(wf => wf.ExecutionPointers)
78 .AsQueryable();
79
80 if (status.HasValue)
81 query = query.Where(x => x.Status == status.Value);
82
83 if (!String.IsNullOrEmpty(type))
84 query = query.Where(x => x.WorkflowDefinitionId == type);
85
86 if (createdFrom.HasValue)
87 query = query.Where(x => x.CreateTime >= createdFrom.Value);
88
89 if (createdTo.HasValue)
90 query = query.Where(x => x.CreateTime <= createdTo.Value);
91
92 var rawResult = await query.Skip(skip).Take(take).ToListAsync();
93 List<WorkflowInstance> result = new List<WorkflowInstance>();
94
95 foreach (var item in rawResult)
96 result.Add(item.ToWorkflowInstance());
97
98 return result;
99
100 }
101 [UnitOfWork]
102 public virtual async Task<WorkflowInstance> GetWorkflowInstance(string Id)
103 {
104
105 var uid = new Guid(Id);
106 var raw = await _workflowRepository.GetAll()
107 .Include(wf => wf.ExecutionPointers)
108 .ThenInclude(ep => ep.ExtensionAttributes)
109 .Include(wf => wf.ExecutionPointers)
110 .FirstAsync(x => x.Id == uid);
111
112 if (raw == null)
113 return null;
114
115 return raw.ToWorkflowInstance();
116
117 }
118 [UnitOfWork]
119 public virtual async Task<IEnumerable<WorkflowInstance>> GetWorkflowInstances(IEnumerable<string> ids)
120 {
121 if (ids == null)
122 {
123 return new List<WorkflowInstance>();
124 }
125
126
127 var uids = ids.Select(i => new Guid(i));
128 var raw = _workflowRepository.GetAll()
129 .Include(wf => wf.ExecutionPointers)
130 .ThenInclude(ep => ep.ExtensionAttributes)
131 .Include(wf => wf.ExecutionPointers)
132 .Where(x => uids.Contains(x.Id));
133
134 return (await raw.ToListAsync()).Select(i => i.ToWorkflowInstance());
135
136 }
137 [UnitOfWork]
138 public virtual async Task PersistWorkflow(WorkflowInstance workflow)
139 {
140
141 var uid = new Guid(workflow.Id);
142 var existingEntity = await _workflowRepository.GetAll()
143 .Where(x => x.Id == uid)
144 .Include(wf => wf.ExecutionPointers)
145 .ThenInclude(ep => ep.ExtensionAttributes)
146 .Include(wf => wf.ExecutionPointers)
147 .AsTracking()
148 .FirstAsync();
149 var persistable = workflow.ToPersistable(existingEntity);
150 await CurrentUnitOfWork.SaveChangesAsync();
151 }
152 [UnitOfWork]
153 public virtual async Task TerminateSubscription(string eventSubscriptionId)
154 {
155
156 var uid = new Guid(eventSubscriptionId);
157 var existing = await _eventSubscriptionRepository.FirstOrDefaultAsync(x => x.Id == uid);
158 _eventSubscriptionRepository.Delete(existing);
159 await CurrentUnitOfWork.SaveChangesAsync();
160
161 }
162 [UnitOfWork]
163 public virtual void EnsureStoreExists()
164 {
165
166
167 }
168 [UnitOfWork]
169 public virtual async Task<IEnumerable<EventSubscription>> GetSubscriptions(string eventName, string eventKey, DateTime asOf)
170 {
171
172 asOf = asOf.ToUniversalTime();
173 var raw = await _eventSubscriptionRepository.GetAll()
174 .Where(x => x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf)
175 .ToListAsync();
176
177 return raw.Select(item => item.ToEventSubscription()).ToList();
178
179 }
180 [UnitOfWork]
181 public virtual async Task<string> CreateEvent(Event newEvent)
182 {
183
184 newEvent.Id = _guidGenerator.Create().ToString();
185 var persistable = newEvent.ToPersistable();
186 var result = _eventRepository.InsertAsync(persistable);
187 await CurrentUnitOfWork.SaveChangesAsync();
188 return newEvent.Id;
189 }
190 [UnitOfWork]
191 public virtual async Task<Event> GetEvent(string id)
192 {
193
194 Guid uid = new Guid(id);
195 var raw = await _eventRepository
196 .FirstOrDefaultAsync(x => x.Id == uid);
197
198 if (raw == null)
199 return null;
200
201 return raw.ToEvent();
202
203 }
204 [UnitOfWork]
205 public virtual async Task<IEnumerable<string>> GetRunnableEvents(DateTime asAt)
206 {
207 var now = asAt.ToUniversalTime();
208
209 asAt = asAt.ToUniversalTime();
210 var raw = await _eventRepository.GetAll()
211 .Where(x => !x.IsProcessed)
212 .Where(x => x.EventTime <= now)
213 .Select(x => x.Id)
214 .ToListAsync();
215
216 return raw.Select(s => s.ToString()).ToList();
217
218 }
219 [UnitOfWork]
220 public virtual async Task MarkEventProcessed(string id)
221 {
222
223 var uid = new Guid(id);
224 var existingEntity = await _eventRepository.GetAll()
225 .Where(x => x.Id == uid)
226 .AsTracking()
227 .FirstAsync();
228
229 existingEntity.IsProcessed = true;
230 await CurrentUnitOfWork.SaveChangesAsync();
231 }
232 [UnitOfWork]
233 public virtual async Task<IEnumerable<string>> GetEvents(string eventName, string eventKey, DateTime asOf)
234 {
235
236 var raw = await _eventRepository.GetAll()
237 .Where(x => x.EventName == eventName && x.EventKey == eventKey)
238 .Where(x => x.EventTime >= asOf)
239 .Select(x => x.Id)
240 .ToListAsync();
241
242 var result = new List<string>();
243
244 foreach (var s in raw)
245 result.Add(s.ToString());
246
247 return result;
248
249 }
250 [UnitOfWork]
251 public virtual async Task MarkEventUnprocessed(string id)
252 {
253
254 var uid = new Guid(id);
255 var existingEntity = await _eventRepository.GetAll()
256 .Where(x => x.Id == uid)
257 .AsTracking()
258 .FirstAsync();
259
260 existingEntity.IsProcessed = false;
261 await CurrentUnitOfWork.SaveChangesAsync();
262
263 }
264 [UnitOfWork]
265 public virtual async Task PersistErrors(IEnumerable<ExecutionError> errors)
266 {
267
268 var executionErrors = errors as ExecutionError[] ?? errors.ToArray();
269 if (executionErrors.Any())
270 {
271 foreach (var error in executionErrors)
272 {
273 await _executionErrorRepository.InsertAsync(error.ToPersistable());
274 }
275 await CurrentUnitOfWork.SaveChangesAsync();
276
277 }
278
279 }
280 [UnitOfWork]
281 public virtual async Task<EventSubscription> GetSubscription(string eventSubscriptionId)
282 {
283
284 var uid = new Guid(eventSubscriptionId);
285 var raw = await _eventSubscriptionRepository.FirstOrDefaultAsync(x => x.Id == uid);
286
287 return raw?.ToEventSubscription();
288
289 }
290 [UnitOfWork]
291 public virtual async Task<EventSubscription> GetFirstOpenSubscription(string eventName, string eventKey, DateTime asOf)
292 {
293
294 var raw = await _eventSubscriptionRepository.FirstOrDefaultAsync(x => x.EventName == eventName && x.EventKey == eventKey && x.SubscribeAsOf <= asOf && x.ExternalToken == null);
295
296 return raw?.ToEventSubscription();
297
298 }
299 [UnitOfWork]
300 public virtual async Task<bool> SetSubscriptionToken(string eventSubscriptionId, string token, string workerId, DateTime expiry)
301 {
302
303 var uid = new Guid(eventSubscriptionId);
304 var existingEntity = await _eventSubscriptionRepository.GetAll()
305 .Where(x => x.Id == uid)
306 .AsTracking()
307 .FirstAsync();
308
309 existingEntity.ExternalToken = token;
310 existingEntity.ExternalWorkerId = workerId;
311 existingEntity.ExternalTokenExpiry = expiry;
312 await CurrentUnitOfWork.SaveChangesAsync();
313
314 return true;
315
316 }
317 [UnitOfWork]
318 public virtual async Task ClearSubscriptionToken(string eventSubscriptionId, string token)
319 {
320
321 var uid = new Guid(eventSubscriptionId);
322 var existingEntity = await _eventSubscriptionRepository.GetAll()
323 .Where(x => x.Id == uid)
324 .AsTracking()
325 .FirstAsync();
326
327 if (existingEntity.ExternalToken != token)
328 throw new InvalidOperationException();
329
330 existingEntity.ExternalToken = null;
331 existingEntity.ExternalWorkerId = null;
332 existingEntity.ExternalTokenExpiry = null;
333 await CurrentUnitOfWork.SaveChangesAsync();
334
335 }
336
337 public Task<PersistedWorkflow> GetPersistedWorkflow(Guid id)
338 {
339 return _workflowRepository.GetAsync(id);
340 }
341
342 public Task<PersistedWorkflowDefinition> GetPersistedWorkflowDefinition(string id, int version)
343 {
344 return _workflowDefinitionRepository.GetAll().AsNoTracking().FirstOrDefaultAsync(u => u.Id == id && u.Version == version);
345 }
346
347 public Task<PersistedExecutionPointer> GetPersistedExecutionPointer(string id)
348 {
349 return _executionPointerRepository.GetAsync(id);
350 }
351 }
  • 服务注册添加AddWorkflow时把IPersistenceProvider提供的默认实现换成AbpPersistenceProvider

 public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAbpWorkflow(this IServiceCollection services, Action<WorkflowOptions> setupAction = null)
{
services.AddSingleton<IPersistenceProvider, AbpPersistenceProvider>();
services.AddWorkflow(options =>
{ options.UsePersistence(sp => sp.GetService<AbpPersistenceProvider>());
setupAction?.Invoke(options);
});
services.AddWorkflowDSL();
return services;
}
}

  到此为止,ABP已经实现了WorkflowCore的默认的持久化存储。

2.ABP中AbpWorkflow和AbpStepBody的自定义注册

为了满足开发人员和用户的需求,我提供了两种流程注册方式,一种是开发人员后台编码定义固定流程另一种是用户通过流程设计器实现自定义业务流程。

  • 开发人员后台编码定义固定流程

这里参考ABP的EventBus注册方式,实现IWindsorInstaller ,在组件注册时拦截并注册:

//ABP工作流接口
public interface IAbpWorkflow : IWorkflow<WorkflowParamDictionary>
{
} //工作流注册接口
public interface IAbpWorkflowRegisty
{
void RegisterWorkflow(Type type);
} //Abp工作流注册实现
public class AbpWorkflowRegisty : IAbpWorkflowRegisty, ISingletonDependency
{
private IWorkflowRegistry _workflowRegistry;
private readonly IIocManager _iocManager; public AbpWorkflowRegisty(IWorkflowRegistry workflowRegistry, IIocManager iocManager)
{
this._workflowRegistry = workflowRegistry;
this._iocManager = iocManager;
} public void RegisterWorkflow(Type type)
{
var workflow = _iocManager.Resolve(type);
if (!(workflow is IAbpWorkflow))
{
throw new AbpException("RegistType must implement from AbpWorkflow!");
}
_workflowRegistry.RegisterWorkflow(workflow as IWorkflow<WorkflowParamDictionary>);
} } //拦截器实现
internal class WorkflowInstaller : IWindsorInstaller
{
private readonly IIocResolver _iocResolver; private IAbpWorkflowRegisty serviceSelector; public WorkflowInstaller(IIocResolver iocResolver)
{
_iocResolver = iocResolver;
} public void Install(IWindsorContainer container, IConfigurationStore store)
{
serviceSelector = container.Resolve<IAbpWorkflowRegisty>();
container.Kernel.ComponentRegistered += Kernel_ComponentRegistered;
} private void Kernel_ComponentRegistered(string key, IHandler handler)
{
if (!typeof(IAbpWorkflow).GetTypeInfo().IsAssignableFrom(handler.ComponentModel.Implementation))
{
return;
} var interfaces = handler.ComponentModel.Implementation.GetTypeInfo().GetInterfaces();
foreach (var @interface in interfaces)
{
if (!typeof(IAbpWorkflow).GetTypeInfo().IsAssignableFrom(@interface))
{
continue;
}
serviceSelector.RegisterWorkflow( handler.ComponentModel.Implementation);
}
}
}

  到这里,把拦截器注册到模块类的Initialize中,开发人员定义流程只需要实现IAbpWorkflow接口,系统启动时会自动注册。如图:

  • 自定义注册StepBody

这里参考ABP中标准的配置模式(不清楚的可以去看下ABP的源码,ABP的配置系统和权限系统都是这样配置的),将注册的StepBody存储在内存中提供给用户自定义组合流程节点使用,下列代码展示了注册指定用户审核的StepBody,执行方法体的实现:

 1  public class DefaultStepBodyProvider : AbpStepBodyProvider
2 {
3 public override void Build(IAbpStepBodyDefinitionContext context)
4 {
5 var step1 = new AbpWorkflowStepBody();
6 step1.Name = "FixedUserAudit";
7 step1.DisplayName = "指定用户审核";
8 step1.StepBodyType = typeof(GeneralAuditingStepBody);
9 step1.Inputs.Add(new WorkflowParam()
10 {
11 InputType = new SelectUserInputType(),//定义前端输入类型,继承Abp.UI.Inputs.InputTypeBase
12 Name = "UserId",
13 DisplayName = "审核人"
14 });
15 context.Create(step1);
16
17 }
18 }
19
20
21
22 /// <summary>
23 /// 指定用户审批StepBody
24 /// </summary>
25 public class GeneralAuditingStepBody : StepBody, ITransientDependency
26 {
27 private const string ActionName = "AuditEvent";
28 protected readonly INotificationPublisher _notificationPublisher;
29 protected readonly IAbpPersistenceProvider _abpPersistenceProvider;
30 protected readonly UserManager _userManager;
31
32 public readonly IRepository<PersistedWorkflowAuditor, Guid> _auditorRepository;
33
34 public GeneralAuditingStepBody(INotificationPublisher notificationPublisher, UserManager userManager, IAbpPersistenceProvider abpPersistenceProvider,
35 IRepository<PersistedWorkflowAuditor, Guid> auditorRepository)
36 {
37 _notificationPublisher = notificationPublisher;
38 _abpPersistenceProvider = abpPersistenceProvider;
39 _userManager = userManager;
40 _auditorRepository = auditorRepository;
41 }
42
43 /// <summary>
44 /// 审核人
45 /// </summary>
46 public long UserId { get; set; }
47
48 [UnitOfWork]
49 public override ExecutionResult Run(IStepExecutionContext context)
50 {
51 if (!context.ExecutionPointer.EventPublished)
52 {
53 var workflow = _abpPersistenceProvider.GetPersistedWorkflow(context.Workflow.Id.ToGuid()).Result;
54 var workflowDefinition = _abpPersistenceProvider.GetPersistedWorkflowDefinition(context.Workflow.WorkflowDefinitionId, context.Workflow.Version).Result;
55
56 var userIdentityName = _userManager.Users.Where(u => u.Id == workflow.CreatorUserId).Select(u => u.FullName).FirstOrDefault();
57
58 //通知审批人
59 _notificationPublisher.PublishTaskAsync(new Abp.Notifications.TaskNotificationData($"【{userIdentityName}】提交的{workflowDefinition.Title}需要您审批!"),
60 userIds: new UserIdentifier[] { new UserIdentifier(workflow.TenantId, UserId) },
61 entityIdentifier: new EntityIdentifier(workflow.GetType(), workflow.Id)
62 ).Wait();
63 //添加审核人记录
64 var auditUserInfo = _userManager.GetUserById(UserId);
65 _auditorRepository.Insert(new PersistedWorkflowAuditor() { WorkflowId = workflow.Id, ExecutionPointerId = context.ExecutionPointer.Id, Status = Abp.Entitys.CommEnum.EnumAuditStatus.UnAudited, UserId = UserId, TenantId = workflow.TenantId, UserHeadPhoto = auditUserInfo.HeadImage, UserIdentityName = auditUserInfo.FullName });
66 DateTime effectiveDate = DateTime.MinValue;
67 return ExecutionResult.WaitForEvent(ActionName, Guid.NewGuid().ToString(), effectiveDate);
68 }
69 var pass = _auditorRepository.GetAll().Any(u => u.ExecutionPointerId == context.ExecutionPointer.Id && u.UserId == UserId && u.Status == Abp.Entitys.CommEnum.EnumAuditStatus.Pass);
70
71 if (!pass)
72 {
73 context.Workflow.Status = WorkflowStatus.Complete;
74 return ExecutionResult.Next();
75 }
76 return ExecutionResult.Next();
77 }
78 }

查看代码

3.设计器实现

流程设计器我用的是Abp提供的Vue项目模板+jsplumb来实现的,话不多说直接上图把:

上图所示,每个节点执行操作选择的是我们后台注册的AbpStepBody。

注:开发人员可根据业务需求尽可能的给用户提供所需的StepBody。这样一来,整个流程的灵活性是非常好的。

4.设计器提交的流程数据转换成WorkflowCore支持的Json数据结构

前端传给后台的数据结构如下:

后台接收数据后转换成Workflow 支持的Josn字符串,再使用WorkflowCore.DSL提供的帮助类注册流程即可,转换后的Json如下:

 1 {
2 "DataType": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e",
3 "DefaultErrorBehavior": 0,
4 "DefaultErrorRetryInterval": null,
5 "Steps": [{
6 "StepType": "Abp.Workflows.DefaultSteps.NullStepBody, Abp.Workflows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
7 "Id": "start_1600248885360yurl0hgrvpd",
8 "Name": "start_1600248885360yurl0hgrvpd",
9 "CancelCondition": null,
10 "ErrorBehavior": null,
11 "RetryInterval": null,
12 "Do": [],
13 "CompensateWith": [],
14 "Saga": false,
15 "NextStepId": null,
16 "Inputs": {},
17 "Outputs": {},
18 "SelectNextStep": {
19 "step_1600248890720r3o927aajy8": "1==1"
20 }
21 }, {
22 "StepType": "Abp.Workflows.StepBodys.GeneralAuditingStepBody, Abp.Workflows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
23 "Id": "step_1600248890720r3o927aajy8",
24 "Name": "step_1600248890720r3o927aajy8",
25 "CancelCondition": null,
26 "ErrorBehavior": null,
27 "RetryInterval": null,
28 "Do": [],
29 "CompensateWith": [],
30 "Saga": false,
31 "NextStepId": null,
32 "Inputs": {
33 "UserId": "\"4\""
34 },
35 "Outputs": {},
36 "SelectNextStep": {
37 "end_16002488928403hmjauowus7": "decimal.Parse(data[\"Days\"].ToString()) <= 1",
38 "step_160032897781681o9ko9j3nr": "decimal.Parse(data[\"Days\"].ToString()) > 1"
39 }
40 }, {
41 "StepType": "Abp.Workflows.DefaultSteps.SendNotificationToInitiatorStepBody, Abp.Workflows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
42 "Id": "end_16002488928403hmjauowus7",
43 "Name": "end_16002488928403hmjauowus7",
44 "CancelCondition": null,
45 "ErrorBehavior": null,
46 "RetryInterval": null,
47 "Do": [],
48 "CompensateWith": [],
49 "Saga": false,
50 "NextStepId": null,
51 "Inputs": {
52 "Message": "\"您的流程已完成\""
53 },
54 "Outputs": {},
55 "SelectNextStep": {}
56 }, {
57 "StepType": "Abp.Workflows.StepBodys.GeneralAuditingStepBody, Abp.Workflows, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
58 "Id": "step_160032897781681o9ko9j3nr",
59 "Name": "step_160032897781681o9ko9j3nr",
60 "CancelCondition": null,
61 "ErrorBehavior": null,
62 "RetryInterval": null,
63 "Do": [],
64 "CompensateWith": [],
65 "Saga": false,
66 "NextStepId": null,
67 "Inputs": {
68 "UserId": "\"5\""
69 },
70 "Outputs": {},
71 "SelectNextStep": {
72 "end_16002488928403hmjauowus7": "1==1"
73 }
74 }],
75 "Id": "c51e908f-60e3-4a01-ab63-3bce0eaedc48",
76 "Version": 1,
77 "Description": "请假"
78 }

查看Json

总结

一句话,上面所写的一切都是为了将流程注册到WorkflowCore中而做的铺垫。

后面我会把代码整理一份作为一个ABP的独立模块开源出来供大家参考!

有四年没写博客了,很多东西写着写着觉得没意思,就不写了,这篇写得不好希望各位博友口下留情!

ABP+WorkflowCore+jsplumb实现工作流的更多相关文章

  1. 工作流引擎之Elsa入门系列教程之一 初始化项目并创建第一个工作流

    引子 工作流(Workflow)是对工作流程及其各操作步骤之间业务规则的抽象.概括描述. 为了实现某个业务目标,需要多方参与.按预定规则提交数据时,就可以用到工作流. 通过流程引擎,我们按照流程图,编 ...

  2. ABP Framework V4.4 RC 新增功能介绍

    目录 新增功能概述 启动模板删除 EntityFrameworkCore.DbMigrations 项目 CMS-Kit 动态菜单管理 Razor引擎对文本模板的支持 DbContext/Entiti ...

  3. 一个适合于.NET Core的超轻量级工作流引擎:Workflow-Core

    一.关于Workflow-Core 近期工作上有一个工作流的开发需求,自己基于面向对象和职责链模式捣鼓了一套小框架,后来在github上发现一个轻量级的工作流引擎轮子:Workflow-Core,看完 ...

  4. 企业级工作流解决方案(十五)--集成Abp和ng-alain--Abp其他改造

    配置功能增强 Abp定义了各种配置接口,但是没有定义这些配置数据从哪里来,但是管理配置数据对于一个应用程序来说,是必不可少的一件事情. .net的配置数据管理,一般放在Web.config文件或者Ap ...

  5. 企业级工作流解决方案(十)--集成Abp和ng-alain--权限系统

    权限系统 应用系统离不开权限控制,权限中心不一定能抽象出所有的业务场景,这里定义的权限系统不一定能够满足所有的场景,但应该可以满足多数的业务需求. Abp的zero项目也定义了权限相关的表,但里面很多 ...

  6. 开源轻量级工作流WorkflowCore介绍

    在.Net Framework环境下,我们使用Windows Workflow Foundation(WF)作为项目的工作流引擎,可是.Net Core已经不支持WF了,需要为基于.Net Core的 ...

  7. 企业级工作流解决方案(十三)--集成Abp和ng-alain--数据库读写分离

    说到程序里面数据库管理,无非就是两件事情,一是数据库操作,对于数据库的操作,各种程序语言都有封装,也就是所谓的ORM框架,.net 方向一般用得比较多和就是.net framework和dapper, ...

  8. 企业级工作流解决方案(十四)--集成Abp和ng-alain--自动化脚本

    对于.net方向,做过自动化的,应该没有人不熟悉msbuild吧,非常强大的代码编译工具,.net平台的编译工作都是交给他来完成的,包括.net core的命令,本质上都是调用msbuild来执行的 ...

  9. 企业级工作流解决方案(十二)--集成Abp和ng-alain--用户身份认证与权限验证

    多租户 如果系统需要支持多租户,那么最好事先定义好多租户的存储部署方式,Abp提供了几种方式,根据需要选择,每一个用户身份认证与权限验证都需要完全的隔离 这里设计的权限数据全部存储在缓存中,每个租户单 ...

随机推荐

  1. e3mall商城的归纳总结7之solr搭建和应用

    敬给读者的话 本文主要应用的技术是solr技术的搭建和应用,本文小编尽量写的更详细一些,让读者在不考虑项目的情况下也能正常完成solr的搭建,说完搭建之后,再说明运行solrj在项目中如何应用solr ...

  2. linux 下切换Python版本(某用户,共存,替换)

    当你安装 Debian Linux 时,安装过程有可能同时为你提供多个可用的 Python 版本,因此系统中会存在多个 Python 的可执行二进制文件.你可以按照以下方法使用 ls 命令来查看你的系 ...

  3. Codeforces1249E By Elevator or Stairs?

    题意 给定整数c和数组a,b,\(a_i\)表示通过爬楼梯的方法从第\(i\)层到\(i+1\)层需要的时间,\(b_i\)表示通过坐电梯的方法从第\(i\)层到\(i+1\)层需要的时间,坐电梯前需 ...

  4. Ajax提交数据判断员工编号是否存在,及自动填充与员工编号所对应的员工姓名。

    JSP页面中所需要的JavaScript事件及Ajax <script type="text/javascript"> function checkEmpNo(id){ ...

  5. Java中的String字符串及其常用方法

    字符串(String) 文章目录 字符串(String) 直接定义字符串 常用方法 字符串长度 toLowerCase() & toUpperCase()方法 trim()方法去除空格 判空 ...

  6. [Python]在当前目录下创建三个目录

    import os os.mkdir("2018-03-16-b018") os.mkdir("2019-03-16-b019") os.mkdir(" ...

  7. nginx模型概念和配置文件结构

    一. nginx模型概念: Nginx会按需同时运行多个进程: 一个主进程(master)和几个工作进程(worker),配置了缓存时还会有缓存加载器进程(cache loader)和缓存管理器进程( ...

  8. latex 封面

    latex 封面 代码: \begin{titlepage} \heiti \vspace*{64pt} \begin{center} \fontsize{48pt}{0} XX大学\\ \vspac ...

  9. ThinkPHP6.0 判断是否有文件上传

    有必要考虑不是post请求或没有指定enctype="multipart/form-data"的情况.如果是post请求还是设置了正确的编码,没有文件上传时 request()-& ...

  10. 【原创】Kuberneters-HelmV3.3.1入门介绍及实践

    一.为什么需要Helm? Kubernetes目前已成为容器编排的事实标准,随着传统架构向微服务容器化架构的转变,从一个巨大的单体的应用切分为多个微服务,每个微服务可独立部署和扩展,实现了敏捷开发和快 ...