Browse Source

definition

xf 2 years ago
parent
commit
4d9e319dcd
31 changed files with 779 additions and 365 deletions
  1. 1 0
      src/Hotline.Api/Controllers/OrgController.cs
  2. 88 3
      src/Hotline.Api/Controllers/WorkflowController.cs
  3. 1 1
      src/Hotline.Application/FlowEngine/IWorkflowApplication.cs
  4. 6 3
      src/Hotline.Application/FlowEngine/WorkflowApplication.cs
  5. 9 0
      src/Hotline.Repository.SqlSugar/Extensions/SqlSugarRepositoryExtensions.cs
  6. 2 1
      src/Hotline.Repository.SqlSugar/System/SystemOrganizeRepository.cs
  7. 19 0
      src/Hotline.Repository.SqlSugar/System/WorkflowBusinessRepository.cs
  8. 5 2
      src/Hotline.Share/Dtos/FlowEngine/DefinitionDto.cs
  9. 13 0
      src/Hotline.Share/Dtos/FlowEngine/NextStepOptions.cs
  10. 8 0
      src/Hotline.Share/Dtos/FlowEngine/QueryWorkflowPagedDto.cs
  11. 2 0
      src/Hotline.Share/Dtos/FlowEngine/StartWorkflowDto.cs
  12. 40 4
      src/Hotline.Share/Dtos/FlowEngine/WorkflowDto.cs
  13. 3 3
      src/Hotline.Share/Enums/FlowEngine/EHandlerType.cs
  14. 6 0
      src/Hotline.Share/Enums/FlowEngine/EWorkflowStepStatus.cs
  15. 6 0
      src/Hotline.Share/Requests/PagedKeywordRequest.cs
  16. 0 5
      src/Hotline.Share/Requests/PagedRequest.cs
  17. 28 3
      src/Hotline/FlowEngine/Definitions/Definition.cs
  18. 7 2
      src/Hotline/FlowEngine/Definitions/DefinitionDomainService.cs
  19. 1 1
      src/Hotline/FlowEngine/Definitions/IDefinitionDomainService.cs
  20. 11 1
      src/Hotline/FlowEngine/Definitions/StepBasic.cs
  21. 0 28
      src/Hotline/FlowEngine/WorkflowBusiness.cs
  22. 15 2
      src/Hotline/FlowEngine/Workflows/IWorkflowDomainService.cs
  23. 89 4
      src/Hotline/FlowEngine/Workflows/StepBasicEntity.cs
  24. 48 15
      src/Hotline/FlowEngine/Workflows/Workflow.cs
  25. 248 144
      src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs
  26. 71 63
      src/Hotline/FlowEngine/Workflows/WorkflowStep.cs
  27. 2 78
      src/Hotline/FlowEngine/Workflows/WorkflowTrace.cs
  28. 5 1
      src/Hotline/Identity/Accounts/Account.cs
  29. 13 0
      src/Hotline/Settings/IWorkflowBusinessRepository.cs
  30. 12 1
      src/Hotline/Settings/SystemOrganize.cs
  31. 20 0
      src/Hotline/Settings/WorkflowModule.cs

+ 1 - 0
src/Hotline.Api/Controllers/OrgController.cs

@@ -46,6 +46,7 @@ namespace Hotline.Api.Controllers
             var org = _mapper.Map<SystemOrganize>(dto);
             //处理编码
             org.OrgCode =await _systemOrganizeRepository.GetNewOrgCode(org.ParentId);
+            org.InitOrgLevel();
             await _systemOrganizeRepository.AddAsync(org, HttpContext.RequestAborted);
         }
 

+ 88 - 3
src/Hotline.Api/Controllers/WorkflowController.cs

@@ -1,14 +1,16 @@
 using Hotline.FlowEngine.Definitions;
+using Hotline.FlowEngine.Workflows;
+using Hotline.Identity.Roles;
 using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Settings;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.FlowEngine;
-using Hotline.Share.Dtos.Workflow;
 using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Requests;
+using Hotline.Users;
 using MapsterMapper;
-using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
-using SqlSugar;
+using XF.Domain.Authentications;
 using XF.Domain.Exceptions;
 
 namespace Hotline.Api.Controllers;
@@ -20,15 +22,33 @@ public class WorkflowController : BaseController
 {
     private readonly IDefinitionDomainService _definitionDomainService;
     private readonly IDefinitionRepository _definitionRepository;
+    private readonly IWorkflowDomainService _workflowDomainService;
+    private readonly IWorkflowRepository _workflowRepository;
+    private readonly IUserRepository _userRepository;
+    private readonly ISystemOrganizeRepository _organizeRepository;
+    private readonly IRoleRepository _roleRepository;
+    private readonly ISessionContext _sessionContext;
     private readonly IMapper _mapper;
 
     public WorkflowController(
         IDefinitionDomainService definitionDomainService,
         IDefinitionRepository definitionRepository,
+        IWorkflowDomainService workflowDomainService,
+        IWorkflowRepository workflowRepository,
+        IUserRepository userRepository,
+        ISystemOrganizeRepository organizeRepository,
+        IRoleRepository roleRepository,
+        ISessionContext sessionContext,
         IMapper mapper)
     {
         _definitionDomainService = definitionDomainService;
         _definitionRepository = definitionRepository;
+        _workflowDomainService = workflowDomainService;
+        _workflowRepository = workflowRepository;
+        _userRepository = userRepository;
+        _organizeRepository = organizeRepository;
+        _roleRepository = roleRepository;
+        _sessionContext = sessionContext;
         _mapper = mapper;
     }
 
@@ -163,4 +183,69 @@ public class WorkflowController : BaseController
         }
     }
 
+    /// <summary>
+    /// 分页查询流程
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("paged")]
+    public async Task<PagedDto<WorkflowDto>> QueryPaged([FromQuery] QueryWorkflowPagedDto dto)
+    {
+        var (total, items) = await _workflowRepository.Queryable()
+            .WhereIF(!string.IsNullOrEmpty(dto.ModuleId), d => d.ModuleId == dto.ModuleId)
+            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Id.Contains(dto.Keyword) || d.Title.Contains(dto.Keyword))
+            .ToPagedListAsync(dto, HttpContext.RequestAborted);
+
+        return new PagedDto<WorkflowDto>(total, _mapper.Map<IReadOnlyList<WorkflowDto>>(items));
+    }
+
+    [HttpGet("nextstep-options")]
+    public async Task<IReadOnlyList<NextStepOptions>> GetNextStepOptions(string workflowId)
+    {
+        var workflow = await _workflowDomainService.GetWorkflowAsync(workflowId, HttpContext.RequestAborted);
+        var nextStepDefines = _workflowDomainService.GetNextStepOptions(workflow, HttpContext.RequestAborted);
+
+        //todo 性能
+        var items = new List<NextStepOptions>();
+        foreach (var nextStepDefine in nextStepDefines)
+        {
+            var options = new NextStepOptions
+            {
+                Code = nextStepDefine.Code,
+                Name = nextStepDefine.Name,
+            };
+
+            switch (nextStepDefine.HandlerType)
+            {
+                case EHandlerType.AssignUser:
+                    var users = await _userRepository.QueryAsync(d => nextStepDefine.HandlerClassifies.Contains(d.Id));
+                    options.NextSteps = users.Select(d => new KeyValuePair<string, string>(d.Id, d.Name)).ToList();
+                    break;
+                case EHandlerType.AssignOrg:
+                    var orgs = await _organizeRepository.QueryAsync(d => nextStepDefine.HandlerClassifies.Contains(d.OrgCode));
+                    options.NextSteps = orgs.Select(d => new KeyValuePair<string, string>(d.OrgCode, d.OrgName)).ToList();
+                    break;
+                case EHandlerType.Role:
+                    var roles = await _roleRepository.Queryable().Includes(d => d.Accounts, d => d.User)
+                        .Where(d => nextStepDefine.HandlerClassifies.Contains(d.Id)).ToListAsync();
+                    options.NextSteps = roles.SelectMany(d => d.Accounts).Select(d => d.User).Select(d => new KeyValuePair<string, string>(d.Id, d.Name)).ToList();
+                    break;
+                case EHandlerType.OrgLevel:
+                    //todo 当前操作人所属部门的下级部门并且属于配置orgLevel的部门
+                    //var orgs = await _organizeRepository.QueryAsync(d =>
+                    //    d.IsEnable && d.OrgCode.StartsWith(_sessionContext.RequiredOrgCode) && d.OrgCode)
+                    break;
+                case EHandlerType.OrgType:
+                    break;
+
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+
+            items.Add(options);
+        }
+
+        return items;
+    }
+
 }

+ 1 - 1
src/Hotline.Application/FlowEngine/IWorkflowApplication.cs

