From 3dbc913085a57a653c6036bcfc5b2f51447c7a3f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 7 Jan 2023 22:01:21 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E5=A4=84=E7=90=86=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E5=90=8E=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E4=B8=8D=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E9=97=AE=E9=A2=98=E3=80=82=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E8=AE=BE=E8=AE=A1=E9=83=A8=E5=88=86=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 397 ++++++++++++++++++ .../EventBus/EventBusService.cs | 20 +- .../WorkFlowCore.Framework.csproj | 1 + .../WorkFlowCoreFrameworkService.cs | 9 +- .../Controllers/WorkFlowController.cs | 4 +- .../WorkFlowCore.Host/WorkFlowCore.Host.xml | 2 +- .../appsettings.Development.json | 2 +- .../WorkFlowCore.Test.csproj | 1 + .../Workflow/Workflow_Test.cs | 12 +- WorkFlowCore/WorkFlowCore.sln | 6 +- .../WorkFlowCore/Workflows/RejectNode.cs | 4 +- 11 files changed, 440 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b738b76..adf1a6e 100644 --- a/README.md +++ b/README.md @@ -167,3 +167,400 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ 3. 其它的诸如新建流程设计、流程审批记录等,可通过接口查询,或者自行实现相应的查询接口。 +## API + +### 流程设计相关 + +在流程设计服务里,主要涉及设计和版本的管理。 +设计内容则为流程的设计信息,包括节点,连线,人员,条件以及前端其它设计代码管理。 +版本则为流程的版本管理,服务的设计为,一个流程可以有多个版本,随着业务的调整可以不断的调整流程,通过版本管理,可以保证现有流程与新流程区分开来,使旧流程不受新流程影响。 +- #### /api/WorkFlow/CreateWorkFlow 创建流程(设计) + + 这里的创建很简单,这里实际上是将设计信息的创建统一放到更新操作,一来为了简化新增逻辑,统一在更新处理,二来也是配合前端的交互进行的设计。 + + + - name:流程设计名称 + - des :描述 + + 该操作返回流程基本信息(这时候设计信息还是为空) + ``` + { + + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", //流程id + "workflowNo": "string",//流程编号 + "name": "string",//流程名称 + "activeVersion": 0, //当前使用版本 + "description": "string" //描述 + ...... + } + + ``` + + + + +- #### /api/WorkFlow/UpdateWorkflow 流程设计更新 + 该接口用于保存流程设计信息,当流程版本有多个时,每个版本单独存储,每个版本有自己的设计id。 + + - name:流程设计名称 + - des: 描述 + - drawingInfo: 前端设计绘制信息,考虑到有些前端流程框架的绘制数据可能比较复杂,为便于回显,可以直接将绘制信息记录起来,需要回显可直接读取 + + - versionDescription: 版本描述。 + - workflowId: 流程唯一标识。 由于一个流程可以有多个版本,所以唯一标识 由 流程id 和 版本确定 + ``` + { + versionId: 0 //版本号 + id: //流程设计id 为 CreateWorkFlow 接口返回的 id + } + ``` + + + - workflowNodes: 流程节点。系统的设计思路为节点负责人员选择,连线负责条件判断。一个节点除了有基本的节点信息,还包括 用户选择器(userSelector 用于在流程流转过程中,解析审批人员)。在这里,节点信息允许括驳回节点指定,即可根据不同的条件判定驳回场景下,应流转到哪个节点(补充一点,在当前系统,不管是拒绝还是驳回,都会构建一条记录信息,而不是“删除”上一步操作,所以每一步都是“下一步”)。 + + 流程节点数据格式如下: + + ``` + [ + { + id: 3fa85f64-5717-4562-b3fc-2c963f66afa6 //节点唯一标识,应由前端生成 + name: string //节点名称 + nodeType: 0 //节点类型 + drawingInfo: string //节点绘制信息。预留字段,如果节点的绘制数据较复杂,可以考虑直接保存绘制信息,便于回显 + isWaitingAllUser: true //是否等待所有用户(处理)。 + userSelectors: //用户选择器,指定通过哪个选择器来解析审批人员 + [ + { + selectorId: string // 选择器id + selectorName: string //选择器名称 + selections: //选项值 + [ + { + id: string //选项值id + name: string //选项名称 + } + ] + parameter: string //参数,指定参数将作为选择器使用(参数是否有用,具体看选择器的实现,在后台解析审批人员时,会传递该参数) + description: string //描述 + handleType: 0 //处理类型。分为 只读(类似 抄送,只能看但不能处理)和处理。 + } + ] + rejectNodes: //驳回(拒绝)回退的节点配置 + [ + { + conditions: //条件 + [ + { + conditionId: string //条件id + conditionName: string //条件名称 + parameter: string //参数 + description: string // 描述 + } + ] + nodeId: 3fa85f64-5717-4562-b3fc-2c963f66afa6 //退回的节点id + nodeName: string // 退回的节点名称 + } + ] + } + ] + + ``` + + 其中: + - nodeType 节点类型。系统会自动从“开始”节点解析流程。 + 其中比较特别的类型是“会签”,使用会签节点的效果与 isWaitingAllUser 值为 true 的类似,但它是需要连接到它的节点审批通过才会进行下一个节点,它等待的不同来源节点审批完成。有以下枚举类型: + ``` + /// + /// 开始 + /// + Begin, + /// + /// 结束 + /// + End, + /// + /// 普通 + /// + Judge, + /// + /// 会签,所有指向该节点的节点都要审批完成后到达 + /// + Sign + ``` + - isWaitingAllUser : 一个节点可能会分配到多个人审批,通常同一个节点有其中一个人处理后,即可往下一个节点,但是,如果勾选等待所有用户审批,那这个节点就相当会签节点,所有人都通过才会走下一步。 + + - userSelectors : 用户选择器集合。用户选择器用于在流转到节点时,解析目标审批人员。用户选择器信息主要包括几个信息:哪个选择器,选了哪个选项,额外参数,人员处理方式。 + + - 用户选择器通过 实现 IUserSelector 接口自定义用户选择 + ``` + + [UserSelector("按用户选择","从所有用户选择")] + public class UserSelectorB : IUserSelector + { + //获取选项值 + public List GetSelections() + { + + return UserList.Users.Select(u => new Selection { Id = u.Id, Name = u.Name }).ToList(); + } + //根据选项和参数获取用户(审批人员) + public List GetUsers(SelectorInput input) + { + var result = new List(); + switch (input.SelectionId) + { + default: + result.Add(new User { Id = input.SelectionId, Name = UserList.GetUserById(input.SelectionId).Name }); + break; + } + return result; + } + } + ``` + - /api/WorkFlow/GetAllUserSelectors 前端通过该接口获取所有的用户选择器。默认返回的是 实现了 IUserSelector 接口的类型的基本信息,selectorId 为 类型(在文档这里是 UserSelectorB)全称,selectorName 为 UserSelectorAttribute 的第一个参数: + ``` + [UserSelector("按用户选择","从所有用户选择")] + public class UserSelectorB : IUserSelector + + ``` + - 返回选择器集 + ``` + [ + { + "id": "string", //选择器id(类名全称) + "name": "string", //选择器名称 UserSelectorAttribute 的第一个参数 + "description": "string" //选择器描述 UserSelectorAttribute 的第二个参数 + } + ] + + ``` + + + - /api/WorkFlow/GetUserSelectionsOfUserSelector 通过该接口根据用户选择器id获取该选择器的选项 。 + - userSelectorId 选择器id。 + - 返回选项集 + ``` + [ + { + "id": "string", //选项id + "name": "string" 选项名称 + } + ] + ``` + + + - rejectNodes :拒绝(驳回、回退、回滚)节点配置。通过该配置,根据条件“回到”指定的节点,在必要情况下可以直接退到指定的节点,默认情况下,不配置时,则按原路返回。该配置主要有两项内容:条件,目标节点 + - nodeId、nodeName 目标节点id 和名称, + - conditions 条件处理器的配置集合,可配置多个条件,满足其一则跳转到指定节点。条件处理器信息相对简单,主要包含哪个条件处理器以及额外参数。 + - 通过实现 ICondition 接口实现自定义条件处理器。 + ``` + [Condition("条件处理器A")] + public class ConditionA : ICondition + { + public bool CanAccept(ConditionInput input) + { + try + { + //简单的表达式解析 + var keyvalue = input.Expression.Split('='); + JObject jObject = JObject.Parse(input.WorkTask.FormData); + var token = jObject.SelectToken(keyvalue[0]); + var value = token.Value(); + return value.Equals(keyvalue[1]); + } + catch (Exception) + { + return false; + } + } + } + ``` + - /api/WorkFlow/GetAllconditions 通过该接口可以获取所有的条件处理器信息。 + ``` + [Condition("条件处理器A")] + public class ConditionA : ICondition + + ``` + - 返回基本的条件处理器信息集 + ``` + [ + { + "id": "string", //条件处理器id(类名全称) + "name": "string", //条件处理器名称 ConditionAttribute 的第一个参数 + "description": "string" //条件处理器描述 ConditionAttribute 的第二个参数 + } + ] + + ``` + - workflowLines : 节点连线信息。该属性记录节点间的连线关系(从哪个节点到哪个节点)以及通行(条件是否满足)条件。 + - name: 条件名称。只用于显示 + - fromNodeId: 起始节点 + - toNodeId: 目标节点 + - drawingInfo: 绘制信息。也是冗余备用。万一需要存储前端设计信息,也便于回显 + - conditions :条件选择器信息,用于判断改连线流转是否满足条件,满足才能进行(同 用户选择器 rejectNodes 下的 conditions ),相当于一个“判断”+连线。 + + + + - /api/WorkFlow/UpdateWorkflowActiveVersion 用户切换当前激活(应用)的是哪个版本的流程 + - workflowId :流程id + - activeVersion :激活版本(versionId) + + - /api/WorkFlow/GetAllWorkflowVersions 通过该接口根据流程id获取所有的流程和版本信息(一个流程有多个版本) + - workflowId 流程id + - 返回流程版本集合 + ``` + [ + { + "workflowId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", //流程id + "versionNo": 0, //流程版本 + "description": "string", //版本描述 + "modifiedTime": "2023-01-07T13:40:38.791Z", + "creationTime": "2023-01-07T13:40:38.791Z" + } + ] + + ``` + +- ​/api​/WorkFlow​/GetAllWorkflows 该接口获取所有的流程(设计)基本信息列表 + - 返回基本信息列表 + ``` + [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", //流程id + "workflowNo": "string", //流程编号 + "name": "string", //流程名称 + "activeVersion": 0, //当前应用版本 + "description": "string" //描述 + } + ] + + ``` + +- /api/WorkFlow/GetWorkflowVersion 根据流程id和版本号获取详细的流程设计信息。 + - versionId 版本号 + - id 工作流id + + - 返回详细信息 + ``` + { + "code": "string", + "msg": "string", + "data": { + "modifiedTime": "2023-01-07T13:48:42.386Z", + "modifiedUserId": "string", + "createdUserId": "string", + "creationTime": "2023-01-07T13:48:42.386Z", + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "deletedUserId": "string", + "deletedTime": "2023-01-07T13:48:42.386Z", + "deleted": true, + "workflowId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "versionNo": 0, + "drawingInfo": "string", + "description": "string", + "nodeMaps": [], //节点映射。连线和节点的映射信息集合。 + "allNodes": [] //所有节点 + } + } + + ``` + 其中 + - nodeMaps 和 allNodes 数据差不多。对于 allNodes ,类似 接口 /api/WorkFlow/UpdateWorkflow 的参数。 nodeMaps 是 lines 和 nodes 关联的信息的明细,相当于把 连线line 里的 fromNodeId toNodeId 替换为 fromNode toNode ,对应 allNode 里相同nodeId的 node 节点: + ``` + [ + { + "mapType": 0, + "fromNode": { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "workflowId": { + "versionId": 0, + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + "name": "string", + "nodeType": 0, + "drawingInfo": "string", + "isWaitingAllUser": true, + "userSelectors": [ + { + "selectorId": "string", + "selectorName": "string", + "selections": [ + { + "id": "string", + "name": "string" + } + ], + "parameter": "string", + "description": "string", + "handleType": 0 + } + ], + "rejectNodes": [ + { + "conditions": [ + { + "conditionId": "string", + "conditionName": "string", + "parameter": "string", + "description": "string" + } + ], + "nodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "nodeName": "string" + } + ] + }, + "toNode": { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "workflowId": { + "versionId": 0, + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + "name": "string", + "nodeType": 0, + "drawingInfo": "string", + "isWaitingAllUser": true, + "userSelectors": [ + { + "selectorId": "string", + "selectorName": "string", + "selections": [ + { + "id": "string", + "name": "string" + } + ], + "parameter": "string", + "description": "string", + "handleType": 0 + } + ], + "rejectNodes": [ + { + "conditions": [ + { + "conditionId": "string", + "conditionName": "string", + "parameter": "string", + "description": "string" + } + ], + "nodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "nodeName": "string" + } + ] + }, + "conditions": [ + { + "conditionId": "string", + "conditionName": "string", + "parameter": "string", + "description": "string" + } + ] + } + ] + ``` + + + \ No newline at end of file diff --git a/WorkFlowCore/WorkFlowCore.Common/EventBus/EventBusService.cs b/WorkFlowCore/WorkFlowCore.Common/EventBus/EventBusService.cs index 25dbe32..30e4b49 100644 --- a/WorkFlowCore/WorkFlowCore.Common/EventBus/EventBusService.cs +++ b/WorkFlowCore/WorkFlowCore.Common/EventBus/EventBusService.cs @@ -34,19 +34,27 @@ namespace WorkFlowCore.Common.EventBus return services; } public static IApplicationBuilder InitGlobalEventBus(this IApplicationBuilder app) + { + InitGlobalEventBus(app.ApplicationServices); + + return app; + + } + + public static IServiceProvider InitGlobalEventBus(this IServiceProvider serviceProvider) { //注册普通事件,该事件订阅在单应用有效无法分布式 - EventBusManager.Init(app.ApplicationServices); + EventBusManager.Init(serviceProvider); //注册kafka作为分布式事件 - var kafkaEventBus = app.ApplicationServices.GetService(); - var config = app.ApplicationServices.GetService(); - var configuration = app.ApplicationServices.GetService(); + var kafkaEventBus = serviceProvider.GetService(); + var config = serviceProvider.GetService(); + var configuration = serviceProvider.GetService(); Console.WriteLine("servers:" + configuration["KafkaBootstrapServers"]); - if (kafkaEventBus!=null&&config!=null && config.RegisterAssemblies != null) + if (kafkaEventBus != null && config != null && config.RegisterAssemblies != null) kafkaEventBus.RegistSubscriptions(config.RegisterAssemblies); - return app; + return serviceProvider; } } diff --git a/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCore.Framework.csproj b/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCore.Framework.csproj index a9f7291..aae5881 100644 --- a/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCore.Framework.csproj +++ b/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCore.Framework.csproj @@ -6,6 +6,7 @@ + diff --git a/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs b/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs index 23c5b77..529e975 100644 --- a/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs +++ b/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs @@ -1,8 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Text; +using WorkFlowCore.Authorization; using WorkFlowCore.Conditions; +using WorkFlowCore.Framework.Authorization; using WorkFlowCore.Framework.Conditions; using WorkFlowCore.Framework.EventHandlers; using WorkFlowCore.Framework.Repositories; @@ -53,13 +56,15 @@ namespace WorkFlowCore.Framework services.AddScoped(typeof(IUnitOfWorkManager), typeof(UnitOfWorkManager4EF)); } - + services.AddSingleton(); //事件处理 services.AddScoped(); services.AddScoped(); var assembly = typeof(WorkFlowCoreFrameworkService).Assembly; + services.AddScoped(); + //注册条件和选择器 UserSelectorManager.RegisterSelector(assembly); ConditionManager.RegisterCondition(assembly); diff --git a/WorkFlowCore/WorkFlowCore.Host/Controllers/WorkFlowController.cs b/WorkFlowCore/WorkFlowCore.Host/Controllers/WorkFlowController.cs index fa3be5c..9dc309e 100644 --- a/WorkFlowCore/WorkFlowCore.Host/Controllers/WorkFlowController.cs +++ b/WorkFlowCore/WorkFlowCore.Host/Controllers/WorkFlowController.cs @@ -115,7 +115,7 @@ namespace WorkFlowCore.Host.Controllers /// /// /// - [HttpGet("DeleteWorkflowVersion")] + [HttpDelete("DeleteWorkflowVersion")] public async Task> GetWorkflowVersion(Guid id) { var result = await workflowManager.DeleteWorkflow(id); @@ -138,7 +138,7 @@ namespace WorkFlowCore.Host.Controllers /// 更新流程 /// /// - /// + /// f [HttpPut("UpdateWorkFlow")] public async Task> UpdateWorkFlow(UpdateWorkFlowInput input) { diff --git a/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.xml b/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.xml index ddbc8fb..de5260b 100644 --- a/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.xml +++ b/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.xml @@ -69,7 +69,7 @@ 更新流程 - + f diff --git a/WorkFlowCore/WorkFlowCore.Host/appsettings.Development.json b/WorkFlowCore/WorkFlowCore.Host/appsettings.Development.json index b58cdf3..678228f 100644 --- a/WorkFlowCore/WorkFlowCore.Host/appsettings.Development.json +++ b/WorkFlowCore/WorkFlowCore.Host/appsettings.Development.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "Default": "Database=Workflow;Data Source=81.71.14.205;Port=3308;UserId=root;Password=ShiHuiDai123!;Charset=utf8;TreatTinyAsBoolean=false;Allow User Variables=True", + "Default": "Database=WorkflowCore;Data Source=39.101.74.14;Port=3308;UserId=root;Password=ShiHuiDai123!;Charset=utf8;TreatTinyAsBoolean=false;Allow User Variables=True", "DefaultVersion": "8.0.26" }, "Logging": { diff --git a/WorkFlowCore/WorkFlowCore.Test/WorkFlowCore.Test.csproj b/WorkFlowCore/WorkFlowCore.Test/WorkFlowCore.Test.csproj index d518e33..4d6037e 100644 --- a/WorkFlowCore/WorkFlowCore.Test/WorkFlowCore.Test.csproj +++ b/WorkFlowCore/WorkFlowCore.Test/WorkFlowCore.Test.csproj @@ -7,6 +7,7 @@ + diff --git a/WorkFlowCore/WorkFlowCore.Test/Workflow/Workflow_Test.cs b/WorkFlowCore/WorkFlowCore.Test/Workflow/Workflow_Test.cs index 2e32235..c278567 100644 --- a/WorkFlowCore/WorkFlowCore.Test/Workflow/Workflow_Test.cs +++ b/WorkFlowCore/WorkFlowCore.Test/Workflow/Workflow_Test.cs @@ -1,10 +1,12 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using NUnit.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using WorkFlowCore.Common.EventBus; using WorkFlowCore.Conditions; using WorkFlowCore.Framework; using WorkFlowCore.Framework.Repositories; @@ -37,12 +39,20 @@ namespace WorkFlowCore.Test.Workflow services.AddScoped(); services.AddScoped(); + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + services.AddSingleton(configuration); + + services.AddDefautEventBus(typeof(WorkFlowCoreFrameworkService).Assembly); + + BasicRepository.ClearData(); BasicRepository.ClearData(); BasicRepository.ClearData(); BasicRepository.ClearData(); serviceProvider = services.BuildServiceProvider(); + serviceProvider.InitGlobalEventBus(); } [Test] diff --git a/WorkFlowCore/WorkFlowCore.sln b/WorkFlowCore/WorkFlowCore.sln index 7165398..a8297ed 100644 --- a/WorkFlowCore/WorkFlowCore.sln +++ b/WorkFlowCore/WorkFlowCore.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30413.136 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32516.85 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkFlowCore", "WorkFlowCore\WorkFlowCore.csproj", "{8B678BB1-F787-448D-8714-4D34E165B4DF}" EndProject @@ -11,7 +11,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkFlowCore.Test", "WorkFl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkFlowCore.Host", "WorkFlowCore.Host\WorkFlowCore.Host.csproj", "{1C0D8C51-D49C-4C1D-9C66-1522546360E0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkFlowCore.Common", "WorkFlowCore.Common\WorkFlowCore.Common.csproj", "{8F14A61F-C202-4ED1-A167-B426EE428DDA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkFlowCore.Common", "WorkFlowCore.Common\WorkFlowCore.Common.csproj", "{8F14A61F-C202-4ED1-A167-B426EE428DDA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/WorkFlowCore/WorkFlowCore/Workflows/RejectNode.cs b/WorkFlowCore/WorkFlowCore/Workflows/RejectNode.cs index 7cdd308..9a984c5 100644 --- a/WorkFlowCore/WorkFlowCore/Workflows/RejectNode.cs +++ b/WorkFlowCore/WorkFlowCore/Workflows/RejectNode.cs @@ -7,7 +7,7 @@ namespace WorkFlowCore.Workflows public class RejectNode { public List Conditions { get; set; } - public Guid NodeId; - public Guid NodeName; + public Guid NodeId { get; set; } + public string NodeName { get; set; } } } -- Gitee From 6ade9522cbc656fed87106e4bbe4e1d2e992ce7c Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 7 Jan 2023 22:11:11 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E7=BE=8E=E5=8C=96=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 68 +++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index adf1a6e..14d59d7 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ 在流程设计服务里,主要涉及设计和版本的管理。 设计内容则为流程的设计信息,包括节点,连线,人员,条件以及前端其它设计代码管理。 版本则为流程的版本管理,服务的设计为,一个流程可以有多个版本,随着业务的调整可以不断的调整流程,通过版本管理,可以保证现有流程与新流程区分开来,使旧流程不受新流程影响。 -- #### /api/WorkFlow/CreateWorkFlow 创建流程(设计) +- #### ***/api/WorkFlow/CreateWorkFlow*** 创建流程(设计) 这里的创建很简单,这里实际上是将设计信息的创建统一放到更新操作,一来为了简化新增逻辑,统一在更新处理,二来也是配合前端的交互进行的设计。 @@ -199,15 +199,15 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ -- #### /api/WorkFlow/UpdateWorkflow 流程设计更新 +- #### ***/api/WorkFlow/UpdateWorkflow*** 流程设计更新 该接口用于保存流程设计信息,当流程版本有多个时,每个版本单独存储,每个版本有自己的设计id。 - - name:流程设计名称 - - des: 描述 - - drawingInfo: 前端设计绘制信息,考虑到有些前端流程框架的绘制数据可能比较复杂,为便于回显,可以直接将绘制信息记录起来,需要回显可直接读取 + - **name**:流程设计名称 + - **des**: 描述 + - **drawingInfo**: 前端设计绘制信息,考虑到有些前端流程框架的绘制数据可能比较复杂,为便于回显,可以直接将绘制信息记录起来,需要回显可直接读取 - - versionDescription: 版本描述。 - - workflowId: 流程唯一标识。 由于一个流程可以有多个版本,所以唯一标识 由 流程id 和 版本确定 + - **versionDescription**: 版本描述。 + - **workflowId**: 流程唯一标识。 由于一个流程可以有多个版本,所以唯一标识 由 流程id 和 版本确定 ``` { versionId: 0 //版本号 @@ -216,7 +216,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` - - workflowNodes: 流程节点。系统的设计思路为节点负责人员选择,连线负责条件判断。一个节点除了有基本的节点信息,还包括 用户选择器(userSelector 用于在流程流转过程中,解析审批人员)。在这里,节点信息允许括驳回节点指定,即可根据不同的条件判定驳回场景下,应流转到哪个节点(补充一点,在当前系统,不管是拒绝还是驳回,都会构建一条记录信息,而不是“删除”上一步操作,所以每一步都是“下一步”)。 + - **workflowNodes**: 流程节点。系统的设计思路为节点负责人员选择,连线负责条件判断。一个节点除了有基本的节点信息,还包括 用户选择器(userSelector 用于在流程流转过程中,解析审批人员)。在这里,节点信息允许括驳回节点指定,即可根据不同的条件判定驳回场景下,应流转到哪个节点(补充一点,在当前系统,不管是拒绝还是驳回,都会构建一条记录信息,而不是“删除”上一步操作,所以每一步都是“下一步”)。 流程节点数据格式如下: @@ -267,8 +267,8 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` 其中: - - nodeType 节点类型。系统会自动从“开始”节点解析流程。 - 其中比较特别的类型是“会签”,使用会签节点的效果与 isWaitingAllUser 值为 true 的类似,但它是需要连接到它的节点审批通过才会进行下一个节点,它等待的不同来源节点审批完成。有以下枚举类型: + - **nodeType** 节点类型。系统会自动从“开始”节点解析流程。 + 其中比较特别的类型是“会签”,使用会签节点的效果与 **isWaitingAllUser** 值为 *true* 的类似,但它是需要连接到它的节点审批通过才会进行下一个节点,它等待的不同来源节点审批完成。有以下枚举类型: ``` /// /// 开始 @@ -287,9 +287,9 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ /// Sign ``` - - isWaitingAllUser : 一个节点可能会分配到多个人审批,通常同一个节点有其中一个人处理后,即可往下一个节点,但是,如果勾选等待所有用户审批,那这个节点就相当会签节点,所有人都通过才会走下一步。 + - **isWaitingAllUser** : 一个节点可能会分配到多个人审批,通常同一个节点有其中一个人处理后,即可往下一个节点,但是,如果勾选等待所有用户审批,那这个节点就相当会签节点,所有人都通过才会走下一步。 - - userSelectors : 用户选择器集合。用户选择器用于在流转到节点时,解析目标审批人员。用户选择器信息主要包括几个信息:哪个选择器,选了哪个选项,额外参数,人员处理方式。 + - **userSelectors** : 用户选择器集合。用户选择器用于在流转到节点时,解析目标审批人员。用户选择器信息主要包括几个信息:哪个选择器,选了哪个选项,额外参数,人员处理方式。 - 用户选择器通过 实现 IUserSelector 接口自定义用户选择 ``` @@ -317,7 +317,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ } } ``` - - /api/WorkFlow/GetAllUserSelectors 前端通过该接口获取所有的用户选择器。默认返回的是 实现了 IUserSelector 接口的类型的基本信息,selectorId 为 类型(在文档这里是 UserSelectorB)全称,selectorName 为 UserSelectorAttribute 的第一个参数: + - ***/api/WorkFlow/GetAllUserSelectors*** 前端通过该接口获取所有的用户选择器。默认返回的是 实现了 IUserSelector 接口的类型的基本信息,selectorId 为 类型(在文档这里是 UserSelectorB)全称,selectorName 为 UserSelectorAttribute 的第一个参数: ``` [UserSelector("按用户选择","从所有用户选择")] public class UserSelectorB : IUserSelector @@ -336,7 +336,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` - - /api/WorkFlow/GetUserSelectionsOfUserSelector 通过该接口根据用户选择器id获取该选择器的选项 。 + - ***/api/WorkFlow/GetUserSelectionsOfUserSelector*** 通过该接口根据用户选择器id获取该选择器的选项 。 - userSelectorId 选择器id。 - 返回选项集 ``` @@ -349,9 +349,9 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` - - rejectNodes :拒绝(驳回、回退、回滚)节点配置。通过该配置,根据条件“回到”指定的节点,在必要情况下可以直接退到指定的节点,默认情况下,不配置时,则按原路返回。该配置主要有两项内容:条件,目标节点 - - nodeId、nodeName 目标节点id 和名称, - - conditions 条件处理器的配置集合,可配置多个条件,满足其一则跳转到指定节点。条件处理器信息相对简单,主要包含哪个条件处理器以及额外参数。 + - **rejectNodes** :拒绝(驳回、回退、回滚)节点配置。通过该配置,根据条件“回到”指定的节点,在必要情况下可以直接退到指定的节点,默认情况下,不配置时,则按原路返回。该配置主要有两项内容:条件,目标节点 + - **nodeId、nodeName** 目标节点id 和名称, + - **conditions** 条件处理器的配置集合,可配置多个条件,满足其一则跳转到指定节点。条件处理器信息相对简单,主要包含哪个条件处理器以及额外参数。 - 通过实现 ICondition 接口实现自定义条件处理器。 ``` [Condition("条件处理器A")] @@ -375,7 +375,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ } } ``` - - /api/WorkFlow/GetAllconditions 通过该接口可以获取所有的条件处理器信息。 + - ***/api/WorkFlow/GetAllconditions*** 通过该接口可以获取所有的条件处理器信息。 ``` [Condition("条件处理器A")] public class ConditionA : ICondition @@ -392,21 +392,21 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ] ``` - - workflowLines : 节点连线信息。该属性记录节点间的连线关系(从哪个节点到哪个节点)以及通行(条件是否满足)条件。 - - name: 条件名称。只用于显示 - - fromNodeId: 起始节点 - - toNodeId: 目标节点 - - drawingInfo: 绘制信息。也是冗余备用。万一需要存储前端设计信息,也便于回显 - - conditions :条件选择器信息,用于判断改连线流转是否满足条件,满足才能进行(同 用户选择器 rejectNodes 下的 conditions ),相当于一个“判断”+连线。 + - **workflowLines** : 节点连线信息。该属性记录节点间的连线关系(从哪个节点到哪个节点)以及通行(条件是否满足)条件。 + - **name: 条件名称。只用于显示 + - **fromNodeId**: 起始节点 + - **toNodeId**: 目标节点 + - **drawingInfo**: 绘制信息。也是冗余备用。万一需要存储前端设计信息,也便于回显 + - **conditions** :条件选择器信息,用于判断改连线流转是否满足条件,满足才能进行(同 用户选择器 rejectNodes 下的 conditions ),相当于一个“判断”+连线。 - - /api/WorkFlow/UpdateWorkflowActiveVersion 用户切换当前激活(应用)的是哪个版本的流程 - - workflowId :流程id - - activeVersion :激活版本(versionId) + - ***/api/WorkFlow/UpdateWorkflowActiveVersion*** 用户切换当前激活(应用)的是哪个版本的流程 + - **workflowId** :流程id + - **activeVersion** :激活版本(versionId) - - /api/WorkFlow/GetAllWorkflowVersions 通过该接口根据流程id获取所有的流程和版本信息(一个流程有多个版本) - - workflowId 流程id + - ***/api/WorkFlow/GetAllWorkflowVersions*** 通过该接口根据流程id获取所有的流程和版本信息(一个流程有多个版本) + - **workflowId** 流程id - 返回流程版本集合 ``` [ @@ -421,7 +421,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` -- ​/api​/WorkFlow​/GetAllWorkflows 该接口获取所有的流程(设计)基本信息列表 +- #### ***​/api​/WorkFlow​/GetAllWorkflows*** 该接口获取所有的流程(设计)基本信息列表 - 返回基本信息列表 ``` [ @@ -436,9 +436,9 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` -- /api/WorkFlow/GetWorkflowVersion 根据流程id和版本号获取详细的流程设计信息。 - - versionId 版本号 - - id 工作流id +- #### ***/api/WorkFlow/GetWorkflowVersion*** 根据流程id和版本号获取详细的流程设计信息。 + - **versionId** 版本号 + - **id** 工作流id - 返回详细信息 ``` @@ -465,7 +465,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` 其中 - - nodeMaps 和 allNodes 数据差不多。对于 allNodes ,类似 接口 /api/WorkFlow/UpdateWorkflow 的参数。 nodeMaps 是 lines 和 nodes 关联的信息的明细,相当于把 连线line 里的 fromNodeId toNodeId 替换为 fromNode toNode ,对应 allNode 里相同nodeId的 node 节点: + - **nodeMaps** 和 allNodes 数据差不多。对于 allNodes ,类似 接口 ***/api/WorkFlow/UpdateWorkflow*** 的参数。 nodeMaps 是 lines 和 nodes 关联的信息的明细,相当于把 连线line 里的 fromNodeId toNodeId 替换为 fromNode toNode ,对应 allNode 里相同nodeId的 node 节点: ``` [ { -- Gitee From f1e1ea24341db2179c8555640449992ff4d16af9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 8 Jan 2023 14:06:40 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=92=8C=E9=83=A8=E5=88=86=E5=B8=B8=E7=94=A8=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 298 +++++++++++++++++- .../SimplePluginLoaders/Manifest.cs | 14 + .../PluginAssemblyLoadContext.cs | 29 ++ .../SimplePluginLoaders/SimplePluginLoader.cs | 60 ++++ .../Repositories/WorkTaskRepository.cs | 18 ++ .../Repositories4EF/WorkTaskRepository4EF.cs | 13 + .../WorkFlowCoreFrameworkService.cs | 4 +- .../Controllers/WorkFlowController.cs | 108 ++++++- ...AllProcessingWorkTasksByEntityTypeInput.cs | 12 + .../GetHandledWorkTasksOfUserInput.cs | 14 + .../GetUnHandlerWorkTasksOfUserInput.cs | 14 + .../GetWorkTasksOfCreatorInput.cs | 14 + .../ViewModels/WorkFlowCore/WorkTaskDto.cs | 46 +++ .../ViewModels/WorkFlowCore/WorkflowIdDto.cs | 15 + .../WorkFlowCore.Host.csproj | 2 + .../WorkFlowCore.Host/WorkFlowCore.Host.xml | 73 +++++ .../IRepositories/IWorkTaskRepository.cs | 8 + WorkFlowCore/WorkFlowCore/WorkflowManager.cs | 19 +- 18 files changed, 748 insertions(+), 13 deletions(-) create mode 100644 WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/Manifest.cs create mode 100644 WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/PluginAssemblyLoadContext.cs create mode 100644 WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/SimplePluginLoader.cs create mode 100644 WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetAllProcessingWorkTasksByEntityTypeInput.cs create mode 100644 WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetHandledWorkTasksOfUserInput.cs create mode 100644 WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetUnHandlerWorkTasksOfUserInput.cs create mode 100644 WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetWorkTasksOfCreatorInput.cs create mode 100644 WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/WorkTaskDto.cs create mode 100644 WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/WorkflowIdDto.cs diff --git a/README.md b/README.md index 14d59d7..1acbfa7 100644 --- a/README.md +++ b/README.md @@ -169,12 +169,36 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ## API +### 通用 + - 响应格式 + ``` + { + "code": "string", 编号 success|fail + "msg": "string", 对应编号的 成功|失败 + "data": //接口业务 + } + ``` + +- 部分标准实体返回: + - 这些参数 顾名思义。其中 id 为该实体的数据库标识,有时也作业业务唯一标识。 + ``` + "modifiedTime": "2023-01-08T02:43:38.640Z", //更新时间 + "modifiedUserId": "string", //更新人id + "createdUserId": "string", // + "creationTime": "2023-01-08T02:43:38.640Z", + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "deletedUserId": "string", + "deletedTime": "2023-01-08T02:43:38.640Z", + ``` + +- 接口为 非标准 restful (method 遵循) + ### 流程设计相关 在流程设计服务里,主要涉及设计和版本的管理。 设计内容则为流程的设计信息,包括节点,连线,人员,条件以及前端其它设计代码管理。 版本则为流程的版本管理,服务的设计为,一个流程可以有多个版本,随着业务的调整可以不断的调整流程,通过版本管理,可以保证现有流程与新流程区分开来,使旧流程不受新流程影响。 -- #### ***/api/WorkFlow/CreateWorkFlow*** 创建流程(设计) +- #### 创建流程(设计)***/api/WorkFlow/CreateWorkFlow*** 这里的创建很简单,这里实际上是将设计信息的创建统一放到更新操作,一来为了简化新增逻辑,统一在更新处理,二来也是配合前端的交互进行的设计。 @@ -199,7 +223,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ -- #### ***/api/WorkFlow/UpdateWorkflow*** 流程设计更新 +- #### 流程设计更新 ***/api/WorkFlow/UpdateWorkflow*** 该接口用于保存流程设计信息,当流程版本有多个时,每个版本单独存储,每个版本有自己的设计id。 - **name**:流程设计名称 @@ -421,7 +445,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` -- #### ***​/api​/WorkFlow​/GetAllWorkflows*** 该接口获取所有的流程(设计)基本信息列表 +- #### 获取所有的流程(设计)基本信息列表 ***​/api​/WorkFlow​/GetAllWorkflows*** - 返回基本信息列表 ``` [ @@ -436,7 +460,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` -- #### ***/api/WorkFlow/GetWorkflowVersion*** 根据流程id和版本号获取详细的流程设计信息。 +- #### 根据流程id和版本号获取详细的流程设计信息 ***/api/WorkFlow/GetWorkflowVersion*** 。 - **versionId** 版本号 - **id** 工作流id @@ -563,4 +587,268 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ ``` - \ No newline at end of file + + + + +### 流程审批相关 + + - #### 新建审批 ***/api/WorkFlow/CreateWorkTask*** + - + ``` + { + "workflowId": + { + "versionId": 0, + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + "name": "string", + "formData": "string", + "entityFullName": "string", + "entityKeyValue": "string", + "createdUserId": "string" + } + ``` + + - ***workflowId** :流程id :原始流程id + 版本号。通过接口 ***/api/WorkFlow/GetAllWorkflows*** 获取所有的流程设计。返回流程设计基本信息。workflowId.versionId 对应 GetAllWorkflows 结果集里的 activeVersion。 即这里要创建审批,只需要知道某个流程的以及当前应用的版本(不需要知道所有版本,原则上发起审批的人不需要知道这些)。 + + - ***name*** 审批名称(标题)。 + - ***formData*** 表单数据。这个参数是核心。表单数据可以是平台的任意格式,相应的 **userSelector** 和 **condition** 在处理过程也会传递表单数据共解析和判断。 + - ***entityFullName,entityKeyValue*** 外部系统表单类型全称和唯一标识。这两个参数主要是为了便于外部系统自己的分类和查询。在系统表单处需要查询流程信息时,借助这两个数据可以精确查询。 + - ***createdUserId*** 创建用户id。为系统平台的用户id,也可以不传,前提是自定义实现 IWorkflowSession 接口解析当前请求的用户信息。 + - **IWorkflowSession** 的抽象在 **WorkFlowCore.Authorization** 。在**WorkFlowCore.Framework** 实现自定义的 session,并在 **WorkFlowCoreFrameworkService** 替换现有的注册: + ``` + services.Replace(new ServiceDescriptor(typeof(IWorkflowSession),typeof(DefaultSession), ServiceLifetime.Scoped)); + ``` + + - 返回基本流程信息: + ``` + { + + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "deleted": true, + "workflowId": { + "versionId": 0, + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + "name": "string", + "formData": "string", + "entityFullName": "string", + "entityKeyValue": "string", + "workTaskStatus": 0, + "isProcessed": true, + "isPending": true, + "isProcessing": true, + "isSimulation": true + } + + ``` + 其中有几个的数据: + - ***workTaskStatus*** 审批状态 + ``` + { + /// + /// 待处理 + /// + Pending, + /// + /// 处理中 + /// + Processing, + /// + /// 已完成 + /// + Processed + } + ``` + + - ***isProcessed/isPending/isProcessing*** 为 **workTaskStatus** 的常用判断值 + + - ***isSimulation*** 是否是模拟流程。 该属性不在前端传递,而是通过 **创建模拟流程接口** ***/api/WorkFlow/CreateSimulationWorkTask*** 创建的任务。其参数与 **CreateWorkTask** 一致。 + +- #### 发起审批 ***/api/WorkFlow/StartWorkTask*** + - + ``` + { + "worktaskId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ``` + - ***worktaskId*** 流程审批id。 + + - 返回发起审批后的处理待审批记录(每一条记录对应 每一个审批用户): + ``` + [ + { + + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "workTaskId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "workStepType": 0, + "fromNodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "fromNodeName": "string", + "nodeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "nodeName": "string", + "handleUser": { + "id": "string", + "name": "string" + }, + "isHandled": true, + "handleType": 0, + "comment": "string", + "resourceIds": "string", + "isRead": true, + "readTime": "2023-01-08T02:43:38.627Z", + "handlerTime": "2023-01-08T02:43:38.627Z", + "groupId": "string", + "preStepGroupId": "string", + "formData": "string", + "fromForwardStepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + ] + + ``` + 其中: + - ***workStepType*** 处理类型。与 流程设计(***/api/WorkFlow/UpdateWorkflow*** )中,节点 Node 配置中的 handleType对应: 0 //处理类型。分为 只读(类似 抄送,只能看但不能处理)和处理。 + ``` + /// + /// 处理(正常需要处理的情况) + /// + Handle, + /// + /// 只读(抄送情况下) + /// + ReadOnly, + ``` + + - ***fromNodeId,fromNodeName*** 来源节点。即是从哪个节点流转到当前的(新发起的节点来源节点为 开始节点)。 + + - ***nodeId,nodeName*** 当前审批节点。 + - ***handleUser*** 当前审批人。 + - ***isHandled** 是否已经处理(**workStepType** 为”处理“的情况) + - ***handleType*** 处理方式: + ``` + { + /// + /// 通过 + /// + Pass, + /// + /// 拒绝 + /// + Reject, + /// + /// 撤回 + /// + Withdraw, + /// + /// 转发(转给某人审批) + /// + Forward, + /// + /// 未处理(在会签节点其它节点驳回,但是部分未处理的则更新为该状态) + /// + UnWork + } + + ``` + - ***handlerTime** 处理时间 + + - ***comment*** 批文。 + + - ***resourceIds*** 附件id。这里设计为只记录附件id。每一次审批都可以根据情况上传附件(附件由平台自己管理附件信息)。通常多个附件英文逗号隔开,但具体什么存和解析,由接入平台定义。 + - ***isRead,readTime*** 是否已读,以及已读时间。(目前这两个状态仅在执行”撤回(***/api/WorkFlow/WithdrawProve***)“操作时,才标记已读,设计考虑在用户查看审批明细时,也应该标记已读) + - ***groupId*** 分组id。分组id 在业务上没意义,在流程控制上便于”撤回“和”驳回“。*设计上,每一次的节点流转都是一个分组,一个节点可能会根据条件流转到一个或者多个节点上,而这些在同一个节点审批流转的审批就归属为同一组*。 + - ***preStepGroupId*** 前一组id + - ***formData*** 表单数据。这里审批的表单数据不是发起是的表单数据。下面审批接口(***/api/WorkFlow/PassProve***)会提到。每次审批也都可以有自己的表单数据,当存在这些数据并且 **用户选择器** 和 **条件处理器** 都会应用这些数据进行解析时,也会影响到流程的流转。考虑到的这个场景是应用到每次审批都有可能更新表单的情况(具体在审批过程能否编辑数据,这个得看实现情况)。 + - ***comment*** 批文。 + - ***fromForwardStepId** 来源步骤id。如果是转发才会有值。即标记是哪个审批记录转发过来的。 + + +- ### 通过审批 ***/api/WorkFlow/PassProve*** + - + ``` + { + "comment": "string", + "stepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "resourceIds": "string" + } + ``` + + - ***comment*** 批文 + - ***stepId*** 待审批记录id + - ***resourceIds*** 附件id(前面提到过)。 + - 返回发起审批后的处理待审批记录(与 发起审批 ***/api/WorkFlow/StartWorkTask*** 接口返回一致): + +- ### 驳回审批 ***/api/WorkFlow/RejectProve*** (参考发起审批 ***/api/WorkFlow/StartWorkTask***) +- ### 撤回审批 ***/api/WorkFlow/WithdrawProve*** (参考发起审批 ***/api/WorkFlow/StartWorkTask***) + +- ### 转发审批 ***/api/WorkFlow/RejectProve*** + 转发审批为转发当前审批操作转给另一个人进行审批。与其他审批处理操作相似,但多出一个传参。 + + ``` + { + "comment": "string", //批文 + "stepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", //待审批记录id + "resourceIds": "string", //附件id + "userSelectors": + [ + { + "selectorId": "string", + "selectorName": "string", + "selections": [ + { + "id": "string", + "name": "string" + } + ], + "parameter": "string", + "description": "string", + "handleType": 0 + } + ] + } + ``` + + - ***userSelectors*** 用户选择器。此处转发操作也采用用户选择器进行解析,一来统一规范,二来可扩展性也高。参考流程设计接口 (流程设计更新 ***/api/WorkFlow/UpdateWorkflow*** ) + + +- ### 获取所有审批步骤 ***/api/WorkFlow/GetAllTaskStepsOfWorkTask*** + - ***worktaskId*** 审批id。 + - 返回该审批流所有的审批记录。(与 发起审批 ***/api/WorkFlow/StartWorkTask*** 接口返回一致)。 + +- ### 获取所有审批步骤(根据entity 信息) ***/api/WorkFlow/GetAllTaskStepsOfWorkTaskByEntityInfo*** + - ***entityFullName*** 外部分类全称。 + - ***entityKeyValue*** 外部表单标识。 + - (参考 新建审批 ***/api/WorkFlow/CreateWorkTask*** 里的) + +- ### 分页获取用户待处理审批 ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** + - ***CurrentPage*** 当前页(默认从1开始) + - ***MaxResultCount*** 分页大小 + - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) + + +- ### 分页获取用户已处理审批 ***/api/WorkFlow/GetHandledWorkTasksOfUser*** + 参考 **分页获取用户待处理审批** ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** + - 这里的已处理包括自己发起的,也包括别人发起经由自己审批的。 + + +- ### 分页根据entity信息获取处理中的审批 ***​/api​/WorkFlow​/GetAllProcessingWorkTasksByEntityType*** + - ***entityFullName*** 外部分类全称。 + - ***entityKeyValues*** 外部表单标识,多个之间通过英文逗号隔开。 + - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) + +- ### 分页获取用发起的审批 ***/api/WorkFlow/GetWorkTasksOfCreator*** + - ***CurrentPage*** 当前页(默认从1开始) + - ***MaxResultCount*** 分页大小 + - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) + + + + + + + + + diff --git a/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/Manifest.cs b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/Manifest.cs new file mode 100644 index 0000000..9e4e8a4 --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/Manifest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace WorkFlowCore.Common.SimplePluginLoaders +{ + public class Manifest + { + public string Entry { get; set; } + public string Version { get; set; } + public Assembly Assembly { get; set; } + } +} diff --git a/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/PluginAssemblyLoadContext.cs b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/PluginAssemblyLoadContext.cs new file mode 100644 index 0000000..58bfb56 --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/PluginAssemblyLoadContext.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; + +namespace WorkFlowCore.Common.SimplePluginLoaders +{ + public class PluginAssemblyLoadContext : AssemblyLoadContext + { + private AssemblyDependencyResolver _resolver; + + public PluginAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath); + } + + protected override Assembly Load(AssemblyName name) + { + string assemblyPath = _resolver.ResolveAssemblyToPath(name); + if (assemblyPath != null) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } + } +} diff --git a/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/SimplePluginLoader.cs b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/SimplePluginLoader.cs new file mode 100644 index 0000000..aaeff07 --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/SimplePluginLoader.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; + +namespace WorkFlowCore.Common.SimplePluginLoaders +{ + public static class SimplePluginLoader + { + /// + /// 载入插件 + /// + /// + /// + /// + /// + /// + public static IServiceCollection LoadPlugins(this IServiceCollection services, string pluginDir, Action> loaded) where TManifest : Manifest + { + if (!Directory.Exists(pluginDir)) return services; + + var plugins = new List(); + + var dirs = Directory.GetDirectories(pluginDir); + foreach (var dir in dirs) + { + var manifestPath = Path.Combine(dir, "manifest.json"); + if (!File.Exists(manifestPath)) continue; + try + { + var manifest = JsonConvert.DeserializeObject(File.ReadAllText(manifestPath)); + if (manifest == null) continue; + + var context = new PluginAssemblyLoadContext(System.IO.Path.Combine(dir, "src", manifest.Entry)); + var EntryLib = manifest.Entry; + var assembly = context.LoadFromAssemblyName(new System.Reflection.AssemblyName(EntryLib.Substring(0, EntryLib.Length - 4))); + manifest.Assembly = assembly; + plugins.Add(manifest); + } + catch (Exception ex) + { + + // + } + + } + loaded?.Invoke(plugins); + + return services; + } + + public static IServiceCollection LoadPlugins(this IServiceCollection services, string pluginDir, Action> loaded) + { + return LoadPlugins(services, pluginDir, loaded); + } + } +} diff --git a/WorkFlowCore/WorkFlowCore.Framework/Repositories/WorkTaskRepository.cs b/WorkFlowCore/WorkFlowCore.Framework/Repositories/WorkTaskRepository.cs index 031f57d..8517ebe 100644 --- a/WorkFlowCore/WorkFlowCore.Framework/Repositories/WorkTaskRepository.cs +++ b/WorkFlowCore/WorkFlowCore.Framework/Repositories/WorkTaskRepository.cs @@ -93,5 +93,23 @@ namespace WorkFlowCore.Framework.Repositories else result.Items = (await GetListAsync(wt => workTaskIds.Contains(wt.Id))).OrderByDescending(ws => ws.CreationTime).Select(ws => ws.ToWorkTask()).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); return await Task.FromResult(result); } + + public async Task> GetWorkTasksOfCreatorAsync(string userId, int pageIndex = 1, int pageSize = -1) + { + var workTaskIds = (await workStepRepository.GetListAsync(ws => ws.CreatedUserId == userId && ws.IsHandled)).Select(ws => ws.WorkTaskId); + + var workTasks = (await GetListAsync(wt => workTaskIds.Contains(wt.Id) && !wt.IsSimulation)).OrderByDescending(ws => ws.CreationTime); + + var result = new PageResult + { + Total = await GetCountAsync(wt => workTaskIds.Contains(wt.Id) && !wt.IsSimulation) + }; + + if (pageSize < 1) + result.Items = (await Task.FromResult(workTasks.Select(w => w.ToWorkTask()))).ToList(); + else result.Items = (await Task.FromResult(workTasks.Select(w => w.ToWorkTask()).Skip((pageIndex - 1) * pageSize).Take(pageSize))).ToList(); + + return await Task.FromResult(result); + } } } diff --git a/WorkFlowCore/WorkFlowCore.Framework/Repositories4EF/WorkTaskRepository4EF.cs b/WorkFlowCore/WorkFlowCore.Framework/Repositories4EF/WorkTaskRepository4EF.cs index 071650f..bc02c86 100644 --- a/WorkFlowCore/WorkFlowCore.Framework/Repositories4EF/WorkTaskRepository4EF.cs +++ b/WorkFlowCore/WorkFlowCore.Framework/Repositories4EF/WorkTaskRepository4EF.cs @@ -101,5 +101,18 @@ namespace WorkFlowCore.Framework.Repositories4EF else result.Items = worktaskQuery.Skip((pageIndex - 1) * pageSize).Take(pageSize).OrderByDescending(w => w.CreationTime).Select(w => w.ToWorkTask()).ToList(); return await Task.FromResult(result); } + + public async Task> GetWorkTasksOfCreatorAsync(string userId, int pageIndex = 1, int pageSize = -1) + { + var worktaskQuery = workflowDbContext.Set().Where(wt => !wt.Deleted && wt.CreatedUserId == userId && !wt.IsSimulation); + var result = new PageResult + { + Total = worktaskQuery.Count() + }; + if (pageSize < 1) + result.Items = worktaskQuery.Select(w => w.ToWorkTask()).ToList(); + else result.Items = worktaskQuery.Skip((pageIndex - 1) * pageSize).Take(pageSize).OrderByDescending(w => w.CreationTime).Select(w => w.ToWorkTask()).ToList(); + return await Task.FromResult(result); + } } } diff --git a/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs b/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs index 529e975..bb91d42 100644 --- a/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs +++ b/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using System; using System.Collections.Generic; using System.Text; @@ -62,8 +63,7 @@ namespace WorkFlowCore.Framework services.AddScoped(); var assembly = typeof(WorkFlowCoreFrameworkService).Assembly; - - services.AddScoped(); + services.Replace(new ServiceDescriptor(typeof(IWorkflowSession),typeof(DefaultSession), ServiceLifetime.Scoped)); //注册条件和选择器 UserSelectorManager.RegisterSelector(assembly); diff --git a/WorkFlowCore/WorkFlowCore.Host/Controllers/WorkFlowController.cs b/WorkFlowCore/WorkFlowCore.Host/Controllers/WorkFlowController.cs index 9dc309e..53c48a5 100644 --- a/WorkFlowCore/WorkFlowCore.Host/Controllers/WorkFlowController.cs +++ b/WorkFlowCore/WorkFlowCore.Host/Controllers/WorkFlowController.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using WorkFlowCore.Authorization; using WorkFlowCore.Conditions; using WorkFlowCore.Framework.UserSelectors; using WorkFlowCore.Host.ViewModels; @@ -11,6 +12,7 @@ using WorkFlowCore.IRepositories; using WorkFlowCore.UserSelectors; using WorkFlowCore.Workflows; using WorkFlowCore.WorkTasks; +using Mapster; namespace WorkFlowCore.Host.Controllers { @@ -21,12 +23,13 @@ namespace WorkFlowCore.Host.Controllers private ConditionManager conditionManager; private WorkflowManager workflowManager; private IBasicRepository workflowRepository; - private IBasicRepository worktaskRepository; + private IWorkTaskRepository worktaskRepository; private IBasicRepository versionRepository; private IBasicRepository workStepRepository; + private IWorkflowSession workflowSession; - public WorkFlowController(ConditionManager conditionManager, WorkflowManager workflowManager, IBasicRepository workflowRepository, IBasicRepository worktaskRepository, IBasicRepository versionRepository, IBasicRepository workStepRepository) + public WorkFlowController(ConditionManager conditionManager, WorkflowManager workflowManager, IBasicRepository workflowRepository, IWorkTaskRepository worktaskRepository, IBasicRepository versionRepository, IBasicRepository workStepRepository) { this.conditionManager = conditionManager; this.workflowManager = workflowManager; @@ -160,6 +163,18 @@ namespace WorkFlowCore.Host.Controllers return OutputDto.Succeed(worktask); } + /// + /// 创建流程任务 + /// + /// + /// + [HttpPost("CreateSimulationWorkTask")] + public async Task> CreateSimulationWorkTask(CreateWorkTaskInput input) + { + var worktask = await workflowManager.CreateSimulationWorkTask(input.WorkflowId, input.Name, input.FormData, input.EntityFullName, input.EntityKeyValue, input.CreatedUserId); + return OutputDto.Succeed(worktask); + } + /// /// 获取流程任务 /// @@ -272,8 +287,93 @@ namespace WorkFlowCore.Host.Controllers { return OutputDto.Succeed>(UserList.Users.Select(u=>new UserSelectors.User { Id=u.Id,Name=u.Name}).ToList()); } - - + /// + /// 获取所有审批步骤 + /// + /// + /// + [HttpGet("GetAllTaskStepsOfWorkTaskByEntityInfo")] + public async Task> GetAllTaskStepsOfWorkTaskByEntityInfo(string entityFullName, string entityKeyValue) + { + //获取所有过程输出 + var historySteps = await workflowManager.GetAllTaskStepsOfWorkTaskByEntityInfoAsync(entityFullName, entityKeyValue); + return historySteps; + } + + + + private async Task FillWorktaskInfo(List workTasks) + { + var workflows = await workflowRepository.GetListAsync(w => workTasks.Select(s => s.WorkflowId.Id).Contains(w.Id)); + workTasks.ForEach(i => + { + i.WorkflowId.WorkflowName = workflows.FirstOrDefault(w => w.Id == i.WorkflowId.Id && w.ActiveVersion == i.WorkflowId.VersionId)?.Name; + }); + } + + /// + /// 获取用户待处理的流程 + /// + /// + [HttpGet("GetUnHandledWorkTasksOfUser")] + public async Task> GetUnHandledWorkTasksOfUser([FromQuery] GetUnHandledWorkTasksOfUserInput input) + { + var worktasks = await workflowManager.GetUnHandledWorkTasksOfUserAsync(workflowSession.User.Id, input.CurrentPage, input.MaxResultCount); + var result = new PageResult + { + Items = worktasks.Items.Adapt>(),// mapper.Map, List>(worktasks.Items), + Total = worktasks.Total + }; + await FillWorktaskInfo(result.Items); + + return result; + + } + + /// + /// 获取用户已处理的流程 + /// + /// + [HttpGet("GetHandledWorkTasksOfUser")] + public async Task> GetHandledWorkTasksOfUser([FromQuery] GetHandledWorkTasksOfUserInput input) + { + var worktasks = await workflowManager.GetHandledWorkTasksOfUserAsync(workflowSession.User.Id, input.CurrentPage, input.MaxResultCount); + var result = new PageResult + { + Items = worktasks.Items.Adapt>(),// mapper.Map, List>(worktasks.Items), + Total = worktasks.Total + }; + + await FillWorktaskInfo(result.Items); + + return result; + } + /// + /// 根据实体类型获取所有的处理中的工作流 + /// + /// + /// + [HttpGet("GetAllProcessingWorkTasksByEntityType")] + public async Task> GetAllProcessingWorkTasksByEntityType([FromQuery] GetAllProcessingWorkTasksByEntityTypeInput input) + { + var worktaskInfos = await worktaskRepository.GetListAsync(wt => input.EntityKeyValues.Contains(wt.EntityKeyValue) && !wt.Deleted && wt.EntityFullName == input.EntityFullName); + return worktaskInfos.Select(wt => wt.ToWorkTask()).ToList(); + } + [HttpGet("GetWorkTasksOfCreator")] + public async Task> GetWorkTasksOfCreator([FromQuery] GetWorkTasksOfCreatorInput input) + { + var worktasks = await worktaskRepository.GetWorkTasksOfCreatorAsync(workflowSession.User.Id, input.CurrentPage, input.MaxResultCount); + var result = new PageResult + { + Items = worktasks.Items.Adapt>(), //mapper.Map, List>(worktasks.Items), + Total = worktasks.Total + }; + + await FillWorktaskInfo(result.Items); + + return result; + } + } } diff --git a/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetAllProcessingWorkTasksByEntityTypeInput.cs b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetAllProcessingWorkTasksByEntityTypeInput.cs new file mode 100644 index 0000000..86900b8 --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetAllProcessingWorkTasksByEntityTypeInput.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WorkFlowCore.Host.ViewModels.WorkFlowCore +{ + public class GetAllProcessingWorkTasksByEntityTypeInput + { + public string EntityFullName { get; set; } + public string[] EntityKeyValues { get;set; } + } +} diff --git a/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetHandledWorkTasksOfUserInput.cs b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetHandledWorkTasksOfUserInput.cs new file mode 100644 index 0000000..1edbcf4 --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetHandledWorkTasksOfUserInput.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace WorkFlowCore.Host.ViewModels.WorkFlowCore +{ + public class GetHandledWorkTasksOfUserInput + { + public int CurrentPage { get; set; } = 1; + public int MaxResultCount { get; set; } = -1; + } + +} diff --git a/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetUnHandlerWorkTasksOfUserInput.cs b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetUnHandlerWorkTasksOfUserInput.cs new file mode 100644 index 0000000..7571e42 --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetUnHandlerWorkTasksOfUserInput.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace WorkFlowCore.Host.ViewModels.WorkFlowCore +{ + public class GetUnHandledWorkTasksOfUserInput + { + public int CurrentPage { get; set; } = 1; + public int MaxResultCount { get; set; } = -1; + } + +} diff --git a/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetWorkTasksOfCreatorInput.cs b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetWorkTasksOfCreatorInput.cs new file mode 100644 index 0000000..eb559da --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/GetWorkTasksOfCreatorInput.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace WorkFlowCore.Host.ViewModels.WorkFlowCore +{ + public class GetWorkTasksOfCreatorInput + { + public int CurrentPage { get; set; } = 1; + public int MaxResultCount { get; set; } = -1; + } + +} diff --git a/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/WorkTaskDto.cs b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/WorkTaskDto.cs new file mode 100644 index 0000000..6df6e7a --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/WorkTaskDto.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using WorkFlowCore.IRepositories; +using WorkFlowCore.Workflows; +using WorkFlowCore.WorkTasks; + +namespace WorkFlowCore.Host.ViewModels.WorkFlowCore +{ + /// + /// 工作流dto + /// + public class WorkTaskDto: WithBaseInfoEntity + { + /// + /// 流程id + /// + public WorkflowIdDto WorkflowId { get; set; } + /// + /// 任务名称 + /// + public string Name { get; set; } + + /// + /// 表单数据(json) + /// + public string FormData { get; set; } + /// + /// 实体全称 + /// + public string EntityFullName { get; set; } + /// + /// 实体主键值 + /// + public string EntityKeyValue { get; set; } + /// + /// 审批状态 + /// + public WorkTaskStatus WorkTaskStatus { get; set; } = WorkTaskStatus.Pending; + + public bool IsProcessed { get => WorkTaskStatus == WorkTaskStatus.Processed; } + public bool IsPending { get => WorkTaskStatus == WorkTaskStatus.Pending; } + public bool IsProcessing { get => WorkTaskStatus == WorkTaskStatus.Processing; } + public bool IsSimulation { get; set; } + } +} diff --git a/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/WorkflowIdDto.cs b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/WorkflowIdDto.cs new file mode 100644 index 0000000..eedcbef --- /dev/null +++ b/WorkFlowCore/WorkFlowCore.Host/ViewModels/WorkFlowCore/WorkflowIdDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using WorkFlowCore.Workflows; + +namespace WorkFlowCore.Host.ViewModels.WorkFlowCore +{ + public class WorkflowIdDto: WorkflowId + { + /// + /// 流程名称 + /// + public string WorkflowName { get; set; } + } +} diff --git a/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.csproj b/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.csproj index 9c9fb5d..f5b6934 100644 --- a/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.csproj +++ b/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.csproj @@ -15,6 +15,8 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.xml b/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.xml index de5260b..896c91c 100644 --- a/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.xml +++ b/WorkFlowCore/WorkFlowCore.Host/WorkFlowCore.Host.xml @@ -78,6 +78,13 @@ + + + 创建流程任务 + + + + 获取流程任务 @@ -139,6 +146,32 @@ + + + 获取所有审批步骤 + + + + + + + 获取用户待处理的流程 + + + + + + 获取用户已处理的流程 + + + + + + 根据实体类型获取所有的处理中的工作流 + + + + 流程id @@ -234,6 +267,11 @@ 流程连线 + + + 流程名称 + + 绘制信息,前端绘制所需信息 @@ -269,5 +307,40 @@ 拒绝节点 + + + 工作流dto + + + + + 流程id + + + + + 任务名称 + + + + + 表单数据(json) + + + + + 实体全称 + + + + + 实体主键值 + + + + + 审批状态 + + diff --git a/WorkFlowCore/WorkFlowCore/IRepositories/IWorkTaskRepository.cs b/WorkFlowCore/WorkFlowCore/IRepositories/IWorkTaskRepository.cs index 5e84b68..1f005c5 100644 --- a/WorkFlowCore/WorkFlowCore/IRepositories/IWorkTaskRepository.cs +++ b/WorkFlowCore/WorkFlowCore/IRepositories/IWorkTaskRepository.cs @@ -13,5 +13,13 @@ namespace WorkFlowCore.IRepositories Task> GetUnHandledWorkTasksOfUserAsync(string userId, int pageIndex = 1, int pageSize = -1); Task> GetHandledWorkTasksOfUserAsync(string userId, int pageIndex = 1, int pageSize = -1); Task> GetTasksOfStartUserAsync(string userId, int pageIndex = 1, int pageSize = -1); + /// + /// 获取用户发起的流程 + /// + /// + /// + /// + /// + Task> GetWorkTasksOfCreatorAsync(string userId, int pageIndex = 1, int pageSize = -1); } } diff --git a/WorkFlowCore/WorkFlowCore/WorkflowManager.cs b/WorkFlowCore/WorkFlowCore/WorkflowManager.cs index 63bcf4c..fbc53e7 100644 --- a/WorkFlowCore/WorkFlowCore/WorkflowManager.cs +++ b/WorkFlowCore/WorkFlowCore/WorkflowManager.cs @@ -1099,6 +1099,7 @@ namespace WorkFlowCore return userSelector.GetSelections(); } + /// /// 删除流程 /// 同时删除审批记录 @@ -1107,10 +1108,24 @@ namespace WorkFlowCore /// public async Task DeleteWorkTask(Guid worktaskId) { - await workStepRepository.DeleteManyAsync(ws => ws.WorkTaskId == worktaskId); - await workTaskRepository.DeleteAsync(worktaskId); + var worktaskInfo = await workTaskRepository.GetAsync(worktaskId); + + if (worktaskInfo == null) + throw new Exception($"查无此项:{worktaskId}"); + + var worktask = worktaskInfo.ToWorkTask(); + + if (!worktask.IsPending) + throw new Exception("非待审批状态无法删除"); + + using (var unitOfWork = unitOfWorkManager.Begin()) + { + await workStepRepository.DeleteManyAsync(ws => ws.WorkTaskId == worktaskId); + await workTaskRepository.DeleteAsync(worktaskId); + } } + #endregion } } -- Gitee From a08edac0d3ce924070edc6cc29b8e1f66c88be04 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 8 Jan 2023 14:11:22 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 155 +++++++++++++++++++++++++++--------------------------- 1 file changed, 78 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 1acbfa7..8ab02f1 100644 --- a/README.md +++ b/README.md @@ -666,7 +666,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ - ***isSimulation*** 是否是模拟流程。 该属性不在前端传递,而是通过 **创建模拟流程接口** ***/api/WorkFlow/CreateSimulationWorkTask*** 创建的任务。其参数与 **CreateWorkTask** 一致。 -- #### 发起审批 ***/api/WorkFlow/StartWorkTask*** + - #### 发起审批 ***/api/WorkFlow/StartWorkTask*** - ``` { @@ -763,7 +763,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ - ***fromForwardStepId** 来源步骤id。如果是转发才会有值。即标记是哪个审批记录转发过来的。 -- ### 通过审批 ***/api/WorkFlow/PassProve*** + - ### 通过审批 ***/api/WorkFlow/PassProve*** - ``` { @@ -777,78 +777,79 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ - ***stepId*** 待审批记录id - ***resourceIds*** 附件id(前面提到过)。 - 返回发起审批后的处理待审批记录(与 发起审批 ***/api/WorkFlow/StartWorkTask*** 接口返回一致): - -- ### 驳回审批 ***/api/WorkFlow/RejectProve*** (参考发起审批 ***/api/WorkFlow/StartWorkTask***) -- ### 撤回审批 ***/api/WorkFlow/WithdrawProve*** (参考发起审批 ***/api/WorkFlow/StartWorkTask***) - -- ### 转发审批 ***/api/WorkFlow/RejectProve*** - 转发审批为转发当前审批操作转给另一个人进行审批。与其他审批处理操作相似,但多出一个传参。 - - ``` - { - "comment": "string", //批文 - "stepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", //待审批记录id - "resourceIds": "string", //附件id - "userSelectors": - [ - { - "selectorId": "string", - "selectorName": "string", - "selections": [ - { - "id": "string", - "name": "string" - } - ], - "parameter": "string", - "description": "string", - "handleType": 0 - } - ] - } - ``` - - - ***userSelectors*** 用户选择器。此处转发操作也采用用户选择器进行解析,一来统一规范,二来可扩展性也高。参考流程设计接口 (流程设计更新 ***/api/WorkFlow/UpdateWorkflow*** ) - - -- ### 获取所有审批步骤 ***/api/WorkFlow/GetAllTaskStepsOfWorkTask*** - - ***worktaskId*** 审批id。 - - 返回该审批流所有的审批记录。(与 发起审批 ***/api/WorkFlow/StartWorkTask*** 接口返回一致)。 - -- ### 获取所有审批步骤(根据entity 信息) ***/api/WorkFlow/GetAllTaskStepsOfWorkTaskByEntityInfo*** - - ***entityFullName*** 外部分类全称。 - - ***entityKeyValue*** 外部表单标识。 - - (参考 新建审批 ***/api/WorkFlow/CreateWorkTask*** 里的) - -- ### 分页获取用户待处理审批 ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** - - ***CurrentPage*** 当前页(默认从1开始) - - ***MaxResultCount*** 分页大小 - - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) - - -- ### 分页获取用户已处理审批 ***/api/WorkFlow/GetHandledWorkTasksOfUser*** - 参考 **分页获取用户待处理审批** ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** - - 这里的已处理包括自己发起的,也包括别人发起经由自己审批的。 - - -- ### 分页根据entity信息获取处理中的审批 ***​/api​/WorkFlow​/GetAllProcessingWorkTasksByEntityType*** - - ***entityFullName*** 外部分类全称。 - - ***entityKeyValues*** 外部表单标识,多个之间通过英文逗号隔开。 - - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) - -- ### 分页获取用发起的审批 ***/api/WorkFlow/GetWorkTasksOfCreator*** - - ***CurrentPage*** 当前页(默认从1开始) - - ***MaxResultCount*** 分页大小 - - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) - - - - - - - - - + + - ### 驳回审批 ***/api/WorkFlow/RejectProve*** (参考发起审批 ***/api/WorkFlow/StartWorkTask***) + - ### 撤回审批 ***/api/WorkFlow/WithdrawProve*** (参考发起审批 ***/api/WorkFlow/ StartWorkTask***) + + - ### 转发审批 ***/api/WorkFlow/RejectProve*** + 转发审批为转发当前审批操作转给另一个人进行审批。与其他审批处理操作相似,但多出一个传参。 + + ``` + { + "comment": "string", //批文 + "stepId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", //待审批记录id + "resourceIds": "string", //附件id + "userSelectors": + [ + { + "selectorId": "string", + "selectorName": "string", + "selections": [ + { + "id": "string", + "name": "string" + } + ], + "parameter": "string", + "description": "string", + "handleType": 0 + } + ] + } + ``` + + - ***userSelectors*** 用户选择器。此处转发操作也采用用户选择器进行解析,一来统一规范,二来可扩展性也 高。参考流程设计接口 (流程设计更新 ***/api/WorkFlow/UpdateWorkflow*** ) + + + - ### 获取所有审批步骤 ***/api/WorkFlow/GetAllTaskStepsOfWorkTask*** + - ***worktaskId*** 审批id。 + - 返回该审批流所有的审批记录。(与 发起审批 ***/api/WorkFlow/StartWorkTask*** 接口返回一致)。 + + - ### 获取所有审批步骤(根据entity 信息) ***/api/WorkFlow/ GetAllTaskStepsOfWorkTaskByEntityInfo*** + - ***entityFullName*** 外部分类全称。 + - ***entityKeyValue*** 外部表单标识。 + - (参考 新建审批 ***/api/WorkFlow/CreateWorkTask*** 里的) + + - ### 分页获取用户待处理审批 ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** + - ***CurrentPage*** 当前页(默认从1开始) + - ***MaxResultCount*** 分页大小 + - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) + + + - ### 分页获取用户已处理审批 ***/api/WorkFlow/GetHandledWorkTasksOfUser*** + 参考 **分页获取用户待处理审批** ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** + - 这里的已处理包括自己发起的,也包括别人发起经由自己审批的。 + + + - ### 分页根据entity信息获取处理中的审批 ***​/api​/WorkFlow​/ GetAllProcessingWorkTasksByEntityType*** + - ***entityFullName*** 外部分类全称。 + - ***entityKeyValues*** 外部表单标识,多个之间通过英文逗号隔开。 + - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) + + - ### 分页获取用发起的审批 ***/api/WorkFlow/GetWorkTasksOfCreator*** + - ***CurrentPage*** 当前页(默认从1开始) + - ***MaxResultCount*** 分页大小 + - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) + + + + + + + + + + \ No newline at end of file -- Gitee From 2710870c3225154c2795a91d155506ec501cd1e9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 8 Jan 2023 14:13:00 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8ab02f1..5107956 100644 --- a/README.md +++ b/README.md @@ -763,7 +763,7 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ - ***fromForwardStepId** 来源步骤id。如果是转发才会有值。即标记是哪个审批记录转发过来的。 - - ### 通过审批 ***/api/WorkFlow/PassProve*** + - #### 通过审批 ***/api/WorkFlow/PassProve*** - ``` { @@ -778,10 +778,10 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ - ***resourceIds*** 附件id(前面提到过)。 - 返回发起审批后的处理待审批记录(与 发起审批 ***/api/WorkFlow/StartWorkTask*** 接口返回一致): - - ### 驳回审批 ***/api/WorkFlow/RejectProve*** (参考发起审批 ***/api/WorkFlow/StartWorkTask***) - - ### 撤回审批 ***/api/WorkFlow/WithdrawProve*** (参考发起审批 ***/api/WorkFlow/ StartWorkTask***) + - #### 驳回审批 ***/api/WorkFlow/RejectProve*** (参考发起审批 ***/api/WorkFlow/StartWorkTask***) + - #### 撤回审批 ***/api/WorkFlow/WithdrawProve*** (参考发起审批 ***/api/WorkFlow/ StartWorkTask***) - - ### 转发审批 ***/api/WorkFlow/RejectProve*** + - #### 转发审批 ***/api/WorkFlow/RejectProve*** 转发审批为转发当前审批操作转给另一个人进行审批。与其他审批处理操作相似,但多出一个传参。 ``` @@ -811,32 +811,32 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ - ***userSelectors*** 用户选择器。此处转发操作也采用用户选择器进行解析,一来统一规范,二来可扩展性也 高。参考流程设计接口 (流程设计更新 ***/api/WorkFlow/UpdateWorkflow*** ) - - ### 获取所有审批步骤 ***/api/WorkFlow/GetAllTaskStepsOfWorkTask*** + - #### 获取所有审批步骤 ***/api/WorkFlow/GetAllTaskStepsOfWorkTask*** - ***worktaskId*** 审批id。 - 返回该审批流所有的审批记录。(与 发起审批 ***/api/WorkFlow/StartWorkTask*** 接口返回一致)。 - - ### 获取所有审批步骤(根据entity 信息) ***/api/WorkFlow/ GetAllTaskStepsOfWorkTaskByEntityInfo*** + - #### 获取所有审批步骤(根据entity 信息) ***/api/WorkFlow/ GetAllTaskStepsOfWorkTaskByEntityInfo*** - ***entityFullName*** 外部分类全称。 - ***entityKeyValue*** 外部表单标识。 - (参考 新建审批 ***/api/WorkFlow/CreateWorkTask*** 里的) - - ### 分页获取用户待处理审批 ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** + - #### 分页获取用户待处理审批 ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** - ***CurrentPage*** 当前页(默认从1开始) - ***MaxResultCount*** 分页大小 - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) - - ### 分页获取用户已处理审批 ***/api/WorkFlow/GetHandledWorkTasksOfUser*** + - #### 分页获取用户已处理审批 ***/api/WorkFlow/GetHandledWorkTasksOfUser*** 参考 **分页获取用户待处理审批** ***/api/WorkFlow/GetUnHandledWorkTasksOfUser*** - 这里的已处理包括自己发起的,也包括别人发起经由自己审批的。 - - ### 分页根据entity信息获取处理中的审批 ***​/api​/WorkFlow​/ GetAllProcessingWorkTasksByEntityType*** + - #### 分页根据entity信息获取处理中的审批 ***​/api​/WorkFlow​/ GetAllProcessingWorkTasksByEntityType*** - ***entityFullName*** 外部分类全称。 - ***entityKeyValues*** 外部表单标识,多个之间通过英文逗号隔开。 - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) - - ### 分页获取用发起的审批 ***/api/WorkFlow/GetWorkTasksOfCreator*** + - #### 分页获取用发起的审批 ***/api/WorkFlow/GetWorkTasksOfCreator*** - ***CurrentPage*** 当前页(默认从1开始) - ***MaxResultCount*** 分页大小 - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) -- Gitee From 0c3a63c9cfce0ebc87635728d639994331e36760 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 8 Jan 2023 16:42:52 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3=20?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E7=AE=80=E6=98=93=E7=9A=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E3=80=81=E6=9D=A1=E4=BB=B6=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=85=B6=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 66 +++++++++++++++++- .../Condition_PluginDemo/ConditionDemo.cs | 16 +++++ .../Condition_PluginDemo.csproj | 28 ++++++++ .../libs/WorkFlowCore.Common.dll | Bin 0 -> 17920 bytes .../libs/WorkFlowCore.dll | Bin 0 -> 102400 bytes .../Condition_PluginDemo/manifest.json | 4 ++ .../UserSelectorDemo.cs | 47 +++++++++++++ .../UserSelector_PluginDemo.csproj | 17 +++++ .../UserSelector_PluginDemo/manifest.json | 4 ++ ...2\344\273\266\347\233\256\345\275\225.png" | Bin 0 -> 17643 bytes .../PluginAssemblyLoadContext.cs | 7 +- .../SimplePluginLoaders/SimplePluginLoader.cs | 3 +- .../WorkFlowCoreFrameworkService.cs | 32 +++++++-- WorkFlowCore/WorkFlowCore.sln | 24 +++++++ .../Conditions/ConditionManager.cs | 64 ++++++++++++++++- .../UserSelectors/UserSelectorManager.cs | 44 +++++++++++- 16 files changed, 340 insertions(+), 16 deletions(-) create mode 100644 WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/ConditionDemo.cs create mode 100644 WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/Condition_PluginDemo.csproj create mode 100644 WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/libs/WorkFlowCore.Common.dll create mode 100644 WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/libs/WorkFlowCore.dll create mode 100644 WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/manifest.json create mode 100644 WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/UserSelectorDemo.cs create mode 100644 WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/UserSelector_PluginDemo.csproj create mode 100644 WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/manifest.json create mode 100644 "WorkFlowCore/ReadmeImges/\347\224\250\346\210\267\351\200\211\346\213\251\345\231\250\346\217\222\344\273\266\347\233\256\345\275\225.png" diff --git a/README.md b/README.md index 5107956..3f32806 100644 --- a/README.md +++ b/README.md @@ -841,14 +841,74 @@ https://gitee.com/mengtree/workflow-engine/tree/sample/ - ***MaxResultCount*** 分页大小 - 返回流程审批集合.(属性参考 **新建审批** ***/api/WorkFlow/CreateWorkTask***) + - #### 接口现状 + - 目前已实现的接口只能保证基本的流程运作,实际上应用过程可能还有更多的场景没考虑到。特别是对于查询接口,这就需要根据业务自己实现。 - - + 通过自定义实现 IUserSelector 接口实现所需的用户选择器。 + - 实现插件(具体参考 Plugins/UserSelectors/UserSelector_PluginDemo) + - 新建自定义类库 + - 引入项目依赖 WorkFlowCore,或者手动引入dll (WorkFlowCore.Common.dll,WorkFlowCore.dll) + - 实现自定义逻辑。并创建对应的 manifest.json + ``` + { + "Entry": "Condition_PluginDemo.dll", //入口文件dll + "Version": "1.0" //版本号(目前没应用到,预留) + } + + ``` + - 编译生成。并将生成的文件按如下结构按如下格式复制到 Host发布目录下 Plugins\UserSelectors + ![子域间关系](/WorkFlowCore/ReadmeImges/用户选择器插件目录.png ) + + - 重启(启动) Host 服务 + - 目前插件的自定义实现类暂时还无法通过ioc 注入参数。除非插件自己维护 ServiceProvider。 + + + + ### 条件处理器 + 通过自定义实现 ICondition 接口实现所需的用户选择器。 + - 实现插件(具体参考 Plugins/Conditions/UserSelector_PluginDemo) + - 其他步骤类似用户选择器。只是生成后的插件导入到Host 的路径为 Plugins\Conditions + +- + +### 自定义session 实现 +- 自定义实现 IWorkflowSession 接口解析当前请求的用户信息。 + - **IWorkflowSession** 的抽象在 **WorkFlowCore.Authorization** 。在**WorkFlowCore.Framework** 实现自定义的 session,并在 **WorkFlowCoreFrameworkService** 替换现有的注册: + ``` + services.Replace(new ServiceDescriptor(typeof(IWorkflowSession),typeof(DefaultSession), ServiceLifetime.Scoped)); + ``` + +### 切换数据库读写工具 +- 目前默认使用 EF 进行数据库读写。参考(***项目运行***切换)。 +- 如需要实现其他数据库工具,则需要在 **WorkFlowCore.Framework** 实现 WorkflowCore.IRepositories 下的所有接口。并在**WorkFlowCoreFrameworkService** 替换现有的注册,类似: + ``` + if (conf.OrmType== FrameworkConfigOrmType.Default) + { + services.AddScoped(typeof(IBasicRepository<,>), typeof(BasicRepository<,>)); + services.AddScoped(typeof(IBasicRepository<>), typeof(BasicRepository<>)); + services.AddScoped(typeof(IWorkStepRepository), typeof(WorkStepRepository)); + services.AddScoped(typeof(IWorkTaskRepository), typeof(WorkTaskRepository)); + services.AddScoped(typeof(IUnitOfWork), typeof(UnitOfWork)); + services.AddScoped(typeof(UnitOfWork)); + services.AddScoped(typeof(IUnitOfWorkManager), typeof(UnitOfWorkManager)); + } + else if (conf.OrmType == FrameworkConfigOrmType.EF) + { + services.AddScoped(typeof(IBasicRepository<,>), typeof(BasicRepository4EF<,>)); + services.AddScoped(typeof(IBasicRepository<>), typeof(BasicRepository4EF<>)); + services.AddScoped(typeof(IWorkStepRepository), typeof(WorkStepRepository4EF)); + services.AddScoped(typeof(IWorkTaskRepository), typeof(WorkTaskRepository4EF)); + services.AddScoped(typeof(IUnitOfWork), typeof(UnitOfWork4EF)); + services.AddScoped(typeof(IUnitOfWorkManager), typeof(UnitOfWorkManager4EF)); + } + ``` + ### 自定义 EventBus + - 实现 **WorkFlowCore.Common.EventBus** 下 ***IEventBus*** 接口,参考 **DefaultEventBus** 实现。并在 **EventBusService** 进行服务的注册扩展。之后在 Setup services 配置载入即可。 diff --git a/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/ConditionDemo.cs b/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/ConditionDemo.cs new file mode 100644 index 0000000..8972534 --- /dev/null +++ b/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/ConditionDemo.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using WorkFlowCore.Conditions; + +namespace Condition_PluginDemo +{ + [Condition("条件处理器插件demo", "这只是一个demo,当传入参数(parameter)为 'true'(包括引号) 时通过")] + public class ConditionDemo : ICondition + { + public bool CanAccept(ConditionInput input) + { + return "true".Equals(input.Expression); + } + } +} diff --git a/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/Condition_PluginDemo.csproj b/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/Condition_PluginDemo.csproj new file mode 100644 index 0000000..6b5d614 --- /dev/null +++ b/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/Condition_PluginDemo.csproj @@ -0,0 +1,28 @@ + + + + netcoreapp3.1 + + + + + libs\WorkFlowCore.dll + + + libs\WorkFlowCore.Common.dll + + + + + + Never + + + Never + + + Always + + + + diff --git a/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/libs/WorkFlowCore.Common.dll b/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/libs/WorkFlowCore.Common.dll new file mode 100644 index 0000000000000000000000000000000000000000..bfd62e75139d867f57cb32dbb2d605d6540edba1 GIT binary patch literal 17920 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 J_y3*+{ts6qhuQ!D literal 0 HcmV?d00001 diff --git a/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/manifest.json b/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/manifest.json new file mode 100644 index 0000000..7486851 --- /dev/null +++ b/WorkFlowCore/Plugins/Conditions/Condition_PluginDemo/manifest.json @@ -0,0 +1,4 @@ +{ + "Entry": "Condition_PluginDemo.dll", + "Version": "1.0" +} diff --git a/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/UserSelectorDemo.cs b/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/UserSelectorDemo.cs new file mode 100644 index 0000000..4f90724 --- /dev/null +++ b/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/UserSelectorDemo.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using WorkFlowCore.UserSelectors; + +namespace UserSelector_PluginDemo +{ + [UserSelector("用户选择器插件demo","这是一个插件demo")] + public class UserSelectorDemo : IUserSelector + { + private static List selections = null; + private static Dictionary> selectionUsers = null; + + static UserSelectorDemo() + { + selections = new List(); + selectionUsers = new Dictionary>(); + for (int i = 0; i < 4; i++) + { + selections.Add(new Selection { Id = i.ToString(), Name = $"类型选项{i}" }); + var users = new List(); + for (int j = 0; j < 5; j++) + { + users.Add(new User($"UserSelectorDemo-{i}-{j}", $"选项{i}-用{j}")); + } + selectionUsers.Add(i.ToString(), users); + } + } + + public List GetSelections() + { + return selections; + } + + public List GetUsers(SelectorInput input) + { + try + { + return selectionUsers[input.SelectionId]; + } + catch (Exception) + { + return new List(); + } + } + } +} diff --git a/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/UserSelector_PluginDemo.csproj b/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/UserSelector_PluginDemo.csproj new file mode 100644 index 0000000..f295c94 --- /dev/null +++ b/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/UserSelector_PluginDemo.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.1 + + + + + + + + + Always + + + + diff --git a/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/manifest.json b/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/manifest.json new file mode 100644 index 0000000..6c978b7 --- /dev/null +++ b/WorkFlowCore/Plugins/UserSelectors/UserSelector_PluginDemo/manifest.json @@ -0,0 +1,4 @@ +{ + "Entry": "UserSelector_PluginDemo.dll", + "Version": "1.0" +} diff --git "a/WorkFlowCore/ReadmeImges/\347\224\250\346\210\267\351\200\211\346\213\251\345\231\250\346\217\222\344\273\266\347\233\256\345\275\225.png" "b/WorkFlowCore/ReadmeImges/\347\224\250\346\210\267\351\200\211\346\213\251\345\231\250\346\217\222\344\273\266\347\233\256\345\275\225.png" new file mode 100644 index 0000000000000000000000000000000000000000..a19623551f2c8b66a8043136e5e79fb513467648 GIT binary patch literal 17643 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 HE;s%U4Q;V? literal 0 HcmV?d00001 diff --git a/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/PluginAssemblyLoadContext.cs b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/PluginAssemblyLoadContext.cs index 58bfb56..c59ad9e 100644 --- a/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/PluginAssemblyLoadContext.cs +++ b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/PluginAssemblyLoadContext.cs @@ -20,10 +20,11 @@ namespace WorkFlowCore.Common.SimplePluginLoaders string assemblyPath = _resolver.ResolveAssemblyToPath(name); if (assemblyPath != null) { - return LoadFromAssemblyPath(assemblyPath); + //***这里需要使用 Default 来载入程序集,因为有共享 程序集。否则会出现类型转换问题*** + //https://learn.microsoft.com/zh-cn/dotnet/core/dependency-loading/understanding-assemblyloadcontext#shared-dependencies + return Default.LoadFromAssemblyPath(assemblyPath); } - - return null; + return base.Load(name); } } } diff --git a/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/SimplePluginLoader.cs b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/SimplePluginLoader.cs index aaeff07..f0d4b91 100644 --- a/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/SimplePluginLoader.cs +++ b/WorkFlowCore/WorkFlowCore.Common/SimplePluginLoaders/SimplePluginLoader.cs @@ -42,7 +42,7 @@ namespace WorkFlowCore.Common.SimplePluginLoaders } catch (Exception ex) { - + Console.Error.WriteLine($"{ex.Message},{ex}"); // } @@ -54,6 +54,7 @@ namespace WorkFlowCore.Common.SimplePluginLoaders public static IServiceCollection LoadPlugins(this IServiceCollection services, string pluginDir, Action> loaded) { + if (!Directory.Exists(pluginDir)) Directory.CreateDirectory(pluginDir); return LoadPlugins(services, pluginDir, loaded); } } diff --git a/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs b/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs index bb91d42..a8f72cd 100644 --- a/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs +++ b/WorkFlowCore/WorkFlowCore.Framework/WorkFlowCoreFrameworkService.cs @@ -3,8 +3,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; using System.Text; using WorkFlowCore.Authorization; +using WorkFlowCore.Common.SimplePluginLoaders; using WorkFlowCore.Conditions; using WorkFlowCore.Framework.Authorization; using WorkFlowCore.Framework.Conditions; @@ -66,13 +70,27 @@ namespace WorkFlowCore.Framework services.Replace(new ServiceDescriptor(typeof(IWorkflowSession),typeof(DefaultSession), ServiceLifetime.Scoped)); //注册条件和选择器 - UserSelectorManager.RegisterSelector(assembly); - ConditionManager.RegisterCondition(assembly); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + #region 注册条件和选择器 + UserSelectorManager.RegisterSelector(services, assembly); + services.LoadPlugins(Path.Combine(AppContext.BaseDirectory, "Plugins", "UserSelectors"), plugins => + { + UserSelectorManager.RegisterSelector(services, plugins.Select(p => p.Assembly).ToArray()); + + }); + ConditionManager.Registercondition(services, assembly); + + services.LoadPlugins(Path.Combine(AppContext.BaseDirectory, "Plugins", "Conditions"), plugins => + { + ConditionManager.Registercondition(services, plugins.Select(p => p.Assembly).ToArray()); + }); + #endregion + + + //services.AddScoped(); + //services.AddScoped(); + //services.AddScoped(); + //services.AddScoped(); + //services.AddScoped(); } } } diff --git a/WorkFlowCore/WorkFlowCore.sln b/WorkFlowCore/WorkFlowCore.sln index a8297ed..59c4e83 100644 --- a/WorkFlowCore/WorkFlowCore.sln +++ b/WorkFlowCore/WorkFlowCore.sln @@ -13,6 +13,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkFlowCore.Host", "WorkFl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkFlowCore.Common", "WorkFlowCore.Common\WorkFlowCore.Common.csproj", "{8F14A61F-C202-4ED1-A167-B426EE428DDA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{3D75B3EF-C8FF-42E0-A3B0-A0C63FDC6110}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UserSelectors", "UserSelectors", "{F371EB9A-F50A-4FF3-BBC4-1C9C50AF4D78}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Conditions", "Conditions", "{A81DC2DC-4670-40CE-AFA8-708A9E05E93D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserSelector_PluginDemo", "Plugins\UserSelectors\UserSelector_PluginDemo\UserSelector_PluginDemo.csproj", "{D018E18A-B871-4C25-8A79-44EFF4A76228}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Condition_PluginDemo", "Plugins\Conditions\Condition_PluginDemo\Condition_PluginDemo.csproj", "{83CEABEA-4230-4C5E-AA80-D3EC302B7D19}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,10 +49,24 @@ Global {8F14A61F-C202-4ED1-A167-B426EE428DDA}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F14A61F-C202-4ED1-A167-B426EE428DDA}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F14A61F-C202-4ED1-A167-B426EE428DDA}.Release|Any CPU.Build.0 = Release|Any CPU + {D018E18A-B871-4C25-8A79-44EFF4A76228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D018E18A-B871-4C25-8A79-44EFF4A76228}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D018E18A-B871-4C25-8A79-44EFF4A76228}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D018E18A-B871-4C25-8A79-44EFF4A76228}.Release|Any CPU.Build.0 = Release|Any CPU + {83CEABEA-4230-4C5E-AA80-D3EC302B7D19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83CEABEA-4230-4C5E-AA80-D3EC302B7D19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83CEABEA-4230-4C5E-AA80-D3EC302B7D19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83CEABEA-4230-4C5E-AA80-D3EC302B7D19}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F371EB9A-F50A-4FF3-BBC4-1C9C50AF4D78} = {3D75B3EF-C8FF-42E0-A3B0-A0C63FDC6110} + {A81DC2DC-4670-40CE-AFA8-708A9E05E93D} = {3D75B3EF-C8FF-42E0-A3B0-A0C63FDC6110} + {D018E18A-B871-4C25-8A79-44EFF4A76228} = {F371EB9A-F50A-4FF3-BBC4-1C9C50AF4D78} + {83CEABEA-4230-4C5E-AA80-D3EC302B7D19} = {A81DC2DC-4670-40CE-AFA8-708A9E05E93D} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2B47C6B7-D14F-4E7A-AC89-493A7F10350B} EndGlobalSection diff --git a/WorkFlowCore/WorkFlowCore/Conditions/ConditionManager.cs b/WorkFlowCore/WorkFlowCore/Conditions/ConditionManager.cs index df1c17c..7aa1e87 100644 --- a/WorkFlowCore/WorkFlowCore/Conditions/ConditionManager.cs +++ b/WorkFlowCore/WorkFlowCore/Conditions/ConditionManager.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -101,5 +102,66 @@ namespace WorkFlowCore.Conditions } } } + + /// + /// 注册条件 + /// + /// + /// + /// + /// + public static void Registercondition(string conditionId, string conditionName, Type conditionType, string description) + { + lock (objLock) + { + if (AllConditions.Where(s => s.Id == conditionId).Any()) + return; + //throw new Exception($"相同的转换器id[{conditionId}]已存在"); + } + + AllConditions.Add(new Condition(conditionId, conditionName, conditionType, description)); + } + + /// + /// 从 程序集注册 + /// + /// + public static void Registercondition(params Assembly[] assemblies) + { + foreach (var assembly in assemblies) + { + var types = assembly.GetTypes().Where(t => t.GetCustomAttribute() != null); + + foreach (var type in types) + { + var attr = type.GetCustomAttribute(); + var conditionId = type.FullName; + var conditionName = attr.Name ?? type.FullName; + Registercondition(conditionId, conditionName, type, attr.Description); + } + } + } + + /// + /// 从 程序集注册 + /// + /// + /// + public static void Registercondition(IServiceCollection services, params Assembly[] assemblies) + { + foreach (var assembly in assemblies) + { + var types = assembly.GetTypes().Where(t => t.GetCustomAttribute() != null); + + foreach (var type in types) + { + var attr = type.GetCustomAttribute(); + var conditionId = type.FullName; + var conditionName = attr.Name ?? type.FullName; + Registercondition(conditionId, conditionName, type, attr.Description); + services.AddScoped(type); + } + } + } } } diff --git a/WorkFlowCore/WorkFlowCore/UserSelectors/UserSelectorManager.cs b/WorkFlowCore/WorkFlowCore/UserSelectors/UserSelectorManager.cs index a21bdb1..22e3e0a 100644 --- a/WorkFlowCore/WorkFlowCore/UserSelectors/UserSelectorManager.cs +++ b/WorkFlowCore/WorkFlowCore/UserSelectors/UserSelectorManager.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -74,6 +75,47 @@ namespace WorkFlowCore.UserSelectors } } + /// + /// 从 程序集注册 + /// + /// + /// + public static void RegisterSelector(IServiceCollection services, params Assembly[] assemblies) + { + foreach (var assembly in assemblies) + { + var types = assembly.GetTypes().Where(t => t.GetCustomAttribute() != null); + + foreach (var type in types) + { + var attr = type.GetCustomAttribute(); + var selectorId = type.FullName; + var selectorName = attr.Name ?? type.FullName; + RegisterSelector(selectorId, selectorName, type, attr.Description); + services.AddScoped(type); + } + } + } + + /// + /// 从 程序集注册 + /// + /// + /// + public static void RegisterSelector(IServiceCollection services, params Type[] types) + { + var _types = types.Where(t => t.GetCustomAttribute() != null); + + foreach (var type in _types) + { + var attr = type.GetCustomAttribute(); + var selectorId = type.FullName; + var selectorName = attr.Name ?? type.FullName; + RegisterSelector(selectorId, selectorName, type, attr.Description); + services.AddScoped(type); + } + } + /// /// 获取选择器 -- Gitee