瀏覽代碼

workflow_countersign

xf 2 年之前
父節點
當前提交
109c0b2c29

+ 4 - 1
src/Hotline.Application/FlowEngine/WorkflowApplication.cs

@@ -87,7 +87,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     /// <returns></returns>
     public async Task NextAsync(NextWorkflowDto dto, CancellationToken cancellationToken)
     {
-        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, true, true, cancellationToken: cancellationToken);
+        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, true, true, withCountersigns: true, cancellationToken: cancellationToken);
         var nextStepBoxDefine = _workflowDomainService.GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
         //下一节点为结束节点时,无办理人等参数,只有办理意见即可
         var isOutOfCallCenter = nextStepBoxDefine.StepType is not EStepType.End
@@ -95,6 +95,9 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
         var isStartCountersign = nextStepBoxDefine.StepType is not EStepType.End
                                  && nextStepBoxDefine.IsStartCountersign(dto.NextHandlers.Count);
 
+        if (isStartCountersign && nextStepBoxDefine.StepType is EStepType.CountersignEnd)
+            throw UserFriendlyException.SameMessage("汇总节点不允许发起会签");
+
         var flowAssignMode = await GetFlowAssignModeAsync(nextStepBoxDefine,
             dto.NextHandlers.Select(d => d.Id).ToList(), cancellationToken);
 

+ 21 - 4
src/Hotline.Application/Mappers/MapperConfigs.cs

@@ -45,12 +45,22 @@ namespace Hotline.Application.Mappers
                 .Ignore(d => d.Id);
 
             config.ForType<WorkflowStep, WorkflowStep>()
-                .Ignore(d => d.Id);
+                .Ignore(d => d.Id)
+                .Ignore(d => d.PreviousId)
+                .Ignore(d => d.IsMain)
+                .Ignore(d => d.Status)
+                .Ignore(d => d.ParentId)
+                .Ignore(d => d.HandlerId)
+                .Ignore(d => d.Steps)
+                .Ignore(d => d.StartCountersignId)
+                .Ignore(d => d.CountersignId)
+                .Ignore(d => d.IsStartedCountersignComplete)
+                ;
 
             config.ForType<WorkflowStep, WorkflowTrace>()
                 .Ignore(d => d.Id)
-                .Ignore(d=>d.ParentId)
-                .Ignore(d=>d.Status)
+                .Ignore(d => d.ParentId)
+                .Ignore(d => d.Status)
                 .Map(d => d.StepId, s => s.Id);
 
             config.ForType<WorkflowSupplement, WorkflowSupplementDto>()
@@ -62,7 +72,14 @@ namespace Hotline.Application.Mappers
                 ;
 
             config.ForType<BasicWorkflowDto, WorkflowStep>()
-                .IgnoreNullValues(true);
+                .Map(d => d.NextHandlers, s => s.NextHandlers)
+                .Map(d => d.NextMainHandler, s => s.NextMainHandler)
+                .Map(d => d.NextStepCode, s => s.NextStepCode)
+                .Map(d => d.AcceptSms, s => s.NextStepCode)
+                .Map(d => d.Opinion, s => s.NextStepCode)
+                .Map(d => d.Additions, s => s.NextStepCode)
+                .IgnoreNonMapped(true)
+                ;
 
             #endregion
 

+ 22 - 0
src/Hotline/FlowEngine/Workflows/EStepCountersignStatus.cs

@@ -0,0 +1,22 @@
+namespace Hotline.FlowEngine.Workflows;
+
+/// <summary>
+/// 节点会签状态
+/// </summary>
+public enum EStepCountersignStatus
+{
+    /// <summary>
+    /// 无会签(未处于会签流程中,包括外层也无会签嵌套)
+    /// </summary>
+    None = 0,
+
+    /// <summary>
+    /// 会签中
+    /// </summary>
+    InCountersign = 1,
+
+    /// <summary>
+    /// 本身不处于会签流程中,但外层有会签流程嵌套
+    /// </summary>
+    OuterCountersign = 2,
+}

+ 13 - 0
src/Hotline/FlowEngine/Workflows/IWorkflowCountersignRepository.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.FlowEngine.Workflows
+{
+    public interface IWorkflowCountersignRepository : IRepository<WorkflowCountersign>
+    {
+    }
+}

+ 2 - 1
src/Hotline/FlowEngine/Workflows/IWorkflowDomainService.cs

@@ -17,7 +17,8 @@ namespace Hotline.FlowEngine.Workflows
         /// <summary>
         /// 查询工作流
         /// </summary>
-        Task<Workflow> GetWorkflowAsync(string workflowId, bool withDefine = false, bool withSteps = false, bool withTraces = false, bool withSupplements = false, bool withAssigns = false, CancellationToken cancellationToken = default);
+        Task<Workflow> GetWorkflowAsync(string workflowId, bool withDefine = false, bool withSteps = false, bool withTraces = false,
+            bool withSupplements = false, bool withAssigns = false, bool withCountersigns = false, CancellationToken cancellationToken = default);
 
         /// <summary>
         /// 受理,接办

+ 21 - 15
src/Hotline/FlowEngine/Workflows/Workflow.cs

@@ -38,21 +38,21 @@ public class Workflow : CreationEntity
     public EWorkflowStatus Status { get; set; }
 
     /// <summary>
-    /// 当前节点名称(会签状态此字段保存最外层会签办理节点名称)
+    /// 当前办理节点名称(会签状态此字段保存最外层会签办理节点名称)
     /// </summary>
     [SugarColumn(IsNullable = true)]
     public string? CurrentStepName { get; set; }
 
     /// <summary>
-    /// 到达当前节点时间(stepBox创建时间)
+    /// 到达当前办理节点时间(stepBox创建时间)
     /// </summary>
     public DateTime? CurrentStepTime { get; set; }
 
     /// <summary>