@@ -4,7 +4,7 @@ namespace Hotline.Application.FlowEngine
 {
     public interface IWorkflowApplication
     {
-        Task<string> StartWorkflowAsync(StartWorkflowDto dto, CancellationToken cancellationToken = default);
+        Task<string> StartWorkflowAsync(StartWorkflowDto dto, string moduleId, CancellationToken cancellationToken = default);
         
         /// <summary>
         /// 流转至下一节点(节点办理)

+ 6 - 3
src/Hotline.Application/FlowEngine/WorkflowApplication.cs

@@ -1,6 +1,7 @@
 using Hotline.FlowEngine.Definitions;
 using Hotline.FlowEngine.Workflows;
 using Hotline.SeedData;
+using Hotline.Settings;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Enums.FlowEngine;
 using Hotline.Users;
@@ -17,6 +18,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     private readonly IWorkflowDomainService _workflowDomainService;
     private readonly IWorkflowRepository _workflowRepository;
     private readonly IUserRepository _userRepository;
+    private readonly IWorkflowBusinessRepository _workflowBusinessRepository;
     private readonly ISessionContext _sessionContext;
 
     public WorkflowApplication(
@@ -24,16 +26,18 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
         IWorkflowDomainService workflowDomainService,
         IWorkflowRepository workflowRepository,
         IUserRepository userRepository,
+        IWorkflowBusinessRepository workflowBusinessRepository,
         ISessionContext sessionContext)
     {
         _definitionDomainService = definitionDomainService;
         _workflowDomainService = workflowDomainService;
         _workflowRepository = workflowRepository;
         _userRepository = userRepository;
+        _workflowBusinessRepository = workflowBusinessRepository;
         _sessionContext = sessionContext;
     }
 
-    public async Task<string> StartWorkflowAsync(StartWorkflowDto dto, CancellationToken cancellationToken = default)
+    public async Task<string> StartWorkflowAsync(StartWorkflowDto dto, string moduleId, CancellationToken cancellationToken = default)
     {
         var definition = await _definitionDomainService.GetLastVersionDefinitionAsync(dto.DefinitionCode, cancellationToken);
         if (definition == null)
@@ -41,9 +45,8 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
         if (definition.Status != EDefinitionStatus.Enable)
             throw new UserFriendlyException("该模板不可用");
 
-        var workflow = await _workflowDomainService.CreateWorkflowAsync(definition, cancellationToken);
+        var workflow = await _workflowDomainService.CreateWorkflowAsync(definition, dto.Title, moduleId, cancellationToken);
         await _workflowDomainService.StartAsync(workflow, dto, cancellationToken);
-
         return workflow.Id;
     }
 

+ 9 - 0
src/Hotline.Repository.SqlSugar/Extensions/SqlSugarRepositoryExtensions.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Linq.Expressions;
 using System.Text;
 using System.Threading.Tasks;
+using Hotline.Share.Requests;
 using SqlSugar;
 using XF.Domain.Entities;
 
@@ -18,5 +19,13 @@ namespace Hotline.Repository.SqlSugar.Extensions
             var items = await query.ToPageListAsync(pageIndex, pageSize, total);
             return (total.Value, items);
         }
+
+        public static async Task<(int Total, List<TEntity> Items)> ToPagedListAsync<TEntity>(this ISugarQueryable<TEntity> query, PagedRequest dto, CancellationToken cancellationToken = default)
+            where TEntity : class, IEntity<string>, new()
+        {
+            RefAsync<int> total = 0;
+            var items = await query.ToPageListAsync(dto.PageIndex, dto.PageSize, total);
+            return (total.Value, items);
+        }
     }
 }

+ 2 - 1
src/Hotline.Repository.SqlSugar/System/SystemOrganizeRepository.cs

@@ -37,7 +37,8 @@ namespace Hotline.Repository.SqlSugar.System
             if (model!=null)
             {
                 //return $"{int.Parse(model.OrgCode) +1:###}";
-                return (int.Parse(model.OrgCode) + 1).ToString().PadLeft(3,'0');
+                //return (int.Parse(model.OrgCode) + 1).ToString().PadLeft(3,'0');
+                return int.Parse(model.OrgCode).ToString("000");
             }
             //如果不存在下级
             else

+ 19 - 0
src/Hotline.Repository.SqlSugar/System/WorkflowBusinessRepository.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Settings;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace Hotline.Repository.SqlSugar.System
+{
+    public class WorkflowBusinessRepository : BaseRepository<WorkflowModule>, IWorkflowBusinessRepository, IScopeDependency
+    {
+        public WorkflowBusinessRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
+        {
+        }
+    }
+}

+ 5 - 2
src/Hotline.Share/Dtos/FlowEngine/DefinitionDto.cs

@@ -36,10 +36,13 @@ namespace Hotline.Share.Dtos.FlowEngine
         public string Description { get; set; }
 
         public List<StepDefineDto> Steps { get; set; }
+        
+        public string ExternalData { get; set; }
 
+        /// <summary>
+        /// 业务模块名称
+        /// </summary>
         public string ModuleName { get; set; }
-
-        public string ExternalData { get; set; }
     }
 
     public class StepDefineDto : StepBasicDto

+ 13 - 0
src/Hotline.Share/Dtos/FlowEngine/NextStepOptions.cs

@@ -0,0 +1,13 @@
+namespace Hotline.Share.Dtos.FlowEngine;
+
+/// <summary>
+/// 下一节点配置参数
+/// </summary>
+public class NextStepOptions
+{
+    public string Code { get; set; }
+
+    public string Name { get; set; }
+
+    public IReadOnlyList<KeyValuePair<string, string>> NextSteps { get; set; }
+}

+ 8 - 0
src/Hotline.Share/Dtos/FlowEngine/QueryWorkflowPagedDto.cs

@@ -0,0 +1,8 @@
+using Hotline.Share.Requests;
+
+namespace Hotline.Share.Dtos.FlowEngine;
+
+public record QueryWorkflowPagedDto : PagedKeywordRequest
+{
+    public string? ModuleId { get; set; }
+}

+ 2 - 0
src/Hotline.Share/Dtos/FlowEngine/StartWorkflowDto.cs

@@ -3,5 +3,7 @@
     public class StartWorkflowDto : BasicWorkflowDto
     {
         public string DefinitionCode { get; set; }
+
+        public string Title { get; set; }
     }
 }

+ 40 - 4
src/Hotline.Share/Dtos/FlowEngine/WorkflowDto.cs

@@ -1,21 +1,44 @@
 using Hotline.Share.Enums.FlowEngine;
+using XF.Utility.EnumExtensions;
 
 namespace Hotline.Share.Dtos.FlowEngine
 {
     public class WorkflowDto
     {
+        public string Id { get; set; }
+
+        public string Title { get; set; }
+
+        /// <summary>
+        /// 到期时间
+        /// </summary>
+        public DateTime ExpiredTime { get; set; }
+        
+        public DateTime? CompleteTime { get; set; }
+
+        /// <summary>
+        /// 当前节点名称(会签状态此字段保存最外层会签办理节点名称)
+        /// </summary>
+        public string CurrentStepName { get; set; }
+
+        /// <summary>
+        /// 到达当前节点时间(stepBox创建时间)
+        /// </summary>
+        public DateTime CurrentStepTime { get; set; }
+        
+        public EWorkflowStatus Status { get; set; }
+
         public DefinitionDto Definition { get; set; }
 
         /// <summary>
         /// 主节点,依据流转进度动态生成
         /// </summary>
         public List<StepBoxDto> StepBoxes { get; set; }
-
-        public DateTime? CompleteTime { get; set; }
-
+        
         public List<WorkflowTraceDto> Traces { get; set; }
 
-        public EWorkflowStatus Status { get; set; }
+        public List<WorkflowSupplementDto> Supplements { get; set; }
+
     }
 
     public class StepBoxDto
@@ -39,4 +62,17 @@ namespace Hotline.Share.Dtos.FlowEngine
 
         public DateTime? CreationTime { get; set; }
     }
+
+    public class WorkflowSupplementDto
+    {
+        /// <summary>
+        /// 补充意见
+        /// </summary>
+        public string Opinion { get; set; }
+
+        /// <summary>
+        /// 附件
+        /// </summary>
+        public List<string> Additions { get; set; } = new();
+    }
 }

+ 3 - 3
src/Hotline.Share/Enums/FlowEngine/EHandlerType.cs

@@ -10,12 +10,12 @@ public enum EHandlerType
     /// <summary>
     /// 部门级别
     /// </summary>
-    DepLevel = 1,
+    OrgLevel = 1,
 
     /// <summary>
     /// 部门类型
     /// </summary>
-    DepType = 2,
+    OrgType = 2,
 
     /// <summary>
     /// 指定用户
@@ -25,5 +25,5 @@ public enum EHandlerType
     /// <summary>
     /// 指定部门
     /// </summary>
