ABP+WorkflowCore+jsplumb實現工作流
前言
ABP目前已經是很成熟的開發框架了,它提供了很多我們日常開發所必須的功能,並且很方便擴充套件,讓我們能更專注於業務的開發。但是ABP官方並沒有給我們實現工作流。
在.net core環境下的開源工作流引擎很少,其中WorkflowCore是一款輕量級工作流引擎,對於小型工作流和責任鏈型別的需求開發很適合,但只能通過後臺編碼或者json的方式定義工作流程,看了原始碼後覺得擴充套件性還是挺好的,至少能滿足我的需求,於是選擇對它下手。
jsPlumb是一個開源的比較強大的繪圖元件,這裡不多介紹,我就是用它實現一個簡單的流程設計器。
花了差不多一個月的時間,把這三者結合到一起實現一個簡單而強大的工作流模組。
目錄
- ABP模組實現WorkflowCore持久化儲存介面(IPersistenceProvider)
- ABP中AbpWorkflow和AbpStepBody的自定義註冊
- 設計器實現
- 設計器提交的流程資料轉換成WorkflowCore支援的Json資料結構
- 總結
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的獨立模組開源出來供大家參考!
有四年沒寫部落格了,很多東西寫著寫著覺得沒意思,就不寫了,這篇寫得不好希望各位博友口下留情!