-    /// 当前会签办理节点code,嵌套会签为最外层会签办理节点code(不处于会签状态则无值)
+    /// 会签办理节点code,嵌套会签为最外层会签办理节点code(不处于会签状态则无值)
     /// </summary>
     [SugarColumn(IsNullable = true)]
-    public string? CurrentCountersignStepCode { get; set; }
+    public string? TopCountersignStepCode { get; set; }
 
     /// <summary>
     /// 中心直办件
@@ -107,6 +107,12 @@ public class Workflow : CreationEntity
     [Navigate(NavigateType.OneToMany, nameof(WorkflowAssign.WorkflowId))]
     public List<WorkflowAssign> Assigns { get; set; }
 
+    /// <summary>
+    /// 会签
+    /// </summary>
+    [Navigate(NavigateType.OneToMany, nameof(WorkflowCountersign.WorkflowId))]
+    public List<WorkflowCountersign> Countersigns { get; set; }
+
     /// <summary>
     /// 主节点,依据流转进度动态生成或删除
     /// </summary>
@@ -116,7 +122,6 @@ public class Workflow : CreationEntity
     [SugarColumn(IsIgnore = true)]
     public List<WorkflowTrace> Traces { get; set; } = new();
 
-
     #region Mehod
 
     /// <summary>
@@ -142,7 +147,7 @@ public class Workflow : CreationEntity
         CurrentStepName = currentStepName;
         CurrentStepTime = currentStepTime;
         if (!string.IsNullOrEmpty(currentCountersignCode))
-            CurrentCountersignStepCode = currentCountersignCode;
+            TopCountersignStepCode = currentCountersignCode;
     }
 
     /// <summary>
@@ -150,14 +155,15 @@ public class Workflow : CreationEntity
     /// </summary>
     public bool CheckIfCountersignOver()
     {
-        var countersignStepBox = StepBoxes.First(d => d.Code == CurrentCountersignStepCode);
-        var isCountersignOver = countersignStepBox.Steps.All(d =>
-            d.Status is EWorkflowStepStatus.Completed &&
-            (!d.HasStartCountersign || d.IsCountersignComplete.GetValueOrDefault()));
-        return isCountersignOver;
+        //var countersignStepBox = StepBoxes.First(d => d.Code == TopCountersignStepCode);
+        //var isCountersignOver = countersignStepBox.Steps.All(d =>
+        //    d.Status is EWorkflowStepStatus.Completed &&
+        //    (!d.HasStartCountersign || d.IsCountersignComplete.GetValueOrDefault()));
+        //return isCountersignOver;
+        return Countersigns.All(d => d.IsCompleted());
     }
 
-    public void EndCountersign() => CurrentCountersignStepCode = null;
+    public void EndCountersign() => TopCountersignStepCode = null;
 
     /// <summary>
     /// 更新workflow中当前停留节点,时间和会签开始节点code
@@ -182,12 +188,12 @@ public class Workflow : CreationEntity
     /// 流程是否处于会签中
     /// </summary>
     /// <returns></returns>
-    public bool IsInCountersign() => !string.IsNullOrEmpty(CurrentCountersignStepCode);
+    public bool IsInCountersign() => !string.IsNullOrEmpty(TopCountersignStepCode);
 
     /// <summary>
     /// 结束流程会签状态
     /// </summary>
-    public void CloseCountersignStatus() => CurrentCountersignStepCode = null;
+    public void CloseCountersignStatus() => TopCountersignStepCode = null;
 
     /// <summary>
     /// 重置最终办理意见
@@ -218,7 +224,7 @@ public class Workflow : CreationEntity
     /// 当前用户是否可以办理该流程
     /// </summary>
     /// <returns></returns>
-    public bool CanHandle(string userId, string orgCode) => 
+    public bool CanHandle(string userId, string orgCode) =>
         HandlerUsers.Contains(userId) || HandlerOrgs.Contains(orgCode);
 
     #endregion

+ 24 - 4
src/Hotline/FlowEngine/Workflows/WorkflowCountersign.cs

@@ -13,14 +13,22 @@ namespace Hotline.FlowEngine.Workflows
         public string WorkflowId { get; set; }
 
         /// <summary>
-        /// 发起会签节点code
+        /// 发起会签节点id(从开始节点直接发起的会签,该id和code为start节点的id与code)
         /// </summary>
+        public string StartStepId { get; set; }
+
+        //冗余
         public string StartStepCode { get; set; }
 
         /// <summary>
-        /// 会签汇总节点code
+        /// 会签汇总节点id
         /// </summary>
-        public string EndStepCode { get; set; }
+        [SugarColumn(IsNullable = true)]
+        public string? EndStepId { get; set; }
+
+        //冗余
+        [SugarColumn(IsNullable = true)]
+        public string? EndStepCode { get; set; }
 
         /// <summary>
         /// 会签结束时间
@@ -39,9 +47,21 @@ namespace Hotline.FlowEngine.Workflows
         public int Members { get; set; }
 
         /// <summary>
-        /// 会签是否完成(如有嵌套会签,下级所有会签都完成才可判定当前会签为完成)
+        /// 会签是否完成(如有嵌套会签,表示下级所有会签都完成了)
         /// </summary>
         public bool IsCompleted() => CompleteTime.HasValue;
+
+        /// <summary>
+        /// 结束会签(需提前确认所有子集都已结束)
+        /// </summary>
+        /// <param name="endStepId"></param>
+        /// <param name="endStepCode"></param>
+        public void Complete(string endStepId, string endStepCode)
+        {
+            EndStepId = endStepId;
+            EndStepCode = endStepCode;
+            CompleteTime = DateTime.Now;
+        }
     }
 
 }

+ 237 - 131
src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs

@@ -23,6 +23,7 @@ namespace Hotline.FlowEngine.Workflows
         private readonly IWorkflowTraceRepository _workflowTraceRepository;
         private readonly IWorkflowSupplementRepository _workflowSupplementRepository;
         private readonly IWorkflowAssignRepository _workflowAssignRepository;