-    AssignDep = 4,
+    AssignOrg = 4,
 }

+ 6 - 0
src/Hotline.Share/Enums/FlowEngine/EWorkflowStepStatus.cs

@@ -4,6 +4,12 @@ namespace Hotline.Share.Enums.FlowEngine;
 
 public enum EWorkflowStepStatus
 {
+    /// <summary>
+    /// 已创建
+    /// </summary>
+    [Description("已创建")]
+    Created = 0,
+
     /// <summary>
     /// 已指派(待受理)
     /// </summary>

+ 6 - 0
src/Hotline.Share/Requests/PagedKeywordRequest.cs

@@ -0,0 +1,6 @@
+namespace Hotline.Share.Requests;
+
+public record PagedKeywordRequest : PagedRequest
+{
+    public string? Keyword { get; set; }
+}

+ 0 - 5
src/Hotline.Share/Requests/PagedRequest.cs

@@ -8,9 +8,4 @@ namespace Hotline.Share.Requests
 
         public int Skip() => (PageIndex - 1) * PageSize;
     }
-
-    public record PagedKeywordRequest : PagedRequest
-    {
-        public string? Keyword { get; set; }
-    }
 }

+ 28 - 3
src/Hotline/FlowEngine/Definitions/Definition.cs

@@ -1,5 +1,7 @@
-using Hotline.Share.Enums.FlowEngine;
+using Hotline.FlowEngine.Workflows;
+using Hotline.Share.Enums.FlowEngine;
 using SqlSugar;
+using XF.Domain.Exceptions;
 using XF.Domain.Repository;
 
 namespace Hotline.FlowEngine.Definitions;
@@ -26,10 +28,33 @@ public class Definition : CreationEntity
     [SugarColumn(ColumnDataType = "varchar(4000)", IsJson = true)]
     public List<StepDefine> Steps { get; set; } = new();
 
-    public string ModuleName { get; set; }
-
     [SugarColumn(ColumnDataType = "varchar(4000)", IsNullable = true)]
     public string? ExternalData { get; set; }
 
     public EDefinitionStatus Status { get; set; }
+
+    /// <summary>
+    /// 业务模块名称
+    /// </summary>
+    public string ModuleName { get; set; }
+
+    #region Method
+
+    public StepDefine FindStartStep() =>
+        Steps.First(d => d.StepType is EStepType.Start);
+
+    public List<StepDefine> FindSteps(List<NextStepDefine> steps) =>
+        Steps.Where(d => steps.Select(x => x.Code).Contains(d.Code)).ToList();
+
+    public List<StepDefine> FindSteps(List<NextStep> steps) =>
+        Steps.Where(d => steps.Select(x => x.Code).Contains(d.Code)).ToList();
+
+    public StepDefine? FindStep(string code)
+    {
+        if (!Steps.Any())
+            throw new UserFriendlyException($"流程未配置节点,DefineCode: {Code}", "流程配置错误");
+        return Steps.FirstOrDefault(d => d.Code == code);
+    }
+
+    #endregion
 }

+ 7 - 2
src/Hotline/FlowEngine/Definitions/DefinitionDomainService.cs

@@ -71,10 +71,12 @@ public class DefinitionDomainService : IDefinitionDomainService, IScopeDependenc
     /// <param name="definitionCode"></param>
     /// <param name="cancellationToken"></param>
     /// <returns></returns>
-    public async Task<IReadOnlyList<NextStepDefine>> GetInitNextStepsAsync(string definitionCode, CancellationToken cancellationToken)
+    public async Task<IReadOnlyList<StepDefine>> GetSecondStepsAsync(string definitionCode, CancellationToken cancellationToken)
     {
         var definition = await GetLastVersionDefinitionAsync(definitionCode, cancellationToken);
-        return definition.Steps.First(d => d.StepType == EStepType.Start).NextSteps;
+        if (definition == null)
+            throw new UserFriendlyException($"无效模板编码, code:{definitionCode}", "无效模板编码");
+        return definition.FindSteps(definition.FindStartStep().NextSteps);
     }
 
     #region private
@@ -105,6 +107,9 @@ public class DefinitionDomainService : IDefinitionDomainService, IScopeDependenc
 
         if (definition.Steps.GroupBy(d => d.Code).Count() < definition.Steps.Count)
             throw UserFriendlyException.SameMessage("一个模板内不允许出现code相同的节点");
+
+        if (definition.Steps.Count(d => d.NextSteps.Any(x => x.Code == "End")) > 1)
+            throw UserFriendlyException.SameMessage("结束节点只能有一个上级节点");
     }
 
     #endregion

+ 1 - 1
src/Hotline/FlowEngine/Definitions/IDefinitionDomainService.cs

@@ -25,6 +25,6 @@ namespace Hotline.FlowEngine.Definitions
         /// <param name="definitionCode"></param>
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
-        Task<IReadOnlyList<NextStepDefine>> GetInitNextStepsAsync(string definitionCode, CancellationToken cancellationToken);
+        Task<IReadOnlyList<StepDefine>> GetSecondStepsAsync(string definitionCode, CancellationToken cancellationToken);
     }
 }

+ 11 - 1
src/Hotline/FlowEngine/Definitions/StepBasic.cs

@@ -31,8 +31,18 @@ public class StepBasic
     public List<string> HandlerClassifies { get; set; } = new();
 
     /// <summary>
-    /// 会签模式
+    /// 当前环节会签模式
     /// </summary>
     public ECountersignMode CountersignMode { get; set; }
 
+    /// <summary>
+    /// 发起会签节点code(不支持发起会签节点无此字段)
+    /// </summary>
+    public string? CountersignStartCode { get; set; }
+
+    /// <summary>
+    /// 会签汇总节点code(不支持发起会签节点无此字段)
+    /// </summary>
+    public string? CountersignEndCode { get; set; }
+
 }

+ 0 - 28
src/Hotline/FlowEngine/WorkflowBusiness.cs

@@ -1,28 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using XF.Domain.Repository;
-
-namespace Hotline.FlowEngine;
-
-/// <summary>
-/// 工作流业务模块
-/// </summary>
-public class WorkflowBusiness
-{
-    /// <summary>
-    /// 工单管理
-    /// </summary>
-    public const string OrderManage = "OrderManage";
-
-    /// <summary>
-    /// key:value = 模块名称:工作流模板对应code
-    /// </summary>
-    public static Dictionary<string, string> Modules = new()
-    {
-        { OrderManage, "GongDanShenPi"},
-    };
-}

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

@@ -17,17 +17,20 @@ namespace Hotline.FlowEngine.Workflows
         /// </summary>
         /// <returns></returns>
         Task<IReadOnlyList<NextStepDefine>> GetNextStepsAsync(QueryWorkflowDto dto, CancellationToken cancellationToken);
-        
+
 
         ///////////
 
-        Task<Workflow> CreateWorkflowAsync(Definition definition, CancellationToken cancellationToken);
+        Task<Workflow> CreateWorkflowAsync(Definition definition, string title, string moduleId, CancellationToken cancellationToken);
 
         /// <summary>
         /// 进行流程的开始节点
         /// </summary>
         Task StartAsync(Workflow workflow, BasicWorkflowDto dto, CancellationToken cancellationToken);
 
+        /// <summary>
+        /// 有definition,steps,无traces
+        /// </summary>
         Task<Workflow> GetWorkflowAsync(string workflowId, CancellationToken cancellationToken);
 
         /// <summary>
@@ -62,6 +65,16 @@ namespace Hotline.FlowEngine.Workflows
         /// <returns></returns>
         Task SupplementAsync(Workflow workflow, EndWorkflowDto dto, CancellationToken cancellationToken);
 
+        /// <summary>
+        /// 终止流程
+        /// </summary>
+        Task TerminateAsync(string id, CancellationToken cancellationToken);
+
         StepDefine GetStepBoxDefine(Definition definition, string stepCode);
+
+        /// <summary>
+        /// 查询当前待办节点的下一级节点配置(办理参数)
+        /// </summary>
+        IReadOnlyList<StepDefine> GetNextStepOptions(Workflow workflow, CancellationToken cancellationToken);
     }
 }

+ 89 - 4
src/Hotline/FlowEngine/Workflows/StepBasicEntity.cs

@@ -6,6 +6,8 @@ namespace Hotline.FlowEngine.Workflows;
 
 public class StepBasicEntity : CreationEntity
 {
+    public string WorkflowId { get; set; }
+
     public string Name { get; set; }
 
     /// <summary>
@@ -13,8 +15,6 @@ public class StepBasicEntity : CreationEntity
     /// </summary>
     public string Code { get; set; }
 
-    public string TypeName { get; init; }
-
     public EStepType StepType { get; init; }
 
     /// <summary>
@@ -28,12 +28,97 @@ public class StepBasicEntity : CreationEntity
     /// 根据类型可能为:roles, depLevels, depTypes, depCodes, userIds
     /// </example>
     /// </summary>
-    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
+    [SugarColumn(ColumnDataType = "varchar(1000)", IsJson = true)]
     public List<string> HandlerClassifies { get; set; } = new();
 
     /// <summary>
-    /// 会签模式
+    /// 当前环节会签模式
     /// </summary>
     public ECountersignMode CountersignMode { get; set; }
 
+    #region 办理参数
+
+    /// <summary>
+    /// (下一节点办理人)根据审批者类型不同,此字段为不同内容
+    /// <example>
+    /// 部门等级/分类为:orgCodes, 角色为:userIds
+    /// </example>
+    /// </summary>
+    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
+    public List<string> NextHandlers { get; set; }
+
+    /// <summary>
+    /// 下一节点主办,(NextHandlers其中一个, 如果不是会签则只有一个)
+    /// </summary>
+    public string NextMainHandler { get; set; }
+
+    /// <summary>
+    /// 下一节点code
+    /// </summary>
+    public string NextStepCode { get; set; }
+
+    /// <summary>
+    /// 是否短信通知
+    /// </summary>
+    public bool AcceptSms { get; set; }
+
+    /// <summary>
+    /// 办理意见
+    /// </summary>
+    [SugarColumn(Length = 2000)]
+    public string Opinion { get; set; }
+
+    /// <summary>
+    /// 附件
+    /// </summary>
+    [SugarColumn(ColumnDataType = "varchar(1000)", IsJson = true)]
+    public List<string> Additions { get; set; }
+
+    #endregion
+
+    #region 办理
+
+    /// <summary>
+    /// 办理人
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? UserId { get; set; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? UserName { get; set; }
+
+    /// <summary>
+    /// 办理人部门code
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? OrgCode { get; set; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? OrgName { get; set; }
+
+    /// <summary>
+    /// 办理完成时间
+    /// </summary>
+    public DateTime? CompleteTime { get; set; }
+
+    #endregion
+
+    #region 接办
+
+    /// <summary>
+    /// 接办人
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? AcceptUserId { get; set; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? AcceptUserName { get; set; }
+
+    /// <summary>
+    /// 接办时间
+    /// </summary>
+    public DateTime? AcceptTime { get; set; }
+
+    #endregion
+    
 }

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

@@ -9,6 +9,10 @@ public class Workflow : CreationEntity
 {
     public string DefinitionId { get; set; }
 
+    public string ModuleId { get; set; }
+
+    public string Title { get; set; }
+
     /// <summary>
     /// 到期时间
     /// </summary>
@@ -19,7 +23,7 @@ public class Workflow : CreationEntity
     public EWorkflowStatus Status { get; set; }
 
     /// <summary>
-    /// 当前节点名称(会签状态此字段保存开启会签时办理节点名称)
+    /// 当前节点名称(会签状态此字段保存最外层会签办理节点名称)
     /// </summary>
     public string CurrentStepName { get; set; }
 
@@ -29,9 +33,9 @@ public class Workflow : CreationEntity
     public DateTime CurrentStepTime { get; set; }
 
     /// <summary>
-    /// 当前发起会签节点code(不处于会签状态则无值)
+    /// 当前会签办理节点code,嵌套会签为最外层会签办理节点code(不处于会签状态则无值)
     /// </summary>
-    public string? CurrentCountersignCode { get; set; }
+    public string? CurrentCountersignStepCode { get; set; }
 
 
     [Navigate(NavigateType.OneToOne, nameof(DefinitionId))]
@@ -44,7 +48,7 @@ public class Workflow : CreationEntity
     public List<WorkflowSupplement> Supplements { get; set; }
 
     /// <summary>
-    /// 主节点,依据流转进度动态生成
+    /// 主节点,依据流转进度动态生成或删除
     /// </summary>
     [SugarColumn(IsIgnore = true)]
     public List<WorkflowStep> StepBoxes { get; set; }
@@ -52,12 +56,23 @@ public class Workflow : CreationEntity
     [SugarColumn(IsIgnore = true)]
     public List<WorkflowTrace> Traces { get; set; } = new();
 
+    #region Mehod
+
+    /// <summary>
+    /// 流程结束
+    /// </summary>
     public void Complete()
     {
         Status = EWorkflowStatus.Completed;
         CompleteTime = DateTime.Now;
     }
 
+    public void Terminate()
+    {
+        Status = EWorkflowStatus.Terminated;
+        CompleteTime = DateTime.Now;
+    }
+
     /// <summary>
     /// 流程进行流转
     /// </summary>
@@ -66,36 +81,54 @@ public class Workflow : CreationEntity
         CurrentStepName = currentStepName;
         CurrentStepTime = currentStepTime;
         if (!string.IsNullOrEmpty(currentCountersignCode))
-            CurrentCountersignCode = currentCountersignCode;
+            CurrentCountersignStepCode = currentCountersignCode;
     }
 
     /// <summary>
-    /// 检查会签是否结束,并更新当前会签节点
+    /// 检查会签是否结束
     /// </summary>
-    public void CheckCountersignAndUpdate()
+    public void CompleteCountersign()
     {
         if (IsInCountersign())
         {
-            var countersignStepBox = StepBoxes.First(d => d.Code == CurrentCountersignCode);
-            var countersignOver = countersignStepBox.Steps.All(d => d.Status is EWorkflowStepStatus.Completed);//todo 有bug,考虑会签嵌套的场景
-            if (countersignOver) //会签结束
-                CurrentCountersignCode = null;
+            var countersignStepBox = StepBoxes.First(d => d.Code == CurrentCountersignStepCode);
+            var isCountersignOver = countersignStepBox.Steps.All(d =>
+                d.Status is EWorkflowStepStatus.Completed &&
+                (!d.HasStartCountersign || d.IsCountersignComplete.GetValueOrDefault()));
+            if (isCountersignOver) //会签结束
+                CurrentCountersignStepCode = null;
         }
     }
 
-    public void CheckCurrentStepBoxCompleted()
+    /// <summary>
+    /// 更新workflow中当前停留节点,时间和会签开始节点code
+    /// </summary>
+    /// <param name="isStartCountersign"></param>
+    /// <param name="stepBox"></param>
+    public void SetWorkflowCurrentStepInfo(bool isStartCountersign, WorkflowStep stepBox)
     {
-
+        //1.不在会签中,未发起会签(普通处理) 2.不在会签中,发起会签(保存会签节点),3.会签中,不更新
+        if (IsInCountersign()) return;
+        if (isStartCountersign)
+        {
+            FLow(stepBox.Name, stepBox.CreationTime, stepBox.Code);
+        }
+        else
+        {
+            FLow(stepBox.Name, stepBox.CreationTime);
+        }
     }
 
     /// <summary>
     /// 流程是否处于会签中
     /// </summary>
     /// <returns></returns>
-    public bool IsInCountersign() => !string.IsNullOrEmpty(CurrentCountersignCode);
+    public bool IsInCountersign() => !string.IsNullOrEmpty(CurrentCountersignStepCode);
 
     /// <summary>
     /// 结束流程会签状态
     /// </summary>
-    public void CloseCountersignStatus() => CurrentCountersignCode = null;
+    public void CloseCountersignStatus() => CurrentCountersignStepCode = null;
+
+    #endregion
 }

+ 248 - 144
src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs

@@ -63,10 +63,12 @@ namespace Hotline.FlowEngine.Workflows
             return currentStep.NextSteps.Where(d => string.IsNullOrEmpty(d.Predicate)).ToList();
         }
 
-        public async Task<Workflow> CreateWorkflowAsync(Definition definition, CancellationToken cancellationToken)
+        public async Task<Workflow> CreateWorkflowAsync(Definition definition, string title, string moduleId, CancellationToken cancellationToken)
         {
             var workflow = new Workflow
             {
+                Title = title,
+                ModuleId = moduleId,
                 DefinitionId = definition.Id,
                 Status = EWorkflowStatus.Runnable,
                 ExpiredTime = GenerateExpiredTime(definition.Code),
@@ -89,18 +91,17 @@ namespace Hotline.FlowEngine.Workflows
         /// <returns></returns>
         public async Task StartAsync(Workflow workflow, BasicWorkflowDto dto, CancellationToken cancellationToken)
         {
+            var nextStepBoxDefine = GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
+
             if (dto.Handlers.Count > 1)
             {
-                var startStepDefine = workflow.Definition.Steps.First(d => d.StepType == EStepType.Start);
                 //检查是否支持会签
-                if (startStepDefine.CountersignMode == ECountersignMode.UnSupport)
-                    throw UserFriendlyException.SameMessage($"当前开始节点不支持开启会签, defineCode: {workflow.Definition.Code}");
+                if (nextStepBoxDefine.CountersignMode == ECountersignMode.UnSupport)
+                    throw new UserFriendlyException($"当前节点不支持会签, defineCode: {workflow.Definition.Code}", "当前节点不支持会签");
             }
 
-            var nextStepBoxDefine = GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
-
             //第二节点的previousId is string.Empty
-            await CreateStepAsync(workflow, nextStepBoxDefine, dto, string.Empty, string.Empty, cancellationToken);
+            await CreateStepAsync(workflow, nextStepBoxDefine, dto, cancellationToken: cancellationToken);
         }
 
         public async Task<Workflow> GetWorkflowAsync(string workflowId, CancellationToken cancellationToken)
@@ -137,6 +138,16 @@ namespace Hotline.FlowEngine.Workflows
             var (currentStepBox, currentStep) = GetUnCompleteStepOrDefault(workflow.StepBoxes, _sessionContext.RequiredOrgCode, _sessionContext.RequiredUserId);
             if (currentStep is null) return;
             if (currentStep.Status is EWorkflowStepStatus.Accepted) return;
+            if (currentStep.HandlerType is EHandlerType.AssignUser or EHandlerType.Role)
+            {
+                //userId
+                if (currentStep.HandlerId != _sessionContext.RequiredUserId) return;
+            }
+            else
+            {
+                //orgId
+                if (currentStep.HandlerId != _sessionContext.RequiredOrgCode) return;
+            }
             if (currentStep.StepType is EStepType.End)
                 throw new UserFriendlyException("当前流程已流转到最终步骤");
 
@@ -156,41 +167,55 @@ namespace Hotline.FlowEngine.Workflows
         {
             CheckWhetherRunnable(workflow.Status);
 
-            //赋值当前节点
+            #region 办理当前节点
+
             var (currentStepBox, currentStep) = GetUnCompleteStep(workflow.StepBoxes, _sessionContext.RequiredOrgCode, _sessionContext.RequiredUserId);
             if (currentStep.Status is EWorkflowStepStatus.Assigned)
                 await AcceptAsync(workflow, cancellationToken);
             if (currentStep.StepType is EStepType.End)
                 throw new UserFriendlyException("当前流程已流转到最终步骤");
 
-            //下一节点是否发起会签
-            var isCountersign = dto.Handlers.Count > 1;
-            //检查是否支持开启会签
-            if (isCountersign && currentStep.CountersignMode == ECountersignMode.UnSupport)
-                throw UserFriendlyException.SameMessage($"当前节点不支持开启会签, code: {currentStep.Code}");
+            //是否发起会签
+            var isStartCountersign = dto.Handlers.Count > 1;
+            //检查是否支持会签办理
+            if (isStartCountersign && nextStepBoxDefine.CountersignMode == ECountersignMode.UnSupport)
+                throw UserFriendlyException.SameMessage($"下一节点不支持会签办理, code: {currentStep.Code}");
+            if (isStartCountersign)
+                currentStep.StartCountersign();
 
             _mapper.Map(dto, currentStep);
 
-            currentStep.Complete(
-                _sessionContext.RequiredUserId,
-                _sessionContext.UserName,
-                _sessionContext.RequiredOrgCode,
-                _sessionContext.OrgName,
+            //step办理状态
+            currentStep.StepComplete(
+                _sessionContext.RequiredUserId, _sessionContext.UserName,
+                _sessionContext.RequiredOrgCode, _sessionContext.OrgName,
                 dto.NextStepCode);
 
-            if (currentStepBox.Status is not EWorkflowStepStatus.Completed &&
-                currentStepBox.Steps.All(d => d.Status is EWorkflowStepStatus.Completed))
+            //stepBox办理状态
+            currentStepBox.CheckStepBoxStatusAndUpdate();
+
+            var updateSteps = new List<WorkflowStep> { currentStepBox, currentStep };
+            //结束当前会签流程
+            if (currentStep.StepType is EStepType.CountersignEnd && currentStep.IsInCountersign)
             {
-                currentStepBox.Status = EWorkflowStepStatus.Completed;
-                currentStepBox.CompleteTime = currentStepBox.Steps.Max(d => d.CompleteTime);
+                var countersignStartStep = FindCountersignStartStep(workflow, currentStep.CountersignStartCode, currentStep.PrevCountersignId);
+                if (countersignStartStep.HasStartCountersign)
+                {
+                    countersignStartStep.CountersignComplete();
+                    updateSteps.Add(countersignStartStep);
+                }
             }
-            await _workflowStepRepository.UpdateRangeAsync(new List<WorkflowStep> { currentStepBox, currentStep }, cancellationToken);
 
-            //检查会签是否结束,并更新当前会签节点
-            workflow.CheckCountersignAndUpdate();
+            await _workflowStepRepository.UpdateRangeAsync(updateSteps, cancellationToken);
+
+            #endregion
+
+            #region 处理流程
+
+            //检查会签是否结束,并更新当前会签节点字段
+            workflow.CompleteCountersign();
 
             //检查是否流转到流程终点
-            //var nextStepBoxDefine = GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
             if (nextStepBoxDefine.StepType is EStepType.End)
             {
                 workflow.Complete();
@@ -200,25 +225,79 @@ namespace Hotline.FlowEngine.Workflows
                 return;
             }
 
-            //判断是否从中心流转出去,重新计算expiredTime 
+            //是否从中心流转出去,重新计算expiredTime 
             if (isOutOfCallCenter)
                 workflow.ExpiredTime = GenerateExpiredTime(workflow.Definition.Code);
 
             //创建下一节点
-            var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, currentStepBox.Id, currentStep.Id, cancellationToken);
+            var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, currentStepBox, currentStep, cancellationToken);
+
+            //下一节点为汇总节点时,检查下一节点是否可办理
+            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);
+
+                }
+                else
+                {
+                    await UpdateNextCountersignEndAssignedAsync(nextStepBox, currentStep, cancellationToken);
+                }
+            }
 