+        private readonly IWorkflowCountersignRepository _workflowCountersignRepository;
         private readonly ISessionContext _sessionContext;
         private readonly IMapper _mapper;
         private readonly IMediator _mediator;
@@ -34,6 +35,7 @@ namespace Hotline.FlowEngine.Workflows
             IWorkflowTraceRepository workflowTraceRepository,
             IWorkflowSupplementRepository workflowSupplementRepository,
             IWorkflowAssignRepository workflowAssignRepository,
+            IWorkflowCountersignRepository workflowCountersignRepository,
             ISessionContext sessionContext,
             IMapper mapper,
             IMediator mediator,
@@ -44,6 +46,7 @@ namespace Hotline.FlowEngine.Workflows
             _workflowTraceRepository = workflowTraceRepository;
             _workflowSupplementRepository = workflowSupplementRepository;
             _workflowAssignRepository = workflowAssignRepository;
+            _workflowCountersignRepository = workflowCountersignRepository;
 
             _sessionContext = sessionContext;
             _mapper = mapper;
@@ -95,8 +98,19 @@ namespace Hotline.FlowEngine.Workflows
             if (nextStepBoxDefine.HandlerType is not EHandlerType.Role && !dto.NextHandlers.Any())
                 throw UserFriendlyException.SameMessage("未指派办理人");
 
-            //第二节点的previousId is string.Empty
-            var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, cancellationToken: cancellationToken);
+            var (startStepBox, startStep) = await CreateStartStepAsync(workflow, dto, cancellationToken);
+
+            if (isStartCountersign)
+            {
+                //创建会签数据
+                var countersign = await CreateCountersignAsync(workflow.Id, startStep.Id, startStep.Code, dto.NextHandlers.Count, startStep.CountersignId, cancellationToken);
+                startStep.StartCountersignId = countersign.Id;
+                await _workflowStepRepository.UpdateAsync(startStep, cancellationToken);
+            }
+
+            //第二节点(创建即为 已指派/待接办 状态)
+            var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, EWorkflowStepStatus.Assigned,
+                startStepBox, startStep, cancellationToken);
 
             //更新当前节点名称、时间、会签节点code 等字段
             workflow.SetWorkflowCurrentStepInfo(isStartCountersign, nextStepBox);
@@ -113,7 +127,7 @@ namespace Hotline.FlowEngine.Workflows
         public async Task<Workflow> GetWorkflowAsync(string workflowId,
             bool withDefine = false, bool withSteps = false,
             bool withTraces = false, bool withSupplements = false,
-            bool withAssigns = false,
+            bool withAssigns = false, bool withCountersigns = false,
             CancellationToken cancellationToken = default)
         {
             var query = _workflowRepository.Queryable().Where(d => d.Id == workflowId);
@@ -123,6 +137,8 @@ namespace Hotline.FlowEngine.Workflows
                 query = query.Includes(d => d.Supplements, d => d.Creator);
             if (withAssigns)
                 query = query.Includes(d => d.Assigns);
+            if (withCountersigns)
+                query = query.Includes(d => d.Countersigns);
 
             var workflow = await query.FirstAsync();
             if (workflow is null)
@@ -215,6 +231,8 @@ namespace Hotline.FlowEngine.Workflows
             #region 办理当前节点
 
             var (currentStepBox, currentStep) = GetUnCompleteStep(workflow.StepBoxes, _sessionContext.RequiredOrgCode, _sessionContext.RequiredUserId);
+            if (currentStep.Status is EWorkflowStepStatus.Completed or EWorkflowStepStatus.Created)
+                throw UserFriendlyException.SameMessage("当前节点状态无法办理");
             if (currentStep.Status is EWorkflowStepStatus.Assigned)
                 await AcceptAsync(workflow,
                     _sessionContext.RequiredUserId,
@@ -228,8 +246,15 @@ namespace Hotline.FlowEngine.Workflows
             //检查是否支持会签办理
             if (isStartCountersign && nextStepBoxDefine.CountersignMode == ECountersignMode.UnSupport)
                 throw UserFriendlyException.SameMessage($"下一节点不支持会签办理, code: {currentStep.Code}");
+            WorkflowCountersign? countersign = null;
             if (isStartCountersign)
-                currentStep.StartCountersign();
+            {
+                //创建会签数据
+                countersign = await CreateCountersignAsync(workflow.Id, currentStep.Id, currentStep.Code,
+                    dto.NextHandlers.Count, currentStep.CountersignId, cancellationToken);
+                currentStep.StartCountersign(countersign.Id);
+            }
+
 
             _mapper.Map(dto, currentStep);
 
@@ -246,11 +271,26 @@ namespace Hotline.FlowEngine.Workflows
             //结束当前会签流程
             if (currentStep.StepType is EStepType.CountersignEnd && currentStep.IsInCountersign)
             {
-                var countersignStartStep = FindCountersignStartStep(workflow, currentStep.CountersignStartCode, currentStep.PrevCountersignId);
-                if (countersignStartStep.HasStartCountersign)
+                var countersignStartStepBox =
+                    workflow.StepBoxes.FirstOrDefault(d => d.Code == currentStep.CountersignStartCode);
+                if (countersignStartStepBox is null)
+                    throw new UserFriendlyException(
+                    $"未查询到会签开始stepBox, workflowId: {workflow.Id}, stepCode: {currentStep.CountersignStartCode}", "未查询到会签开始节点");
+                var countersignStartStep =
+                    countersignStartStepBox.Steps.FirstOrDefault(d => d.HasStartCountersign && d.StartCountersignId == currentStep.CountersignId);
+                if (countersignStartStep != null)//如果==null,说明未发起会签是继承的外层会签
                 {
+                    //结束step会签信息
                     countersignStartStep.CountersignComplete();
                     updateSteps.Add(countersignStartStep);
+
+                    //结束会签
+                    var currentCountersign = workflow.Countersigns.FirstOrDefault(d => d.Id == currentStep.CountersignId);
+                    if (currentCountersign is null)
+                        throw new UserFriendlyException(
+                            $"未查询到对应会签信息,workflowId:{workflow.Id}, countersignId:{currentStep.CountersignId}", "无效会签编号");
+                    currentCountersign.Complete(currentStep.Id, currentStep.Code);
+                    await _workflowCountersignRepository.UpdateAsync(currentCountersign, cancellationToken);
                 }
             }
 
@@ -262,7 +302,7 @@ namespace Hotline.FlowEngine.Workflows
 
             //检查会签是否结束,并更新当前会签节点字段
             var isCountersignOver = false;
-            if (workflow.IsInCountersign())
+            if (currentStep.StepType is EStepType.CountersignEnd && workflow.IsInCountersign())
             {
                 isCountersignOver = workflow.CheckIfCountersignOver();
                 if (isCountersignOver)
@@ -301,49 +341,37 @@ namespace Hotline.FlowEngine.Workflows
                 await _mediator.Publish(new OrderRecallFinalManageNotify(workflow), cancellationToken);
             }
 
-            //创建下一节点
-            var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, currentStepBox, currentStep, cancellationToken);
+            //创建下一节点(会签汇总节点不重复创建)
+            var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, EWorkflowStepStatus.Created,
+                currentStepBox, currentStep, cancellationToken);
 
             //下一节点为汇总节点时,检查下一节点是否可办理