-            //不在会签中的流程,更新当前节点名称、时间、会签节点code
-            if (!workflow.IsInCountersign())
-                SetWorkflowCurrentStepInfo(workflow, isCountersign, nextStepBox);
+            //更新当前节点名称、时间、会签节点code 等字段
+            workflow.SetWorkflowCurrentStepInfo(isStartCountersign, nextStepBox);
 
             await _workflowRepository.UpdateAsync(workflow, cancellationToken);
 
-            //trace
+            #endregion
+
+            #region 流转记录
+
             await NextTraceAsync(workflow, dto, currentStep, cancellationToken);
 
+            #endregion
+
             await _mediator.Publish(new NextStepNotify(workflow.Id, dto), cancellationToken);
         }
 
+        private async Task UpdateNextCountersignEndAssignedAsync(WorkflowStep nextStepBox, WorkflowStep currentStep, CancellationToken cancellationToken)
+        {
+            var countersignId = string.IsNullOrEmpty(currentStep.TopCountersignId)
+                ? currentStep.PrevCountersignId
+                : currentStep.TopCountersignId;
+
+            var nextStep = nextStepBox.Steps.First(d => d.PrevCountersignId == countersignId);
+            nextStep.SetAssigned();
+
+            await _workflowStepRepository.UpdateAsync(nextStep, cancellationToken);
+
+            //todo publish
+        }
+
+
         /// <summary>
         /// 退回(返回前一节点)
         /// </summary>
@@ -269,19 +348,10 @@ namespace Hotline.FlowEngine.Workflows
             var targetStepBox = workflow.StepBoxes.FirstOrDefault(d => d.Code == dto.TargetStepCode);
             if (targetStepBox == null)
             {
-                //向后跳转
-                var lastStepBox = workflow.StepBoxes.OrderBy(d => d.CreationTime).Last();
-                if (lastStepBox.StepType is EStepType.Start or EStepType.End)
-                    throw UserFriendlyException.SameMessage("无法跳转至开始或结束节点");
-
                 var nextStepBoxDefine = GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
-                var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, lastStepBox.Id, lastStepBox.Id, cancellationToken);
+                var nextStepBox = await CreateStepAsync(workflow, nextStepBoxDefine, dto, cancellationToken: cancellationToken);
 
-                //更新当前节点名称、时间、会签节点code
-                workflow.CloseCountersignStatus();
-                var isCountersign = dto.Handlers.Count > 1;
-                SetWorkflowCurrentStepInfo(workflow, isCountersign, nextStepBox);
-                await _workflowRepository.UpdateAsync(workflow, cancellationToken);
+                await ResetWorkflowCurrentStepInfo(workflow, dto, nextStepBox, cancellationToken);
 
                 #region 补充中间节点处理方案
 
@@ -317,6 +387,15 @@ namespace Hotline.FlowEngine.Workflows
             //todo publish
         }
 
+        private async Task ResetWorkflowCurrentStepInfo(Workflow workflow, RecallDto dto, WorkflowStep stepBox, CancellationToken cancellationToken)
+        {
+            //更新当前节点名称、时间、会签节点code
+            workflow.CloseCountersignStatus();
+            var isCountersign = dto.Handlers.Count > 1;
+            workflow.SetWorkflowCurrentStepInfo(isCountersign, stepBox);
+            await _workflowRepository.UpdateAsync(workflow, cancellationToken);
+        }
+
         /// <summary>
         /// 补充
         /// </summary>
@@ -330,6 +409,18 @@ namespace Hotline.FlowEngine.Workflows
             await _workflowSupplementRepository.AddAsync(supplement, cancellationToken);
         }
 
+        /// <summary>
+        /// 终止流程
+        /// </summary>
+        public async Task TerminateAsync(string id, CancellationToken cancellationToken)
+        {
+            var workflow = await _workflowRepository.GetAsync(id, cancellationToken);
+            if (workflow == null)
+                throw UserFriendlyException.SameMessage("无效的流程编号");
+            workflow.Terminate();
+            await _workflowRepository.UpdateAsync(workflow, cancellationToken);
+        }
+
         /// <summary>
         /// 根据stepCode查询流程配置中对应的节点
         /// </summary>
@@ -337,32 +428,34 @@ namespace Hotline.FlowEngine.Workflows
         {
             if (definition == null) throw new ArgumentNullException(nameof(definition));
             if (string.IsNullOrEmpty(stepCode)) throw new ArgumentNullException(nameof(stepCode));
-            if (!definition.Steps.Any())
-                throw new UserFriendlyException($"流程未配置节点,DefineCode: {definition.Code}", "流程配置错误");
-            var stepDefine = definition.Steps.FirstOrDefault(d => d.Code == stepCode);
+            var stepDefine = definition.FindStep(stepCode);
             if (stepDefine == null)
                 throw new UserFriendlyException($"未找到流程中对应的节点,DefineCode: {definition.Code}, stepCode: {stepCode}",
                     "未查询到对应节点");
             return stepDefine;
         }
 
+        /// <summary>
+        /// 查询当前待办节点的下一级节点配置(办理参数)
+        /// </summary>
+        public IReadOnlyList<StepDefine> GetNextStepOptions(Workflow workflow, CancellationToken cancellationToken)
+        {
+            var (currentStepBox, currentStep) = GetUnCompleteStep(workflow.StepBoxes, _sessionContext.RequiredOrgCode, _sessionContext.RequiredUserId);
+            return workflow.Definition.FindSteps(currentStepBox.NextSteps);
+        }
+
 
         #region private
 
         /// <summary>
-        /// 更新workflow中当前停留节点,时间和会签开始节点code
+        /// 在stepCode对应的stepBox中找到开启会签流程的节点
         /// </summary>
-        private static void SetWorkflowCurrentStepInfo(Workflow workflow, bool isCountersign, WorkflowStep stepBox)
+        private static WorkflowStep FindCountersignStartStep(Workflow workflow, string startCountersignStepCode, string startCountersignId)
         {
-            //1.不在会签中,未发起会签(普通处理) 2.不在会签中,发起会签(保存会签节点),3.会签中,不更新
-            if (isCountersign)
-            {
-                workflow.FLow(stepBox.Name, stepBox.CreationTime, stepBox.Code);
-            }
-            else
-            {
-                workflow.FLow(stepBox.Name, stepBox.CreationTime);
-            }
+            var countersignStartStepBox = workflow.StepBoxes.First(d => d.Code == startCountersignStepCode);
+            var countersignStartStep =
+                countersignStartStepBox.Steps.First(d => d.StartCountersignId == startCountersignId);
+            return countersignStartStep;
         }
 
         private async Task JumpTraceAsync(string workflowId, RecallDto dto, CancellationToken cancellationToken)
@@ -447,8 +540,6 @@ namespace Hotline.FlowEngine.Workflows
             return parentTrace;
         }
 
-
-
         private async Task RecallAsync(Workflow workflow, RecallDto dto, WorkflowStep targetStepBox, CancellationToken cancellationToken)
         {
             //update uncompleted traces
@@ -456,7 +547,7 @@ namespace Hotline.FlowEngine.Workflows
 
             //remove completedSteps include target self
             var completeStepBoxes = workflow.StepBoxes.Where(d =>
-                d.Code == dto.TargetStepCode && d.CreationTime > targetStepBox.CreationTime);
+                d.Code == dto.TargetStepCode || d.CreationTime > targetStepBox.CreationTime);
             var removeSteps = new List<WorkflowStep>();
             foreach (var stepBox in completeStepBoxes)
             {
@@ -468,7 +559,18 @@ namespace Hotline.FlowEngine.Workflows
 
             //recreate targetStep
             var nextStepBoxDefine = GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
-            await CreateStepAsync(workflow, nextStepBoxDefine, dto, targetStepBox.PreviousId, targetStepBox.Steps.First().PreviousId, cancellationToken);
+            await CreateStepAsync(workflow, nextStepBoxDefine, dto, targetStepBox, targetStepBox.Steps.First(), cancellationToken);
+
+            //flow manage
+            if (workflow.IsInCountersign())
+            {
+                var currentCountersignStepBox =
+                    workflow.StepBoxes.First(d => d.Code == workflow.CurrentCountersignStepCode);
+                //目标节点在初始会签节点之前或正好
+                if (targetStepBox.Code == workflow.CurrentCountersignStepCode || targetStepBox.CreationTime < currentCountersignStepBox.CreationTime)
+                    await ResetWorkflowCurrentStepInfo(workflow, dto, targetStepBox, cancellationToken);
+
+            }
         }
 
         private static void CheckWhetherRunnable(EWorkflowStatus status)
@@ -477,28 +579,88 @@ namespace Hotline.FlowEngine.Workflows
                 throw new UserFriendlyException("当前流程状态不可继续流转");
         }
 
-        private async Task<WorkflowStep> CreateStepAsync(Workflow workflow, StepDefine stepBoxDefine, BasicWorkflowDto dto, string previousStepBoxId, string previousStepId, CancellationToken cancellationToken)
+        private async Task<WorkflowStep> CreateStepAsync(Workflow workflow, StepDefine stepBoxDefine, BasicWorkflowDto dto,
+            WorkflowStep? prevStepBox = null, WorkflowStep? prevStep = null, CancellationToken cancellationToken = default)
         {
-            var nextStepBox = workflow.StepBoxes.FirstOrDefault(d => d.Code == stepBoxDefine.Code);
-            nextStepBox ??= CreateStepBox(stepBoxDefine, dto, previousStepBoxId);
-            await _workflowStepRepository.AddAsync(nextStepBox, cancellationToken);
+            if (stepBoxDefine.StepType is EStepType.Start or EStepType.End)
+                throw new UserFriendlyException("开始和结束节点无法创建子节点");
+            var stepBox = workflow.StepBoxes.FirstOrDefault(d => d.Code == stepBoxDefine.Code);
+            if (stepBox == null)
+            {
+                stepBox = CreateStepBox(stepBoxDefine, dto, prevStepBox?.Id ?? string.Empty);
+                await _workflowStepRepository.AddAsync(stepBox, cancellationToken);
+            }
+
+            if (stepBoxDefine.StepType is EStepType.CountersignEnd)
+            {
+                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);
+            }
+            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.HandlerType is EHandlerType.AssignUser or EHandlerType.AssignDep)
+            return stepBox;
+        }
+
+        private async Task CreateSubStepsAsync(
+            StepDefine stepBoxDefine,
+            BasicWorkflowDto dto,
+            WorkflowStep stepBox,
+            string prevStepId,
+            EWorkflowStepStatus stepStatus,
+            string? prevCountersignId = null,
+            string? topCountersignId = null,
+            CancellationToken cancellationToken = default)
+        {
+            if (stepBoxDefine.HandlerType is EHandlerType.AssignUser or EHandlerType.AssignOrg)
             {
-                var subSteps = CreateSubSteps(nextStepBox, nextStepBox.HandlerClassifies, dto.NextMainHandler, previousStepId);
-                nextStepBox.Steps.AddRange(subSteps);
+                var subSteps = CreateSubSteps(stepBox, stepBox.HandlerClassifies, dto.NextMainHandler,
+                    prevStepId, prevCountersignId, topCountersignId, stepStatus);
+                stepBox.Steps.AddRange(subSteps);
                 await _workflowStepRepository.AddRangeAsync(subSteps, cancellationToken);
             }
             else
             {
                 if (!dto.Handlers.Any())
                     throw new UserFriendlyException("未指定节点处理者");
-                var subSteps = CreateSubSteps(nextStepBox, dto.Handlers, dto.NextMainHandler, previousStepId);
-                nextStepBox.Steps.AddRange(subSteps);
+                var subSteps = CreateSubSteps(stepBox, dto.Handlers, dto.NextMainHandler,
+                    prevStepId, prevCountersignId, topCountersignId, stepStatus);
+                stepBox.Steps.AddRange(subSteps);
                 await _workflowStepRepository.AddRangeAsync(subSteps, cancellationToken);
             }
-
-            return nextStepBox;
         }
 
         private (WorkflowStep stepBox, WorkflowStep step) GetStep(List<WorkflowStep> stepBoxes, string stepId)