+            var nextStepCanHandle = true;
             if (nextStepBox.StepType is EStepType.CountersignEnd)
             {
                 if (currentStep.IsInCountersign)
                 {
-                    var stepCode = currentStep.StepType is EStepType.CountersignEnd
-                        ? currentStep.CountersignStartCode
-                        : currentStep.Code;
-
-                    var countersignId = string.IsNullOrEmpty(currentStep.TopCountersignId)
-                        ? currentStep.PrevCountersignId
-                        : currentStep.TopCountersignId;
-
-                    var stepBox = workflow.StepBoxes.First(d => d.Code == stepCode);
-                    var countersignSteps =
-                        stepBox.Steps.Where(d => d.PrevCountersignId == countersignId);
-
-                    //check all complete or cs complete
-                    var canHandle = true;
-                    foreach (var countersignStep in countersignSteps)
-                    {
-                        if (countersignStep.Status != EWorkflowStepStatus.Completed) break;
-                        if (countersignStep.HasStartCountersign && !countersignStep.IsCountersignComplete.GetValueOrDefault()) break;
-                    }
-
-                    if (canHandle)
-                    {
-                        await UpdateNextCountersignEndAssignedAsync(nextStepBox, currentStep, cancellationToken);
-                        _mediator.Publish(new CountersignEndAssigned(workflow), cancellationToken);
-                    }
-
-                }
-                else
-                {
-                    await UpdateNextCountersignEndAssignedAsync(nextStepBox, currentStep, cancellationToken);
-                    _mediator.Publish(new CountersignEndAssigned(workflow), cancellationToken);
+                    //同一会签Id,非汇总节点
+                    var steps = await _workflowStepRepository.QueryAsync(d =>
+                        d.WorkflowId == workflow.Id
+                        && d.CountersignId == currentStep.CountersignId
+                        && d.StepType != EStepType.End);
+                    //(当前办理节点所处同一会签内的所有step全都办理完成并且如果开启了会签的step,必须会签结束)
+                    var unComplete = steps.Any(d =>
+                        d.Status != EWorkflowStepStatus.Completed ||
+                        (d.HasStartCountersign && !d.IsStartedCountersignComplete.Value));
+                    nextStepCanHandle = !unComplete;
                 }
             }
+            if (nextStepCanHandle)
+            {
+                //将下一节点处理为已指派/可接办
+                await SetNextCountersignEndAssignedAsync(nextStepBox, currentStep, cancellationToken);
+                _mediator.Publish(new CountersignEndAssigned(workflow), cancellationToken);
+            }
 
-            //更新当前节点名称、时间、会签节点code 等字段
+
+            //更新workflow当前节点名称、时间、会签节点code 等字段
             workflow.SetWorkflowCurrentStepInfo(isStartCountersign, nextStepBox);
 
             workflow.UpdateHandlers(_sessionContext.RequiredUserId, _sessionContext.RequiredOrgCode,
@@ -411,11 +439,18 @@ namespace Hotline.FlowEngine.Workflows
             //update uncompleted traces
             await JumpTraceAsync(workflow.Id, dto, cancellationToken);
 
+            var (currentStepBox, currentStep) = GetUnCompleteStep(workflow.StepBoxes, _sessionContext.RequiredOrgCode, _sessionContext.RequiredUserId);
+            if (currentStepBox.StepType is EStepType.Start)
+                throw UserFriendlyException.SameMessage("当前流程已退回到开始节点");
+            if (currentStepBox.StepType is EStepType.End)
+                throw UserFriendlyException.SameMessage("当前流程已流转到结束节点");
+
             var targetStepBox = workflow.StepBoxes.FirstOrDefault(d => d.Code == dto.TargetStepCode);
             if (targetStepBox == null)
             {
+                //向后跳转
                 var nextStepBoxDefine = GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
-                var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, cancellationToken: cancellationToken);
+                var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, EWorkflowStepStatus.Assigned, currentStepBox, currentStep, cancellationToken);
 
                 await ResetWorkflowCurrentStepInfo(workflow, dto, nextStepBox, cancellationToken);
 
@@ -453,15 +488,6 @@ namespace Hotline.FlowEngine.Workflows
             //todo publish
         }
 