@@ -548,51 +710,22 @@ namespace Hotline.FlowEngine.Workflows
             return new();
         }
 
-
-        private List<WorkflowStep> CreateStepByStepDefine(StepBasic stepBasic, List<string> handlers, string previousId)
-        {
-            return handlers.Select(d =>
-            {
-                var step = _mapper.Map<WorkflowStep>(stepBasic);
-                step.Id = Guid.NewGuid().ToString();
-                step.HandlerId = d;
-                if (!string.IsNullOrEmpty(previousId))
-                    step.PreviousId = previousId;
-                return step;
-            }).ToList();
-        }
-
-        private WorkflowStep CreateStepBox(StepBasic stepBasic, List<WorkflowStep>? steps = null)
-        {
-            var stepBox = _mapper.Map<WorkflowStep>(stepBasic);
-            if (steps != null && steps.Any())
-                stepBox.Steps.AddRange(steps);
-            return stepBox;
-        }
-
-        private WorkflowStep CreateStepBox(StepDefine stepBasic, BasicWorkflowDto dto, string previousStepBoxId)
+        private WorkflowStep CreateStepBox(StepDefine stepBasic, BasicWorkflowDto dto, string prevStepBoxId)
         {
             var stepBox = _mapper.Map<WorkflowStep>(stepBasic);
             _mapper.Map(dto, stepBox);
-            stepBox.PreviousId = previousStepBoxId;
+            stepBox.PreviousId = prevStepBoxId;
             return stepBox;
         }
 
-        //private WorkflowStep CreateStepBox(
-        //    StepDefine stepBasic,
-        //    NextWorkflowDto dto,
-        //    string workflowId,
-        //    string previousId = "")
-        //{
-        //    var stepBox = _mapper.Map<WorkflowStep>(stepBasic);
-        //    _mapper.Map(dto, stepBox);
-        //    stepBox.WorkflowId = workflowId;
-        //    if (!string.IsNullOrEmpty(previousId))
-        //        stepBox.PreviousId = previousId;
-        //    return stepBox;
-        //}
-
-        private List<WorkflowStep> CreateSubSteps(WorkflowStep stepBox, List<string> nextHandlers, string nextMainHandler, string previousStepId)
+        private List<WorkflowStep> CreateSubSteps(
+            WorkflowStep stepBox,
+            List<string> nextHandlers,
+            string nextMainHandler,
+            string prevStepId,
+            string? prevCountersignId,
+            string? topCountersignId,
+            EWorkflowStepStatus stepStatus)
         {
             return nextHandlers.Select(d =>
               {
@@ -600,43 +733,14 @@ namespace Hotline.FlowEngine.Workflows
                   step.ParentId = stepBox.Id;
                   step.HandlerId = d;
                   step.IsMain = d == nextMainHandler;
-                  step.PreviousId = previousStepId;
+                  step.PreviousId = prevStepId;
+                  step.PrevCountersignId = prevCountersignId;
+                  step.TopCountersignId = topCountersignId;
+                  step.Status = stepStatus;
                   return step;
               }).ToList();
         }
 
-        private WorkflowStep InitSubStepsInStepBox(WorkflowStep stepBox, List<string> nextHandlers)
-        {
-            var subSteps = nextHandlers.Select(d =>
-            {
-                var step = _mapper.Map<WorkflowStep>(stepBox);
-                step.ParentId = stepBox.Id;
-                step.HandlerId = d;
-                return step;
-            }).ToList();
-
-            stepBox.Steps = subSteps;
-            return stepBox;
-        }
-
-        private WorkflowTrace CreateTrace(string workflowId, WorkflowStep currentStep, NextWorkflowDto dto, EWorkflowTraceStatus status = EWorkflowTraceStatus.Normal)
-        {
-            var trace = _mapper.Map<WorkflowTrace>(currentStep);
-            _mapper.Map(dto, trace);
-            trace.WorkflowId = workflowId;
-            trace.Status = status;
-            return trace;
-        }
-
-        private WorkflowTrace CreateTrace(string workflowId, WorkflowStep currentStep, PreviousWorkflowDto dto, EWorkflowTraceStatus status = EWorkflowTraceStatus.Normal)
-        {
-            var trace = _mapper.Map<WorkflowTrace>(currentStep);
-            _mapper.Map(dto, trace);
-            trace.WorkflowId = workflowId;
-            trace.Status = status;
-            return trace;
-        }
-
         /// <summary>
         /// 依据配置生成过期时间
         /// </summary>

+ 71 - 63
src/Hotline/FlowEngine/Workflows/WorkflowStep.cs

@@ -5,9 +5,7 @@ namespace Hotline.FlowEngine.Workflows;
 
 public class WorkflowStep : StepBasicEntity
 {
-    public string WorkflowId { get; set; }
-
-    [SugarColumn(ColumnDataType = "varchar(4000)", IsJson = true)]
+    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
     public List<NextStep> NextSteps { get; set; }
 
     /// <summary>
@@ -16,102 +14,82 @@ public class WorkflowStep : StepBasicEntity
     public string HandlerId { get; set; }
 
     /// <summary>
-    /// 办理人部门code
+    /// 前一级节点Id(stepBox此字段为上级stepBoxId,step为上级stepId),汇总节点无此字段(可能有多个上级来源)
     /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? OrgCode { get; set; }
-
-    [SugarColumn(IsNullable = true)]
-    public string? OrgName { get; set; }
+    public string PreviousId { get; set; }
 
     /// <summary>
-    /// 办理人
+    /// 主办
     /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? UserId { get; set; }
-
-    [SugarColumn(IsNullable = true)]
-    public string? UserName { get; set; }
+    public bool IsMain { get; set; }
 
-    /// <summary>
-    /// 办理完成时间
-    /// </summary>
-    public DateTime? CompleteTime { get; set; }
+    public EWorkflowStepStatus Status { get; set; } = EWorkflowStepStatus.Assigned;
 
     /// <summary>
-    /// 接办人
+    /// stepBox此字段无效,记录stepBox与其下subSteps关系
     /// </summary>
     [SugarColumn(IsNullable = true)]
-    public string? AcceptUserId { get; set; }
+    public string? ParentId { get; set; }
 
-    [SugarColumn(IsNullable = true)]
-    public string? AcceptUserName { get; set; }
+    [SugarColumn(IsIgnore = true)]
+    public List<WorkflowStep> Steps { get; set; }
 
-    /// <summary>
-    /// 接办时间
-    /// </summary>
-    public DateTime? AcceptTime { get; set; }
+    #region 会签
 
     /// <summary>
-    /// 上一级节点Id(stepBox此字段为上级stepBoxId,step为上级stepId)
+    /// 发起会签节点code(不支持发起会签节点无此字段)
     /// </summary>
-    public string PreviousId { get; set; }
+    [SugarColumn(IsNullable = true)]
+    public string? CountersignStartCode { get; set; }
 
     /// <summary>
-    /// 主办
+    /// 会签汇总节点code(不支持发起会签节点无此字段)
     /// </summary>
-    public bool IsMain { get; set; }
-
-    public EWorkflowStepStatus Status { get; set; } = EWorkflowStepStatus.Assigned;
-
-    #region 审批参数
+    [SugarColumn(IsNullable = true)]
+    public string? CountersignEndCode { get; set; }
 
     /// <summary>
-    /// (下一节点办理人)根据审批者类型不同,此字段为不同内容
-    /// <example>
-    /// 部门等级/分类为:orgCodes, 角色为:userIds
-    /// </example>
+    /// 发起会签生成会签Id(不发起会签节点无此字段)
     /// </summary>
-    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
-    public List<string> NextHandlers { get; set; }
+    [SugarColumn(IsNullable = true)]
+    public string? StartCountersignId { get; set; }
 
     /// <summary>
-    /// 下一节点主办,(NextHandlers其中一个, 如果不是会签则只有一个)
+    /// 上级会签Id,(不支持发起会签节点无此字段)
     /// </summary>
-    public string NextMainHandler { get; set; }
+    [SugarColumn(IsNullable = true)]
+    public string? PrevCountersignId { get; set; }
 
     /// <summary>
-    /// 下一节点code
+    /// 汇总节点保存最顶级会签节点的会签Id
+    /// <remarks>
+    /// 1.如果当前会签流程前还有上级会签流程,当前汇总节点保存上级会签流程的会签Id
+    /// 2.如果从汇总节点直接发起会签,上级会签流程的会签Id应该继续往下传递,直到上级会签流程汇总
+    /// </remarks>
     /// </summary>
-    public string NextStepCode { get; set; }
+    [SugarColumn(IsNullable = true)]
+    public string? TopCountersignId { get; set; }
 
     /// <summary>
-    /// 是否短信通知
+    /// 当前节点发起的会签是否已结束(未发起会签的节点无此字段)
     /// </summary>
-    public bool AcceptSms { get; set; }
+    [SugarColumn(IsNullable = true)]
+    public bool? IsCountersignComplete { get; set; }
+
 
     /// <summary>
-    /// 办理意见
+    /// 是否处于会签流程中
     /// </summary>
-    [SugarColumn(Length = 2000)]
-    public string Opinion { get; set; }
+    public bool IsInCountersign => !string.IsNullOrEmpty(PrevCountersignId);
 
     /// <summary>
-    /// 附件
+    /// 是否发起会签
     /// </summary>
-    [SugarColumn(ColumnDataType = "varchar(1000)", IsJson = true)]
-    public List<string> Additions { get; set; }
+    public bool HasStartCountersign => !string.IsNullOrEmpty(StartCountersignId);
 
     #endregion
 
-    /// <summary>
-    /// stepBox此字段无效,记录stepBox与其下subSteps关系
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? ParentId { get; set; }
-
-    [SugarColumn(IsIgnore = true)]
-    public List<WorkflowStep> Steps { get; set; }
+    #region Method
 
     /// <summary>
     /// 接办
@@ -127,13 +105,13 @@ public class WorkflowStep : StepBasicEntity
     }
 
     /// <summary>