-        private async Task ResetWorkflowCurrentStepInfo(Workflow workflow, RecallDto dto, WorkflowStep stepBox, CancellationToken cancellationToken)
-        {
-            //更新当前节点名称、时间、会签节点code
-            workflow.CloseCountersignStatus();
-            var isCountersign = dto.NextHandlers.Count > 1;
-            workflow.SetWorkflowCurrentStepInfo(isCountersign, stepBox);
-            await _workflowRepository.UpdateAsync(workflow, cancellationToken);
-        }
-
         /// <summary>
         /// 补充
         /// </summary>
@@ -516,6 +542,21 @@ namespace Hotline.FlowEngine.Workflows
 
         #region private
 
+        private async Task<WorkflowCountersign> CreateCountersignAsync(string workflowId, string startStepId, string startStepCode, int count, string? parentId = null, CancellationToken cancellationToken = default)
+        {
+            var countersign = new WorkflowCountersign
+            {
+                WorkflowId = workflowId,
+                StartStepId = startStepId,
+                StartStepCode = startStepCode,
+                Members = count,
+                ParentId = parentId,
+            };
+            await _workflowCountersignRepository.AddAsync(countersign, cancellationToken);
+            return countersign;
+        }
+
+
         /// <summary>
         /// 更新下级汇总节点可办理状态
         /// </summary>
@@ -523,13 +564,22 @@ namespace Hotline.FlowEngine.Workflows
         /// <param name="currentStep"></param>
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
-        private async Task UpdateNextCountersignEndAssignedAsync(WorkflowStep nextStepBox, WorkflowStep currentStep, CancellationToken cancellationToken)
+        private async Task SetNextCountersignEndAssignedAsync(WorkflowStep nextStepBox, WorkflowStep currentStep, CancellationToken cancellationToken)
         {
-            var countersignId = string.IsNullOrEmpty(currentStep.TopCountersignId)
-                ? currentStep.PrevCountersignId
-                : currentStep.TopCountersignId;
+            WorkflowStep? nextStep;
+            if (currentStep.StepCountersignStatus is EStepCountersignStatus.InCountersign)
+            {
+                nextStep = nextStepBox.Steps.FirstOrDefault(d => d.CountersignId == currentStep.CountersignId);
+            }
+            else
+            {
+                nextStep = nextStepBox.Steps.FirstOrDefault(d => d.ParentId == currentStep.Id);
+            }
 
-            var nextStep = nextStepBox.Steps.First(d => d.PrevCountersignId == countersignId);
+            if (nextStep == null)
+                throw new UserFriendlyException(
+                    $"未查询到下一节点, workflowId:{currentStep.WorkflowId}, currentStepId: {currentStep.Id}");
+            
             nextStep.SetAssigned();
 
             await _workflowStepRepository.UpdateAsync(nextStep, cancellationToken);
@@ -611,13 +661,13 @@ namespace Hotline.FlowEngine.Workflows
             trace.ExpiredTime = workflow.ExpiredTime;
             trace.TimeLimit = workflow.TimeLimit;
 
-            if (!string.IsNullOrEmpty(currentStep.PreviousId) && currentStepBox.Steps.Count > 1)
+            //处于会签中的节点,其对应的trace.parentId赋值上级trace.Id
+            if(currentStep.StepCountersignStatus is EStepCountersignStatus.InCountersign)
             {
-                //有会签
                 var parentTrace = await GetWorkflowTraceAsync(workflow.Id, currentStep.PreviousId, cancellationToken);
                 trace.ParentId = parentTrace.Id;
             }
-
+            
             await _workflowTraceRepository.AddAsync(trace, cancellationToken);
         }
 
@@ -649,20 +699,32 @@ namespace Hotline.FlowEngine.Workflows
 
             //recreate targetStep
             var nextStepBoxDefine = GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
-            await CreateStepAsync(workflow, nextStepBoxDefine, dto, targetStepBox, targetStepBox.Steps.First(), cancellationToken);
+            await CreateStepAsync(workflow, nextStepBoxDefine, dto, EWorkflowStepStatus.Assigned, targetStepBox, targetStepBox.Steps.First(), cancellationToken);
 
             //flow manage
             if (workflow.IsInCountersign())
             {
                 var currentCountersignStepBox =
-                    workflow.StepBoxes.First(d => d.Code == workflow.CurrentCountersignStepCode);
+                    workflow.StepBoxes.First(d => d.Code == workflow.TopCountersignStepCode);
                 //目标节点在初始会签节点之前或正好
-                if (targetStepBox.Code == workflow.CurrentCountersignStepCode || targetStepBox.CreationTime < currentCountersignStepBox.CreationTime)
+                if (targetStepBox.Code == workflow.TopCountersignStepCode || targetStepBox.CreationTime < currentCountersignStepBox.CreationTime)
                     await ResetWorkflowCurrentStepInfo(workflow, dto, targetStepBox, cancellationToken);
 
             }
         }
 
+        /// <summary>
+        /// 重置currentStep信息
+        /// </summary>
+        private async Task ResetWorkflowCurrentStepInfo(Workflow workflow, RecallDto dto, WorkflowStep stepBox, CancellationToken cancellationToken)
+        {
+            //更新当前节点名称、时间、会签节点code
+            workflow.CloseCountersignStatus();
+            var isCountersign = dto.NextHandlers.Count > 1;
+            workflow.SetWorkflowCurrentStepInfo(isCountersign, stepBox);
+            await _workflowRepository.UpdateAsync(workflow, cancellationToken);
+        }
+
         private static void CheckWhetherRunnable(EWorkflowStatus status)
         {
             if (status is not EWorkflowStatus.Runnable)
@@ -675,89 +737,103 @@ namespace Hotline.FlowEngine.Workflows
                 throw new UserFriendlyException("无办理权限");
         }
 
-        private async Task<WorkflowStep> CreateStepAsync(Workflow workflow, StepDefine stepBoxDefine, BasicWorkflowDto dto,
+        /// <summary>
+        /// 创建开始节点(保存开始流程的办理意见,对应definition的start节点)
+        /// </summary>
+        private async Task<(WorkflowStep stepBox, WorkflowStep step)> CreateStartStepAsync(Workflow workflow, BasicWorkflowDto dto, CancellationToken cancellationToken)
+        {
+            if (workflow.StepBoxes.Any())
+                throw UserFriendlyException.SameMessage("无法反复开始流程");
+
+            var startStepDefinition = workflow.Definition.Steps.FirstOrDefault(d => d.StepType == EStepType.Start);
+            if (startStepDefinition == null)
+                throw new UserFriendlyException($"模板未配置开始节点, defineCode: {workflow.Definition.Code}", "模板未配置开始节点");
+
+            var stepBox = CreateStepBox(workflow.Id, startStepDefinition, dto, string.Empty);
+            await _workflowStepRepository.AddAsync(stepBox, cancellationToken);
+
+            //start节点的办理人分类默认为用户,即为当前发起流程的操作员
+            var handler = new IdName { Id = _sessionContext.RequiredUserId, Name = _sessionContext.UserName };
+            await CreateStartSubStepAsync(handler, dto, stepBox, null, cancellationToken);
+            return (stepBox, stepBox.Steps.First());
+        }
+
+        /// <summary>
+        /// 创建节点(不含开始、结束节点)
+        /// </summary>
+        private async Task<WorkflowStep> CreateStepAsync(Workflow workflow, StepDefine stepBoxDefine, BasicWorkflowDto dto, EWorkflowStepStatus status,
             WorkflowStep? prevStepBox = null, WorkflowStep? prevStep = null, CancellationToken cancellationToken = default)
         {
             if (stepBoxDefine.StepType is EStepType.Start or EStepType.End)
-                throw new UserFriendlyException("开始和结束节点无法创建子节点");
+                throw new UserFriendlyException("该方法不支持创建开始或结束节点");
             var stepBox = workflow.StepBoxes.FirstOrDefault(d => d.Code == stepBoxDefine.Code);
             if (stepBox == null)
             {
                 stepBox = CreateStepBox(workflow.Id, stepBoxDefine, dto, prevStepBox?.Id ?? string.Empty);
                 await _workflowStepRepository.AddAsync(stepBox, cancellationToken);
             }
-
-            if (stepBoxDefine.StepType is EStepType.CountersignEnd && workflow.IsInCountersign())
+            else if (stepBox.Status != EWorkflowStepStatus.Created)
             {
-                if (prevStep is null)
-                    throw new UserFriendlyException($"汇总节点的上级节点不能为空节点,workflowId: {workflow.Id}", "创建汇总节点异常");
-
-                var countersignId = string.IsNullOrEmpty(prevStep.TopCountersignId)
-                    ? prevStep.PrevCountersignId
-                    : prevStep.TopCountersignId;
-
-                var step = stepBox.Steps.FirstOrDefault(d => d.PrevCountersignId == countersignId);
-                if (step != null) return stepBox;
-
-                var countersignStartStep = FindCountersignStartStep(workflow, stepBoxDefine.CountersignStartCode, countersignId);
-                string? topCountersignId = countersignStartStep.StepType is EStepType.CountersignEnd
-                    ? countersignStartStep.TopCountersignId
-                    : countersignStartStep.IsInCountersign
-                        ? countersignStartStep.PrevCountersignId
-                        : null;
-
-                await CreateSubStepsAsync(stepBoxDefine, dto, stepBox, prevStep.Id, EWorkflowStepStatus.Created,
-                    countersignId, topCountersignId, cancellationToken);
+                stepBox.Status = EWorkflowStepStatus.Created;
+                await _workflowStepRepository.UpdateAsync(stepBox, cancellationToken);
             }
-            else
-            {
-                if (prevStep is null)
-                {
-
-                    //开始流程或向后跳转场景
-                    await CreateSubStepsAsync(stepBoxDefine, dto, stepBox, string.Empty, EWorkflowStepStatus.Assigned,
-                        null, null, cancellationToken);
-                }
-                else
-                {
-                    var prevCountersignId = prevStep.HasStartCountersign
-                        ? prevStep.StartCountersignId
-                        : prevStep.PrevCountersignId;
 
-                    await CreateSubStepsAsync(stepBoxDefine, dto, stepBox, prevStep.Id, EWorkflowStepStatus.Assigned,
-                        prevCountersignId, null, cancellationToken);
-                }
+            //下一节点为汇总节点时,同一会签只需要创建一次汇总节点
+            if (stepBoxDefine.StepType is EStepType.CountersignEnd && prevStep.StepCountersignStatus == EStepCountersignStatus.InCountersign)
+            {
+                var step = stepBox.Steps.FirstOrDefault(d =>
+                    d.IsInCountersign && d.CountersignId == prevStep.CountersignId);
+                if (step != null)
+                    return stepBox;
             }
 
+            await CreateSubStepsAsync(stepBoxDefine, dto, stepBox, status, prevStep, cancellationToken);
+            
             return stepBox;
         }
 
+        private async Task CreateStartSubStepAsync(
+            IdName handler,
+            BasicWorkflowDto dto,
+            WorkflowStep stepBox,
+            string? startCountersignId = null,
+            CancellationToken cancellationToken = default)
+        {
+            var nextCountersignStatus = string.IsNullOrEmpty(startCountersignId)
+                ? EStepCountersignStatus.InCountersign
+                : EStepCountersignStatus.None;
+            var subStep = CreateSubStep(stepBox, handler, dto.NextMainHandler, null, startCountersignId, null,
+                EWorkflowStepStatus.Completed, nextCountersignStatus);
+            _mapper.Map(dto, subStep);
+            stepBox.Steps.Add(subStep);
+            await _workflowStepRepository.AddAsync(subStep, cancellationToken);
+        }
+
         private async Task CreateSubStepsAsync(
             StepDefine stepBoxDefine,
             BasicWorkflowDto dto,
             WorkflowStep stepBox,
-            string prevStepId,
             EWorkflowStepStatus stepStatus,
-            string? prevCountersignId = null,
-            string? topCountersignId = null,
+            WorkflowStep? prevStep = null,
             CancellationToken cancellationToken = default)
         {
+            //开始节点无会签
+            var countersignStatus = prevStep?.GetNextStepCountersignStatus() ?? EStepCountersignStatus.None;
+            List<WorkflowStep> subSteps;
             if (stepBoxDefine.HandlerType is EHandlerType.AssignUser or EHandlerType.AssignOrg)
             {
-                var subSteps = CreateSubSteps(stepBox, stepBox.HandlerClassifies, dto.NextMainHandler,
-                    prevStepId, prevCountersignId, topCountersignId, stepStatus);
-                stepBox.Steps.AddRange(subSteps);
-                await _workflowStepRepository.AddRangeAsync(subSteps, cancellationToken);
+                subSteps = CreateSubSteps(stepBox, stepBox.HandlerClassifies, dto.NextMainHandler,
+                    prevStep?.Id, prevStep?.StartCountersignId, stepStatus, countersignStatus);
             }
             else
             {
                 if (stepBoxDefine.HandlerType != EHandlerType.Role && !dto.NextHandlers.Any())
                     throw new UserFriendlyException("未指定节点处理者");
-                var subSteps = CreateSubSteps(stepBox, dto.NextHandlers, dto.NextMainHandler,
-                    prevStepId, prevCountersignId, topCountersignId, stepStatus);
-                stepBox.Steps.AddRange(subSteps);
-                await _workflowStepRepository.AddRangeAsync(subSteps, cancellationToken);
+                subSteps = CreateSubSteps(stepBox, dto.NextHandlers, dto.NextMainHandler,
+                    prevStep?.Id, prevStep?.StartCountersignId, stepStatus, countersignStatus);
             }
+            stepBox.Steps.AddRange(subSteps);
+            await _workflowStepRepository.AddRangeAsync(subSteps, cancellationToken);
         }
 
         private (WorkflowStep stepBox, WorkflowStep step) GetStep(List<WorkflowStep> stepBoxes, string stepId)
@@ -807,10 +883,9 @@ namespace Hotline.FlowEngine.Workflows
             return new();
         }
 
-        private WorkflowStep CreateStepBox(string workflowId, StepDefine stepBasic, BasicWorkflowDto dto, string prevStepBoxId)
+        private WorkflowStep CreateStepBox(string workflowId, StepDefine stepDefine, BasicWorkflowDto dto, string prevStepBoxId)
         {
-            var stepBox = _mapper.Map<WorkflowStep>(stepBasic);
-            _mapper.Map(dto, stepBox);
+            var stepBox = _mapper.Map<WorkflowStep>(stepDefine);
             stepBox.WorkflowId = workflowId;
             stepBox.PreviousId = prevStepBoxId;
             return stepBox;
@@ -818,30 +893,61 @@ namespace Hotline.FlowEngine.Workflows
 
         private List<WorkflowStep> CreateSubSteps(
             WorkflowStep stepBox,
-            List<IdName> nextHandlers,
+            List<IdName> handlers,
             string nextMainHandler,
-            string prevStepId,
-            string? prevCountersignId,
-            string? topCountersignId,
-            EWorkflowStepStatus stepStatus)
+            string? prevStepId,
+            string? countersignId,
+            EWorkflowStepStatus stepStatus,
+            EStepCountersignStatus countersignStatus)
         {
+            if (countersignStatus is EStepCountersignStatus.None && !string.IsNullOrEmpty(countersignId))
+                throw UserFriendlyException.SameMessage("非法参数");
+            if (countersignStatus is not EStepCountersignStatus.None && string.IsNullOrEmpty(countersignId))
+                throw UserFriendlyException.SameMessage("非法参数");
+
             var steps = new List<WorkflowStep>();
-            foreach (var nextHandler in nextHandlers)
-            {
-                var step = _mapper.Map<WorkflowStep>(stepBox);
-                step.ParentId = stepBox.Id;
-                step.HandlerId = nextHandler.Id;
-                step.IsMain = nextHandler.Id == nextMainHandler;
-                step.PreviousId = prevStepId;
-                step.PrevCountersignId = prevCountersignId;
-                step.TopCountersignId = topCountersignId;
-                step.Status = stepStatus;
+            foreach (var handler in handlers)
+            {
+                //var step = _mapper.Map<WorkflowStep>(stepBox);
+                //step.ParentId = stepBox.Id;
+                //step.HandlerId = handler.Id;
+                //step.IsMain = handler.Id == nextMainHandler;
+                //step.PreviousId = prevStepId;
+                //step.StartCountersignId = startCountersignId;
+                //step.CountersignId = countersignId;
+                //step.Status = stepStatus;
+
+                var step = CreateSubStep(stepBox, handler, nextMainHandler,
+                    prevStepId, null, countersignId, stepStatus, countersignStatus);
                 steps.Add(step);
             }
 
             return steps;
         }
 
+        private WorkflowStep CreateSubStep(
+            WorkflowStep stepBox,
+            IdName handler,
+            string nextMainHandler,
+            string? prevStepId,
+            string? startCountersignId,
+            string? countersignId,
+            EWorkflowStepStatus stepStatus,
+            EStepCountersignStatus countersignStatus)
+        {
+            var step = _mapper.Map<WorkflowStep>(stepBox);
+            step.ParentId = stepBox.Id;
+            step.HandlerId = handler.Id;
+            step.IsMain = handler.Id == nextMainHandler;
+            step.PreviousId = prevStepId;
+            step.StartCountersignId = startCountersignId;
+            step.CountersignId = countersignId;
+            step.Status = stepStatus;
+            step.StepCountersignStatus = countersignStatus;
+
+            return step;
+        }
+
         /// <summary>
         /// 依据配置生成过期时间
         /// </summary>

+ 70 - 27
src/Hotline/FlowEngine/Workflows/WorkflowStep.cs

@@ -17,7 +17,8 @@ public class WorkflowStep : StepBasicEntity
     /// <summary>
     /// 前一级节点Id(stepBox此字段为上级stepBoxId,step为上级stepId),汇总节点无此字段(可能有多个上级来源)
     /// </summary>
-    public string PreviousId { get; set; }
+    [SugarColumn(IsNullable = true)]
+    public string? PreviousId { get; set; }
 
     /// <summary>
     /// 主办
@@ -37,17 +38,17 @@ public class WorkflowStep : StepBasicEntity
 
     #region 会签
 
-    /// <summary>
-    /// 发起会签节点code(不支持发起会签节点无此字段)
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? CountersignStartCode { get; set; }
+    ///// <summary>
+    ///// 发起会签节点code(不支持发起会签节点无此字段)
+    ///// </summary>
+    //[SugarColumn(IsNullable = true)]
+    //public string? CountersignStartCode { get; set; }
 
-    /// <summary>
-    /// 会签汇总节点code(不支持发起会签节点无此字段)
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? CountersignEndCode { get; set; }
+    ///// <summary>
+    ///// 会签汇总节点code(不支持发起会签节点无此字段)
+    ///// </summary>
+    //[SugarColumn(IsNullable = true)]
+    //public string? CountersignEndCode { get; set; }
 
     /// <summary>
     /// 发起会签生成会签Id(不发起会签节点无此字段)
@@ -56,36 +57,61 @@ public class WorkflowStep : StepBasicEntity
     public string? StartCountersignId { get; set; }
 
     /// <summary>
-    /// 上级会签Id,(不支持发起会签节点无此字段)
+    /// 会签id
     /// </summary>
     [SugarColumn(IsNullable = true)]
-    public string? PrevCountersignId { get; set; }
-
-    /// <summary>
-    /// 汇总节点保存最顶级会签节点的会签Id
+    public string? CountersignId { get; set; }
+
+    ///// <summary>
+    ///// 上级会签Id,(不支持发起会签节点无此字段)
+    ///// </summary>
+    //[SugarColumn(IsNullable = true)]
+    //public string? PrevCountersignId { get; set; }
+
+    ///// <summary>
+    ///// 汇总节点保存最顶级会签节点的会签Id
+    ///// <remarks>
+    ///// 1.如果当前会签流程前还有上级会签流程,当前汇总节点保存上级会签流程的会签Id
+    ///// 2.如果从汇总节点直接发起会签,上级会签流程的会签Id应该继续往下传递,直到上级会签流程汇总
+    ///// </remarks>
+    ///// </summary>
+    //[SugarColumn(IsNullable = true)]
+    //public string? TopCountersignId { get; set; }
+
+    /// <summary>
+    /// 当前节点发起的会签是否已结束,冗余(未发起会签的节点此字段为null)
     /// <remarks>
-    /// 1.如果当前会签流程前还有上级会签流程,当前汇总节点保存上级会签流程的会签Id
-    /// 2.如果从汇总节点直接发起会签,上级会签流程的会签Id应该继续往下传递,直到上级会签流程汇总
+    /// 发起的会签结束以后更新,如果有子会签,则所有子会签都结束以后才会更新此字段
     /// </remarks>
     /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? TopCountersignId { get; set; }
+    public bool? IsStartedCountersignComplete { get; set; }
 
     /// <summary>
-    /// 当前节点发起的会签是否已结束(未发起会签的节点无此字段)
+    /// 节点会签状态
     /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public bool? IsCountersignComplete { get; set; }
+    public EStepCountersignStatus StepCountersignStatus { get; set; }
+
+    /// <summary>
+    /// 发起会签节点code,冗余(不支持发起会签节点无此字段)
+    /// </summary>
+    public string? CountersignStartCode { get; set; }
+
+    /// <summary>
+    /// 会签汇总节点code,冗余(不支持发起会签节点无此字段)
+    /// </summary>
+    public string? CountersignEndCode { get; set; }
 
 
     /// <summary>
     /// 是否处于会签流程中
     /// </summary>
-    public bool IsInCountersign => !string.IsNullOrEmpty(PrevCountersignId);
+    [SugarColumn(IsIgnore = true)]
+    public bool IsInCountersign => !string.IsNullOrEmpty(CountersignId);
 
     /// <summary>
-    /// 是否发起会签
+    /// 是否发起会签
     /// </summary>
+    [SugarColumn(IsIgnore = true)]
     public bool HasStartCountersign => !string.IsNullOrEmpty(StartCountersignId);
 
     #endregion
@@ -141,18 +167,35 @@ public class WorkflowStep : StepBasicEntity
     /// <summary>
     /// 会签结束
     /// </summary>
-    public void CountersignComplete() => IsCountersignComplete = true;
+    public void CountersignComplete() => IsStartedCountersignComplete = true;
 
     /// <summary>
     /// 开启会签
     /// </summary>
-    public void StartCountersign() => StartCountersignId = Guid.NewGuid().ToString();
+    public void StartCountersign(string startCountersignId)
+    {
+        StartCountersignId = startCountersignId;
+        IsStartedCountersignComplete = false;
+    }
 
     /// <summary>
     /// step设置为可接办状态
     /// </summary>
     public void SetAssigned() => Status = EWorkflowStepStatus.Assigned;
 
+    /// <summary>
+    /// 依据当前节点获取下一节点会签状态
+    /// </summary>
+    /// <returns></returns>
+    public EStepCountersignStatus GetNextStepCountersignStatus()
+    {
+        if (HasStartCountersign)
+        {
+            return EStepCountersignStatus.InCountersign;
+        }
+        return IsInCountersign ? EStepCountersignStatus.OuterCountersign : EStepCountersignStatus.None;
+    }
+
     #endregion
 
 }