-    /// 办理完成
+    /// 节点办理完成
     /// </summary>
     /// <param name="userId"></param>
     /// <param name="userName"></param>
     /// <param name="orgCode"></param>
     /// <param name="orgName"></param>
-    public void Complete(string userId, string userName, string orgCode, string orgName, string nextStepCode)
+    public void StepComplete(string userId, string userName, string orgCode, string orgName, string nextStepCode)
     {
         UserId = userId;
         UserName = userName;
@@ -144,4 +122,34 @@ public class WorkflowStep : StepBasicEntity
 
         NextSteps.First(d => d.Code == nextStepCode).Selected = true;
     }
+
+    /// <summary>
+    /// 检查stepBox状态,如果子节点全都办理,则stepBox办理完成
+    /// </summary>
+    public void CheckStepBoxStatusAndUpdate()
+    {
+        if (Status is not EWorkflowStepStatus.Completed && Steps.All(d => d.Status is EWorkflowStepStatus.Completed))
+        {
+            Status = EWorkflowStepStatus.Completed;
+            CompleteTime = Steps.Max(d => d.CompleteTime);
+        }
+    }
+
+    /// <summary>
+    /// 会签结束
+    /// </summary>
+    public void CountersignComplete() => IsCountersignComplete = true;
+
+    /// <summary>
+    /// 开启会签
+    /// </summary>
+    public void StartCountersign() => StartCountersignId = Guid.NewGuid().ToString();
+
+    /// <summary>
+    /// step设置为可接办状态
+    /// </summary>
+    public void SetAssigned() => Status = EWorkflowStepStatus.Assigned;
+
+    #endregion
+
 }

+ 2 - 78
src/Hotline/FlowEngine/Workflows/WorkflowTrace.cs

@@ -9,95 +9,19 @@ namespace Hotline.FlowEngine.Workflows;
 /// </summary>
 public class WorkflowTrace : StepBasicEntity
 {
-    public string WorkflowId { get; set; }
 
     public string StepId { get; set; }
 
-    /// <summary>
-    /// 办理人部门code
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? OrgCode { get; set; }
-
-    [SugarColumn(IsNullable = true)]
-    public string? OrgName { get; set; }
-
-    /// <summary>
-    /// 办理人
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? UserId { get; set; }
-
-    [SugarColumn(IsNullable = true)]
-    public string? UserName { get; set; }
-
-    /// <summary>
-    /// 办理完成时间
-    /// </summary>
-    public DateTime? CompleteTime { get; set; }
-
-    /// <summary>
-    /// 接办人
-    /// </summary>
-    public string AcceptUserId { get; set; }
-
-    public string AcceptUserName { get; set; }
-
-    /// <summary>
-    /// 接办时间
-    /// </summary>
-    public DateTime AcceptTime { get; set; }
-
     /// <summary>
     /// 流转记录状态
     /// </summary>
     public EWorkflowTraceStatus Status { get; set; }
 
     /// <summary>
-    /// 过期时间
+    /// 过期时间(生成流转记录时取值当前workflow的过期时间)
     /// </summary>
     public DateTime ExpiredTime { get; set; }
-
-    #region 审批参数
-
-    /// <summary>
-    /// (下一节点办理人)根据审批者类型不同,此字段为不同内容
-    /// <example>
-    /// 部门等级/分类为:depCodes, 角色为:userIds
-    /// </example>
-    /// </summary>
-    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
-    public List<string> NextHandlers { get; set; }
-
-    /// <summary>
-    /// 下一节点主办,(NextHandlers其中一个, 如果不是会签则只有一个)
-    /// </summary>
-    public string NextMainHandler { get; set; }
-
-    /// <summary>
-    /// 下一节点code
-    /// </summary>
-    public string NextStepCode { get; set; }
-
-    /// <summary>
-    /// 是否短信通知
-    /// </summary>
-    public bool AcceptSms { get; set; }
-
-    /// <summary>
-    /// 办理意见
-    /// </summary>
-    [SugarColumn(Length = 2000)]
-    public string Opinion { get; set; }
-
-    /// <summary>
-    /// 附件
-    /// </summary>
-    [SugarColumn(ColumnDataType = "varchar(1000)", IsJson = true)]
-    public List<string> Additions { get; set; }
-
-    #endregion
-
+    
     /// <summary>
     /// 会签从属关系
     /// </summary>

+ 5 - 1
src/Hotline/Identity/Accounts/Account.cs

@@ -1,5 +1,6 @@
 using Hotline.Identity.Roles;
 using Hotline.Share.Enums.Identity;
+using Hotline.Users;
 using SqlSugar;
 using XF.Domain.Repository;
 
@@ -57,9 +58,12 @@ namespace Hotline.Identity.Accounts
 
         public bool PasswordChanged { get; set; }
 
-        public EAccountStatus Status{ get; set; }
+        public EAccountStatus Status { get; set; }
 
         [Navigate(typeof(AccountRole), nameof(AccountRole.AccountId), nameof(AccountRole.RoleId))]
         public List<Role> Roles { get; set; }
+
+        [Navigate(NavigateType.OneToOne, nameof(Id))]
+        public User User { get; set; }
     }
 }

+ 13 - 0
src/Hotline/Settings/IWorkflowBusinessRepository.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.Settings
+{
+    public interface IWorkflowBusinessRepository : IRepository<WorkflowModule>
+    {
+    }
+}

+ 12 - 1
src/Hotline/Settings/SystemOrganize.cs

@@ -2,13 +2,14 @@
 using SqlSugar;
 using System.ComponentModel;
 using XF.Domain.Entities;
+using XF.Domain.Exceptions;
 using XF.Domain.Repository;
 
 namespace Hotline.Settings;
 
 [SugarIndex("unique_org_code", nameof(SystemOrganize.OrgCode), OrderByType.Desc, true)]
 [Description("组织架构")]
-public class SystemOrganize: CreationEntity
+public class SystemOrganize : CreationEntity
 {
     /// <summary>
     /// 组织架构名称
@@ -20,6 +21,8 @@ public class SystemOrganize: CreationEntity
     /// </summary>
     public string OrgCode { get; set; }
 
+    public int OrgLevel { get; set; }
+
     /// <summary>
     /// 上级ID
     /// </summary>
@@ -37,4 +40,12 @@ public class SystemOrganize: CreationEntity
 
     [SugarColumn(IsIgnore = true)]
     public List<SystemOrganize> children { get; set; }
+
+
+    public void InitOrgLevel()
+    {
+        if (OrgCode.Length % 3 != 0)
+            throw new UserFriendlyException("非法部门Code");
+        OrgLevel = OrgCode.Length / 3;
+    }
 }

+ 20 - 0
src/Hotline/Settings/WorkflowModule.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.FlowEngine.Definitions;
+using XF.Domain.Repository;
+
+namespace Hotline.Settings;
+
+/// <summary>
+/// 工作流业务模块
+/// </summary>
+public class WorkflowModule : CreationEntity
+{
+    public string Name { get; set; }
+
+    public string DefinitionCode { get; set; }
+}