Selaa lähdekoodia

Merge branch 'tangj-knowledge' of http://git.fwt.com/Hotline/hotline into tangj-knowledge

TANG JIANG 2 vuotta sitten
vanhempi
commit
894235e0b1
92 muutettua tiedostoa jossa 1497 lisäystä ja 813 poistoa
  1. 3 11
      src/Hotline.Api/Context/DefaultSessionContext.cs
  2. 3 3
      src/Hotline.Api/Controllers/KnowledgeController.cs
  3. 1 0
      src/Hotline.Api/Controllers/OrgController.cs
  4. 8 2
      src/Hotline.Api/Controllers/PbxController.cs
  5. 11 10
      src/Hotline.Api/Controllers/SysController.cs
  6. 166 11
      src/Hotline.Api/Controllers/WorkflowController.cs
  7. 3 3
      src/Hotline.Application/FlowEngine/WorkflowApplication.cs
  8. 1 1
      src/Hotline.Application/Handlers/CallCenter/CallState/AlertExtToOuterNotificationHandler.cs
  9. 1 1
      src/Hotline.Application/Handlers/CallCenter/CallState/AlertMenuToOuterNotificationHandler.cs
  10. 1 1
      src/Hotline.Application/Handlers/CallCenter/CallState/AlertVisitorToExtNotificationHandler.cs
  11. 2 2
      src/Hotline.Application/Handlers/CallCenter/CallState/DtmfNotificationHandler.cs
  12. 1 1
      src/Hotline.Application/Handlers/CallCenter/CallState/RingExtToExtNotificationHandler.cs
  13. 2 2
      src/Hotline.Application/Handlers/CallCenter/CallState/RingExtToOuterNotificationHandler.cs
  14. 1 1
      src/Hotline.Application/Handlers/CallCenter/CallState/RingVisitorToExtNotificationHandler.cs
  15. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/AnswerExtToExtNoificationHandler.cs
  16. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/AnswerExtToOuterNotificationHandler.cs
  17. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/AnswerViisitorToExtNotificationHandler.cs
  18. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/AnsweredExtToExtNotificationHandler.cs
  19. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/AnsweredExtToOuterNotificationHandler.cs
  20. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/AnsweredExtToOuterToExtNotificationHandler.cs
  21. 7 5
      src/Hotline.Application/Handlers/CallCenter/FlowControl/AnsweredVisitorToExtNotificationHandler.cs
  22. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeExtAndExtNotificationHandler.cs
  23. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeExtAndOuterOneNotificationHandler.cs
  24. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeExtAndOuterTwoNotificationHandler.cs
  25. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeOuterAndOuterNotificationHandler.cs
  26. 11 6
      src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeVisitorAndExtNotificationHandler.cs
  27. 2 2
      src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeVisitorOffNotificationHandler.cs
  28. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/CdrNotificationHandler.cs
  29. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/DivertVisitorToExtNotificationHandler.cs
  30. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/EndOfAnnOuterToMenuNotificationHandler.cs
  31. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/EndOfAnnVisitorToMenuNotificationHandler.cs
  32. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/IncomingNotificationHandler.cs
  33. 1 1
      src/Hotline.Application/Handlers/CallCenter/FlowControl/QueueVisitorToGroupBusyNotificationHandler.cs
  34. 23 2
      src/Hotline.Application/Handlers/FlowEngine/NextStepHandler.cs
  35. 1 1
      src/Hotline.Application/Hotline.Application.csproj
  36. 13 0
      src/Hotline.Application/Identity/IdentityAppService.cs
  37. 1 0
      src/Hotline.NewRock/Handlers/DeviceEventHandler.cs
  38. 2 2
      src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionFilterBuilder.cs
  39. 1 1
      src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionManager.cs
  40. 1 1
      src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionScheme.cs
  41. 9 0
      src/Hotline.Repository.SqlSugar/Extensions/SqlSugarRepositoryExtensions.cs
  42. 2 1
      src/Hotline.Repository.SqlSugar/Orders/OrderRepository.cs
  43. 1 1
      src/Hotline.Repository.SqlSugar/System/SystemDataAuthorityRepository.cs
  44. 4 3
      src/Hotline.Repository.SqlSugar/System/SystemOrganizeRepository.cs
  45. 0 9
      src/Hotline.Repository.SqlSugar/System/TrunkIvrManagerRepository.cs
  46. 5 1
      src/Hotline.Share/Dtos/Dic/DicDto.cs
  47. 8 1
      src/Hotline.Share/Dtos/FlowEngine/DefinitionDto.cs
  48. 13 0
      src/Hotline.Share/Dtos/FlowEngine/NextStepOptions.cs
  49. 8 0
      src/Hotline.Share/Dtos/FlowEngine/QueryWorkflowPagedDto.cs
  50. 2 0
      src/Hotline.Share/Dtos/FlowEngine/StartWorkflowDto.cs
  51. 40 4
      src/Hotline.Share/Dtos/FlowEngine/WorkflowDto.cs
  52. 22 0
      src/Hotline.Share/Dtos/Order/OrderDto.cs
  53. 1 1
      src/Hotline.Share/Dtos/Roles/RoleAuthorityDto.cs
  54. 0 17
      src/Hotline.Share/Enums/EWorkflowBusinessModule.cs
  55. 3 3
      src/Hotline.Share/Enums/FlowEngine/EHandlerType.cs
  56. 6 0
      src/Hotline.Share/Enums/FlowEngine/EWorkflowStepStatus.cs
  57. 3 4
      src/Hotline.Share/Enums/Settings/EAuthorityType.cs
  58. 5 0
      src/Hotline.Share/Enums/Settings/EOrgType.cs
  59. 0 0
      src/Hotline.Share/Enums/Settings/EValueType.cs
  60. 0 4
      src/Hotline.Share/Hotline.Share.csproj
  61. 6 0
      src/Hotline.Share/Requests/PagedKeywordRequest.cs
  62. 0 5
      src/Hotline.Share/Requests/PagedRequest.cs
  63. 36 4
      src/Hotline/FlowEngine/Definitions/Definition.cs
  64. 7 2
      src/Hotline/FlowEngine/Definitions/DefinitionDomainService.cs
  65. 1 1
      src/Hotline/FlowEngine/Definitions/IDefinitionDomainService.cs
  66. 11 1
      src/Hotline/FlowEngine/Definitions/StepBasic.cs
  67. 17 1
      src/Hotline/FlowEngine/Definitions/StepDefine.cs
  68. 0 13
      src/Hotline/FlowEngine/Notifies/NextStepNotify.cs
  69. 18 0
      src/Hotline/FlowEngine/Notifies/WorkflowNotify.cs
  70. 0 28
      src/Hotline/FlowEngine/WorkflowBusiness.cs
  71. 15 18
      src/Hotline/FlowEngine/Workflows/IWorkflowDomainService.cs
  72. 89 4
      src/Hotline/FlowEngine/Workflows/StepBasicEntity.cs
  73. 58 15
      src/Hotline/FlowEngine/Workflows/Workflow.cs
  74. 292 181
      src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs
  75. 71 63
      src/Hotline/FlowEngine/Workflows/WorkflowStep.cs
  76. 2 78
      src/Hotline/FlowEngine/Workflows/WorkflowTrace.cs
  77. 5 1
      src/Hotline/Identity/Accounts/Account.cs
  78. 1 1
      src/Hotline/KnowledgeBase/IKnowledgeDomainService.cs
  79. 2 2
      src/Hotline/KnowledgeBase/KnowledgeDomainService.cs
  80. 11 12
      src/Hotline/Orders/Order.cs
  81. 4 233
      src/Hotline/Orders/OrderComplain.cs
  82. 2 2
      src/Hotline/Orders/OrderDomainService.cs
  83. 242 0
      src/Hotline/Orders/OrderExtensionEntity.cs
  84. 25 0
      src/Hotline/Orders/OrderReport.cs
  85. 1 1
      src/Hotline/Settings/ISystemDataAuthorityRepository.cs
  86. 0 1
      src/Hotline/Settings/ITrunkIvrManagerRepository.cs
  87. 11 1
      src/Hotline/Settings/SysDicData.cs
  88. 1 2
      src/Hotline/Settings/SystemDataAuthority.cs
  89. 21 2
      src/Hotline/Settings/SystemOrganize.cs
  90. 45 0
      src/Hotline/Settings/WorkflowModule.cs
  91. 66 6
      src/XF.Domain.Repository/Entity.cs
  92. 23 1
      src/XF.Domain/Entities/IDataPermission.cs

+ 3 - 11
src/Hotline.Api/Context/DefaultSessionContext.cs

@@ -26,17 +26,9 @@ namespace Hotline.Api.Token
             Phone = user.FindFirstValue(JwtClaimTypes.PhoneNumber);
             //Roles = user.Claims.Where(d => d.Type == JwtClaimTypes.Role).Select(d => d.Value).ToArray();
             Roles = user.Claims.Where(d => d.Type == ClaimTypes.Role).Select(d => d.Value).ToArray();
-
-            var dbUser = uow.Db.Queryable<User>()
-                .Includes(d => d.Organization)
-                .FirstAsync(d => d.Id == RequiredUserId)
-                .GetAwaiter().GetResult();
-            if (dbUser == null)
-                throw new UserFriendlyException($"无效用户编号:{RequiredUserId}", "未查询到用户");
-            UserName ??= dbUser.Name;
-            OrgId = dbUser.OrgId;
-            OrgCode = dbUser.OrgCode;
-            OrgName = dbUser.Organization.OrgName;
+            OrgId = user.FindFirstValue(AppClaimTypes.DepartmentId);
+            OrgCode = user.FindFirstValue(AppClaimTypes.DepartmentCode);
+            OrgName = user.FindFirstValue(AppClaimTypes.DepartmentName);
         }
 
         /// <summary>

+ 3 - 3
src/Hotline.Api/Controllers/KnowledgeController.cs

@@ -23,7 +23,7 @@ namespace Hotline.Api.Controllers
         private readonly IKnowledgeRepository _knowledgeRepository;
         private readonly IKnowledgeTempRepository _knowledgeTempRepository;
         private readonly ISessionContext _sessionContext;
-        private readonly IKnowledgeService _knowledgeService;
+        private readonly IKnowledgeDomainService _knowledgeService;
         private readonly IMapper _mapper;
 
         /// <summary>
@@ -32,10 +32,10 @@ namespace Hotline.Api.Controllers
         /// <param name="knowledgeRepository"></param>
         /// <param name="knowledgeTempRepository"></param>
         /// <param name="sessionContext"></param>
-        /// <param name="knowledgeService"></param>
+        /// <param name="knowledgeDomainService"></param>
         /// <param name="mapper"></param>
         public KnowledgeController(IKnowledgeRepository knowledgeRepository, IKnowledgeTempRepository knowledgeTempRepository,
-           ISessionContext sessionContext, IKnowledgeService knowledgeService, IMapper mapper)
+           ISessionContext sessionContext, IKnowledgeDomainService knowledgeService, IMapper mapper)
         {
             _knowledgeRepository = knowledgeRepository;
             _knowledgeTempRepository = knowledgeTempRepository;

+ 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);
         }
 

+ 8 - 2
src/Hotline.Api/Controllers/PbxController.cs

@@ -15,6 +15,7 @@ using Hotline.Users;
 using MapsterMapper;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.AspNetCore.Mvc;
+using SqlSugar;
 using XF.Domain.Authentications;
 using XF.Domain.Cache;
 using XF.Domain.Exceptions;
@@ -602,10 +603,15 @@ namespace Hotline.Api.Controllers
         /// </summary>
         /// <returns></returns>
         [Permission(EPermission.GetTrunkList)]
-        [HttpGet("turnk-list")]
+        [HttpGet("trunk-list")]
         public async Task<IReadOnlyList<TrunkIvrManager>> GetTrunkList()
         {
-            return await _trunkIvrManagerRepository.GetTrunkList();
+            return await _trunkIvrManagerRepository.Queryable()
+                    .Includes(d => d.WorkCategoryModel)
+                    .Includes(d => d.RestCategoryModel)
+                    .Includes(d => d.WorkToGroupModel.MappingField(z => z.No, () => d.WorkToGroup))
+                    .Includes(d => d.RestToGroupModel.MappingField(z => z.No, () => d.RestToGroup))
+                    .ToListAsync();
         }
 
         /// <summary>

+ 11 - 10
src/Hotline.Api/Controllers/SysController.cs

@@ -274,18 +274,19 @@ namespace Hotline.Api.Controllers
         /// <returns></returns>
         [Permission(EPermission.GetSysDicData)]
         [HttpGet("dictdata-type")]
-        public async Task<PagedDto<SysDicData>> GetSysDicData([FromQuery] GetSysDicDataDto dto)
+        public async Task<List<SysDicData>> GetSysDicData([FromQuery] GetSysDicDataDto dto)
         {
-            var (total, items) = await _sysDicDataRepository.QueryPagedAsync(
-                x=> true,
-                x=>x.OrderByDescending(d=>d.CreationTime),
-                dto.PageIndex,
-                dto.PageSize,
-                false,
-                whereIfs: (true, x => x.DicTypeId == dto.typeid)
-                );
+            //var (total, items) = await _sysDicDataRepository.QueryPagedAsync(
+            //    x=> true,
+            //    x=>x.OrderByDescending(d=>d.CreationTime),
+            //    dto.PageIndex,
+            //    dto.PageSize,
+            //    false,
+            //    whereIfs: (true, x => x.DicTypeId == dto.typeid)
+            //    );
+            return await _sysDicDataRepository.Queryable().Where(x => x.DicTypeId == dto.typeid).WhereIF(!string.IsNullOrEmpty(dto.datavalue),x=>x.DicDataValue.Contains(dto.datavalue)).ToTreeAsync(x=>x.children,x=>x.ParentId,"");
             //return await _sysDicDataRepository.Queryable().Where(x => x.DicTypeId == typeid).ToListAsync();
-            return new PagedDto<SysDicData> { Total = total, Items = items };
+            //return new PagedDto<SysDicData> { Total = total, Items = items };
         }
 
         /// <summary>

+ 166 - 11
src/Hotline.Api/Controllers/WorkflowController.cs

@@ -1,14 +1,17 @@
 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 +23,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;
     }
 
@@ -40,17 +61,26 @@ public class WorkflowController : BaseController
     [HttpGet("definition")]
     public async Task<PagedDto<DefinitionDto>> QueryDefinitions([FromQuery] PagedRequest dto)
     {
-        var result = await _definitionRepository.Queryable()
+        //todo 数据量大需重构
+        var query1 = await _definitionRepository.Queryable()
             .Where(d => d.Status == EDefinitionStatus.Temporary)
+            .ToListAsync();
+        var query2 = await _definitionRepository.Queryable()
+            .Select(d => new { i = SqlFunc.RowNumber($"{d.Version} desc", d.Code), d })
             .MergeTable()
-            .Where(d => d.Status != EDefinitionStatus.Temporary)
-            .GroupBy(d => d.Code)
-            .OrderByDescending(d => d.Version)
-            .Take(1)
-            .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
-
-        var items = _mapper.Map<IReadOnlyList<DefinitionDto>>(result.Items);
-        return new PagedDto<DefinitionDto>(result.Total, items);
+            .Where(d => d.i == 1)
+            .ToListAsync();
+
+        var query = query1.Union(query2.Select(d => d.d));
+        var total = query.Count();
+        var items = query
+            .OrderBy(d => d.Status)
+            .ThenByDescending(d => d.CreationTime)
+            .Skip(dto.Skip())
+            .Take(dto.PageSize)
+            .ToList();
+
+        return new PagedDto<DefinitionDto>(total, _mapper.Map<IReadOnlyList<DefinitionDto>>(items));
     }
 
     /// <summary>
@@ -163,4 +193,129 @@ 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));
+    }
+
+    /// <summary>
+    /// 查询当前流程下一节点参数
+    /// </summary>
+    /// <param name="workflowId"></param>
+    /// <returns></returns>
+    [HttpGet("nextsteps")]
+    public async Task<IReadOnlyList<NextStepOptions>> GetNextStepOptions(string workflowId)
+    {
+        var workflow = await _workflowDomainService.GetWorkflowAsync(workflowId, true, true, cancellationToken: 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:
+                    //当前操作人所属部门的下级部门并且属于配置orgLevel的部门
+                    var levels = nextStepDefine.HandlerClassifies.Select(d => int.Parse(d));
+                    var orgs1 = await _organizeRepository.QueryAsync(d =>
+                        d.IsEnable && d.OrgCode.StartsWith(_sessionContext.RequiredOrgCode) &&
+                        levels.Contains(d.OrgLevel));
+                    options.NextSteps = orgs1.Select(d => new KeyValuePair<string, string>(d.OrgCode, d.OrgName)).ToList();
+                    break;
+                case EHandlerType.OrgType:
+                    var types = nextStepDefine.HandlerClassifies.Select(d => Enum.Parse<EOrgType>(d));
+                    var org2 = await _organizeRepository.QueryAsync(d =>
+                        d.IsEnable && d.OrgCode.StartsWith(_sessionContext.RequiredOrgCode) &&
+                        types.Contains(d.OrgType));
+                    options.NextSteps = org2.Select(d => new KeyValuePair<string, string>(d.OrgCode, d.OrgName)).ToList();
+                    break;
+
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+
+            items.Add(options);
+        }
+
+        return items;
+    }
+
+    /// <summary>
+    /// 跳转至任意节点
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("jump")]
+    public async Task Jump([FromBody] RecallDto dto)
+    {
+        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, true, true, cancellationToken: HttpContext.RequestAborted);
+        await _workflowDomainService.JumpAsync(workflow, dto, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 终止流程
+    /// </summary>
+    /// <param name="workflowId"></param>
+    /// <returns></returns>
+    [HttpPut("terminate/{workflowId}")]
+    public async Task Terminate(string workflowId)
+    {
+        await _workflowDomainService.TerminateAsync(workflowId, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 获取跳转参数
+    /// </summary>
+    /// <param name="workflowId"></param>
+    /// <returns></returns>
+    [HttpGet("jump")]
+    public async Task<IReadOnlyList<StepDefineDto>> GetJumpOptions(string workflowId)
+    {
+        var workflow = await _workflowDomainService.GetWorkflowAsync(workflowId, true, true, cancellationToken: HttpContext.RequestAborted);
+        return _mapper.Map<IReadOnlyList<StepDefineDto>>(workflow.Definition.Steps);
+    }
+
+    /// <summary>
+    /// 获取撤回参数
+    /// </summary>
+    /// <param name="workflowId"></param>
+    /// <returns></returns>
+    [HttpGet("recall")]
+    public async Task<IReadOnlyList<StepDefineDto>> GetRecallOptions(string workflowId)
+    {
+        var workflow = await _workflowDomainService.GetWorkflowAsync(workflowId, true, true, cancellationToken: HttpContext.RequestAborted);
+        return _mapper.Map<IReadOnlyList<StepDefineDto>>(workflow.StepBoxes);
+    }
+
 }

+ 3 - 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;
@@ -41,9 +42,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, cancellationToken);
         await _workflowDomainService.StartAsync(workflow, dto, cancellationToken);
-
         return workflow.Id;
     }
 
@@ -55,7 +55,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     /// <returns></returns>
     public async Task NextAsync(NextWorkflowDto dto, CancellationToken cancellationToken)
     {
-        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, cancellationToken);
+        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.WorkflowId, true, true, cancellationToken: cancellationToken);
         var nextStepBoxDefine = _workflowDomainService.GetStepBoxDefine(workflow.Definition, dto.NextStepCode);
         var isOutOfCallCenter =
             await CheckIfFlowOutOfCallCenterAsync(nextStepBoxDefine, dto.NextMainHandler, cancellationToken);

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/CallState/AlertExtToOuterNotificationHandler.cs

@@ -25,7 +25,7 @@ namespace Hotline.Application.Handlers.CallCenter.CallState
             string telNo = notification.Outer.From != "" ? notification.Outer.From : notification.TelNo;
             if (!string.IsNullOrEmpty(telNo))
             {
-                var model =await _callRepository.GetAsync(x => x.ConversationId==notification.Outer.Id && x.ToNo==notification.Outer.To && x.Trunk==notification.Outer.Trunk && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+                var model =await _callRepository.GetAsync(x => x.ConversationId==notification.Outer.Id && x.ToNo==notification.Outer.To && x.Trunk==notification.Outer.Trunk && x.CreationTime>=DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
                 //如果存在
                 if (model!=null)
                 {

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/CallState/AlertMenuToOuterNotificationHandler.cs

@@ -20,7 +20,7 @@ namespace Hotline.Application.Handlers.CallCenter.CallState
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Outer.Id && x.ToNo == notification.Outer.To &&
-                     x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),
+                     x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime,
                 cancellationToken);
             if (model!=null)
             {

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/CallState/AlertVisitorToExtNotificationHandler.cs

@@ -20,7 +20,7 @@ namespace Hotline.Application.Handlers.CallCenter.CallState
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id &&
-                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model!=null)
             {
                 model.CallStatus = ECallStatus.Alert;

+ 2 - 2
src/Hotline.Application/Handlers/CallCenter/CallState/DtmfNotificationHandler.cs

@@ -31,12 +31,12 @@ namespace Hotline.Application.Handlers.CallCenter.CallState
             string info = string.Empty;
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id && x.FromNo == notification.Visitor.From &&
-                     x.ToNo == notification.Visitor.To, cancellationToken);
+                     x.ToNo == notification.Visitor.To,true,x=>x.CreationTime, cancellationToken);
             if (model == null)
             {
                 model = await _callRepository.GetAsync(
                     x => x.ConversationId == notification.Outer.Id && x.FromNo == notification.Outer.From &&
-                         x.ToNo == notification.Outer.To, cancellationToken);
+                         x.ToNo == notification.Outer.To,true,x=>x.CreationTime, cancellationToken);
                 isvis = false;
             }
             if (model != null)

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/CallState/RingExtToExtNotificationHandler.cs

@@ -30,7 +30,7 @@ namespace Hotline.Application.Handlers.CallCenter.CallState
 
         public async Task Handle(RingExtToExtNotification notification, CancellationToken cancellationToken)
         {
-            var model = await _callRepository.GetAsync(x=>x.FromNo == notification.FromTelNo && x.ToNo== notification.ToTelNo && x.CallStatus == Share.Enums.CallCenter.ECallStatus.Alert && x.CreationTime >= DateTime.Now.AddHours(-1), cancellationToken);
+            var model = await _callRepository.GetAsync(x=>x.FromNo == notification.FromTelNo && x.ToNo== notification.ToTelNo && x.CallStatus == Share.Enums.CallCenter.ECallStatus.Alert && x.CreationTime >= DateTime.Now.AddHours(-1),true,x=>x.CreationTime, cancellationToken);
             if (model!=null)
             {
                 model.CallStatus = Share.Enums.CallCenter.ECallStatus.Ring;

+ 2 - 2
src/Hotline.Application/Handlers/CallCenter/CallState/RingExtToOuterNotificationHandler.cs

@@ -22,12 +22,12 @@ namespace Hotline.Application.Handlers.CallCenter.CallState
         public async Task Handle(RingExtToOuterNotification notification, CancellationToken cancellationToken)
         {
             var model = await _callRepository.GetAsync(x =>
-                x.FromNo == notification.TelNo && x.ToNo == notification.Outer.To && x.CreationTime >=DateTime.Now.AddHours(-2), cancellationToken);
+                x.FromNo == notification.TelNo && x.ToNo == notification.Outer.To && x.CreationTime >=DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model==null)
             {
                 model = await _callRepository.GetAsync(
                     x => x.ConversationId == notification.Outer.Id && x.ToNo == notification.Outer.To &&
-                         x.Trunk == notification.Outer.Trunk, cancellationToken);
+                         x.Trunk == notification.Outer.Trunk,true,x=>x.CreationTime, cancellationToken);
             }
                        
             if (model!=null)

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/CallState/RingVisitorToExtNotificationHandler.cs

@@ -29,7 +29,7 @@ namespace Hotline.Application.Handlers.CallCenter.CallState
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id &&
-                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             var workModel = _userCacheManager.GetWorkByTel(notification.TelNo);
             if (model!=null)
             {

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/AnswerExtToExtNoificationHandler.cs

@@ -22,7 +22,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
 
         public async Task Handle(AnswerExtToExtNotification notification, CancellationToken cancellationToken)
         {
-            var model = await _callRepository.GetAsync(x=>x.FromNo == notification.FromTelNo && x.ToNo == notification.ToTelNo && x.CallStatus == Share.Enums.CallCenter.ECallStatus.Ring && x.CreationTime >= DateTime.Now.AddHours(-1),cancellationToken);
+            var model = await _callRepository.GetAsync(x=>x.FromNo == notification.FromTelNo && x.ToNo == notification.ToTelNo && x.CallStatus == Share.Enums.CallCenter.ECallStatus.Ring && x.CreationTime >= DateTime.Now.AddHours(-1),true,x=>x.CreationTime,cancellationToken);
 
             if (model!= null)
             {

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/AnswerExtToOuterNotificationHandler.cs

@@ -20,7 +20,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model =await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Outer.Id && x.ToNo == notification.Outer.To &&
-                     x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),
+                     x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime,
                 cancellationToken);
             if (model!=null)
             {

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/AnswerViisitorToExtNotificationHandler.cs

@@ -21,7 +21,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id &&
-                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model != null)
             {
                 model.CallStatus = ECallStatus.Answer;

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/AnsweredExtToExtNotificationHandler.cs

@@ -25,7 +25,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
 
         public async Task Handle(AnsweredExtToExtNotification notification, CancellationToken cancellationToken)
         {
-            var model = await _callRepository.GetAsync(x => x.FromNo == notification.FromTelNo && x.ToNo == notification.ToTelNo && x.CreationTime >= DateTime.Now.AddHours(-1), cancellationToken);
+            var model = await _callRepository.GetAsync(x => x.FromNo == notification.FromTelNo && x.ToNo == notification.ToTelNo && x.CreationTime >= DateTime.Now.AddHours(-1), true, x => x.CreationTime, cancellationToken);
             if (model!=null)
             {
                 model.CallStatus = Share.Enums.CallCenter.ECallStatus.Answered;

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/AnsweredExtToOuterNotificationHandler.cs

@@ -20,7 +20,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Outer.Id &&
-                     x.FromNo == notification.Outer.From && x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+                     x.FromNo == notification.Outer.From && x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model != null)
             {
                 model.CallStatus = ECallStatus.Answered;

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/AnsweredExtToOuterToExtNotificationHandler.cs

@@ -20,7 +20,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(x =>
                 x.ConversationId == notification.Outer.Id && x.ToNo == notification.Outer.To &&
-                x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),cancellationToken);
+                x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime,cancellationToken);
             if (model!=null)
             {
                 model.CallStatus = ECallStatus.Answered;

+ 7 - 5
src/Hotline.Application/Handlers/CallCenter/FlowControl/AnsweredVisitorToExtNotificationHandler.cs

@@ -26,7 +26,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id &&
-                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             var workModel = _userCacheManager.GetWorkByTel(notification.TelNo);
             if (model != null)
             {
@@ -49,11 +49,13 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
                 };
                 await _callDetailRepository.AddAsync(detail, cancellationToken);
 
-                //调用业务通知 通知前端
-                await _realtimeService.AnsweredAsync(workModel.UserId, new Share.Dtos.Realtime.AnsweredDto() { Id=model.Id,From = notification.Visitor.From,To =  notification.TelNo }, cancellationToken);
+                var callDetail = _callDetailRepository.GetAsync(x => x.CallId == model.Id && x.EventName == "ANSWER");
+                if (callDetail==null)
+                {
+                    //调用业务通知 通知前端
+                    await _realtimeService.AnsweredAsync(workModel.UserId, new Share.Dtos.Realtime.AnsweredDto() { Id = model.Id, From = notification.Visitor.From, To = notification.TelNo }, cancellationToken);
+                }
             }
-
-            
         }
     }
 }

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeExtAndExtNotificationHandler.cs

@@ -22,7 +22,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
 
         public async Task Handle(ByeExtAndExtNotification notification, CancellationToken cancellationToken)
         {
-            var model = await _callRepository.GetAsync(x => x.FromNo == notification.FromTelNo && x.ToNo == notification.ToTelNo && x.CallStatus == Share.Enums.CallCenter.ECallStatus.Answered && x.CreationTime >= DateTime.Now.AddHours(-1), cancellationToken);
+            var model = await _callRepository.GetAsync(x => x.FromNo == notification.FromTelNo && x.ToNo == notification.ToTelNo && x.CallStatus == Share.Enums.CallCenter.ECallStatus.Answered && x.CreationTime >= DateTime.Now.AddHours(-1),true,x=>x.CreationTime, cancellationToken);
             if (model!=null)
             {
                 model.CallStatus = Share.Enums.CallCenter.ECallStatus.Bye;

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeExtAndOuterOneNotificationHandler.cs

@@ -21,7 +21,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Outer.Id && 
-                     x.Trunk==notification.Outer.Trunk && x.ToNo == notification.Outer.To && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+                     x.Trunk==notification.Outer.Trunk && x.ToNo == notification.Outer.To && x.CreationTime>=DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model != null)
             {
                 model.CallStatus = ECallStatus.Bye;

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeExtAndOuterTwoNotificationHandler.cs

@@ -19,7 +19,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         public async Task Handle(ByeExtAndOuterTwoNotification notification, CancellationToken cancellationToken)
         {
             var model = await _callRepository.GetAsync(
-                x => x.ConversationId == notification.Outer.Id  && x.ToNo == notification.Outer.To && x.Trunk==notification.Outer.Trunk && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+                x => x.ConversationId == notification.Outer.Id  && x.ToNo == notification.Outer.To && x.Trunk==notification.Outer.Trunk && x.CreationTime>=DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model != null)
             {
                 model.CallStatus = ECallStatus.Bye;

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeOuterAndOuterNotificationHandler.cs

@@ -20,7 +20,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(x =>
                 x.ConversationId == notification.Outer.Id && x.FromNo == notification.Outer.From &&
-                x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2));
+                x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime,cancellationToken);
             if (model!=null)
             {
                 model.CallStatus = ECallStatus.Bye;

+ 11 - 6
src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeVisitorAndExtNotificationHandler.cs

@@ -28,8 +28,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id && 
-                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
-            var workModel = _userCacheManager.GetWorkByTel(notification.TelNo);
+                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model != null)
             {
                 model.CallStatus = ECallStatus.Bye;
@@ -54,10 +53,16 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
                 _callCacheManager.RemoveCallCache(model.Id);
 
                 //查询应答分机分机号
-                var callDetailAnswer = await _callDetailRepository.GetAsync(x => x.CallId == model.Id && x.EventName == "ANSWER", true, d => d.CreationTime);
-
-                //调用业务通知 通知前端
-                await _realtimeService.ByeAsync(workModel.UserId, new Share.Dtos.Realtime.ByeDto() { Id=model.Id }, cancellationToken);
+                var callDetailAnswer = await _callDetailRepository.GetAsync(x => x.CallId == model.Id && x.EventName == "ANSWER", true, d => d.CreationTime,cancellationToken);
+                if (callDetailAnswer!=null)
+                {
+                    var workModel = _userCacheManager.GetWorkByTel(callDetailAnswer.AnswerNo);
+                    if (workModel!=null)
+                    {
+                        //调用业务通知 通知前端
+                        await _realtimeService.ByeAsync(workModel.UserId, new Share.Dtos.Realtime.ByeDto() { Id = model.Id }, cancellationToken);
+                    }
+                }
             }
         }
     }

+ 2 - 2
src/Hotline.Application/Handlers/CallCenter/FlowControl/ByeVisitorOffNotificationHandler.cs

@@ -29,7 +29,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id &&
-                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model != null)
             {
                 model.CallStatus = ECallStatus.Bye;
@@ -54,7 +54,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
                 _callCacheManager.RemoveCallCache(model.Id);
 
                 //查询应答分机号
-                var callDetailAnswer = await _callDetailRepository.GetAsync(x => x.CallId == model.Id && x.EventName == "ANSWER", true, x => x.CreationTime);
+                var callDetailAnswer = await _callDetailRepository.GetAsync(x => x.CallId == model.Id && x.EventName == "ANSWER", true, x => x.CreationTime,cancellationToken);
                 if (callDetailAnswer!=null)
                 {
                     //调用业务通知 通知前端

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/CdrNotificationHandler.cs

@@ -60,7 +60,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
             else
             {
                 var callDetail = await
-                _callDetailRepository.GetAsync(x => x.OMCallId == notification.CallId, cancellationToken);
+                _callDetailRepository.GetAsync(x => x.OMCallId == notification.CallId,true,x=>x.CreationTime, cancellationToken);
 
                 if (callDetail != null)
                 {

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/DivertVisitorToExtNotificationHandler.cs

@@ -20,7 +20,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id && x.FromNo == notification.Visitor.From &&
-                     x.ToNo == notification.Visitor.To && x.CreationTime >= DateTime.Now.AddHours(-2),
+                     x.ToNo == notification.Visitor.To && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime,
                 cancellationToken);
             if (model!=null)
             {

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/EndOfAnnOuterToMenuNotificationHandler.cs

@@ -26,7 +26,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model =await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Outer.Id && x.FromNo == notification.Outer.From &&
-                     x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+                     x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model!=null)
             {
                 model.CallStatus = ECallStatus.EndOfAnn;

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/EndOfAnnVisitorToMenuNotificationHandler.cs

@@ -28,7 +28,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id && x.FromNo == notification.Visitor.From &&
-                     x.ToNo == notification.Visitor.To && x.CreationTime >= DateTime.Now.AddHours(-2),
+                     x.ToNo == notification.Visitor.To && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime,
                 cancellationToken);
 
             if (model!=null)

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/IncomingNotificationHandler.cs

@@ -64,7 +64,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                 x => x.ConversationId == notification.Visitor.Id && x.Trunk == notification.TrunkId &&
-                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model != null)
             {
                 model.CallStatus = ECallStatus.Incoming;

+ 1 - 1
src/Hotline.Application/Handlers/CallCenter/FlowControl/QueueVisitorToGroupBusyNotificationHandler.cs

@@ -22,7 +22,7 @@ namespace Hotline.Application.Handlers.CallCenter.FlowControl
         {
             var model = await _callRepository.GetAsync(
                x => x.ConversationId == notification.Visitor.Id &&
-                    x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+                    x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2),true,x=>x.CreationTime, cancellationToken);
             if (model!=null)
             {
                 model.CallStatus = Share.Enums.CallCenter.ECallStatus.InQueue;

+ 23 - 2
src/Hotline.Application/Handlers/FlowEngine/NextStepHandler.cs

@@ -1,16 +1,37 @@
 using Hotline.FlowEngine.Notifies;
+using Hotline.Orders;
+using Hotline.Settings;
 using MediatR;
+using XF.Domain.Exceptions;
 
 namespace Hotline.Application.Handlers.FlowEngine;
 
 public class NextStepHandler : INotificationHandler<NextStepNotify>
 {
+    private readonly IOrderRepository _orderRepository;
+
+    public NextStepHandler(IOrderRepository orderRepository)
+    {
+        _orderRepository = orderRepository;
+    }
+
     /// <summary>Handles a notification</summary>
     /// <param name="notification">The notification</param>
     /// <param name="cancellationToken">Cancellation token</param>
     public async Task Handle(NextStepNotify notification, CancellationToken cancellationToken)
     {
-        //todo 1.query order 2.add depcode to assignDepCodes, userId
-        throw new NotImplementedException();
+        var workflow = notification.Workflow;
+        var data = notification.Dto;
+        switch (workflow.ModuleCode)
+        {
+            case WorkflowModuleConsts.Order:
+                var order = await _orderRepository.GetAsync(d => d.WorkflowId == workflow.Id, cancellationToken);
+                if (order == null)
+                    throw new UserFriendlyException($"工单审批流程Id无效, workflowId: {workflow.Id}", "无效流程编号");
+                order.Assign(notification.FlowAssignType, data.Handlers);
+
+                //todo business logic
+                break;
+        }
     }
 }

+ 1 - 1
src/Hotline.Application/Hotline.Application.csproj

@@ -10,7 +10,7 @@
     <PackageReference Include="Conductor.Client" Version="1.0.9" />
     <PackageReference Include="Dapr.AspNetCore" Version="1.9.0" />
     <PackageReference Include="IdentityModel" Version="6.0.0" />
-    <PackageReference Include="XF.Utility.AppIdentityModel" Version="1.0.2" />
+    <PackageReference Include="XF.Utility.AppIdentityModel" Version="1.0.3" />
   </ItemGroup>
 
   <ItemGroup>

+ 13 - 0
src/Hotline.Application/Identity/IdentityAppService.cs

@@ -2,6 +2,7 @@
 using Hotline.Identity.Accounts;
 using Hotline.Share.Dtos.Identity;
 using Hotline.Share.Enums.Identity;
+using Hotline.Users;
 using IdentityModel;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.Extensions.Options;
@@ -17,17 +18,20 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
 {
     private readonly IAccountRepository _accountRepository;
     private readonly IAccountDomainService _accountDomainService;
+    private readonly IUserRepository _userRepository;
     private readonly IJwtSecurity _jwtSecurity;
     private readonly IOptionsSnapshot<IdentityConfiguration> _identityOptionsAccessor;
 
     public IdentityAppService(
         IAccountRepository accountRepository,
         IAccountDomainService accountDomainService,
+        IUserRepository userRepository,
         IJwtSecurity jwtSecurity,
         IOptionsSnapshot<IdentityConfiguration> identityOptionsAccessor)
     {
         _accountRepository = accountRepository;
         _accountDomainService = accountDomainService;
+        _userRepository = userRepository;
         _jwtSecurity = jwtSecurity;
         _identityOptionsAccessor = identityOptionsAccessor;
     }
@@ -61,6 +65,12 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
         account.AccessFailedCount = 0;
         await _accountRepository.UpdateAsync(account, cancellationToken);
 
+        var user = await _userRepository.Queryable()
+            .Includes(d => d.Organization)
+            .FirstAsync(d => d.Id == account.Id);
+        if (user == null)
+            throw UserFriendlyException.SameMessage("未查询到用户数据");
+
         var jwtOptions = _identityOptionsAccessor.Value.Jwt;
         var claims = new List<Claim>
         {
@@ -70,6 +80,9 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
             new(AppClaimTypes.UserDisplayName, account.Name),
             new(JwtClaimTypes.Scope,jwtOptions.Scope),
             new(AppClaimTypes.UserPasswordChanged, account.PasswordChanged.ToString()),
+            new(AppClaimTypes.DepartmentId, user.OrgId??string.Empty),
+            new(AppClaimTypes.DepartmentCode, user.OrgCode??string.Empty),
+            new(AppClaimTypes.DepartmentName, user.Organization?.OrgName??string.Empty),
         };
         claims.AddRange(account.Roles.Select(d => new Claim(JwtClaimTypes.Role, d.Name)));
         var token = _jwtSecurity.EncodeJwtToken(claims);

+ 1 - 0
src/Hotline.NewRock/Handlers/DeviceEventHandler.cs

@@ -208,6 +208,7 @@ namespace Hotline.NewRock.Handlers
                             await _mediator.Publish(_mapper.Map<ByeVisitorAndOuterNotification>(byeRcv.value!),
                                 cancellationToken);
                         }
+                        //分机和分机结束通话,挂断
                         else if(byeRcv.value?.Ext.Count==2)
                         {
                             await _mediator.Publish(_mapper.Map<ByeExtAndExtNotification>(byeRcv.value!), cancellationToken);

+ 2 - 2
src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionFilterBuilder.cs

@@ -1,4 +1,4 @@
-using Hotline.Share.Enums;
+using Hotline.Share.Enums.Settings;
 using System.Linq.Expressions;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
@@ -61,7 +61,7 @@ public class DataPermissionFilterBuilder : IDataPermissionFilterBuilder, IScopeD
     private static bool FlowDataFiltering<TEntity>(TEntity entity, string userId, string depCode) where TEntity : class, IEntity<string>, IDataPermission, IWorkflow, new()
     {
         if (entity.AssignUserIds.Contains(userId)) return true;
-        foreach (var assignDepCode in entity.AssignDepCodes)
+        foreach (var assignDepCode in entity.AssignOrgCodes)
         {
             if (assignDepCode == depCode) return true;
             var baseDep = assignDepCode.Substring(0, 3);

+ 1 - 1
src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionManager.cs

@@ -1,5 +1,5 @@
 using Hotline.Settings;
-using Hotline.Share.Enums;
+using Hotline.Share.Enums.Settings;
 using Hotline.Users;
 using Microsoft.Extensions.DependencyInjection;
 using XF.Domain.Authentications;

+ 1 - 1
src/Hotline.Repository.SqlSugar/DataPermissions/DataPermissionScheme.cs

@@ -1,4 +1,4 @@
-using Hotline.Share.Enums;
+using Hotline.Share.Enums.Settings;
 
 namespace Hotline.Repository.SqlSugar.DataPermissions;
 

+ 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/Orders/OrderRepository.cs

@@ -6,10 +6,11 @@ using System.Threading.Tasks;
 using Hotline.Orders;
 using Hotline.Repository.SqlSugar.DataPermissions;
 using SqlSugar;
+using XF.Domain.Dependency;
 
 namespace Hotline.Repository.SqlSugar.Orders
 {
-    public class OrderRepository : BaseRepositoryWorkflow<Order>, IOrderRepository
+    public class OrderRepository : BaseRepositoryWorkflow<Order>, IOrderRepository, IScopeDependency
     {
         public OrderRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
         {

+ 1 - 1
src/Hotline.Repository.SqlSugar/System/SystemDataAuthorityRepository.cs

@@ -1,6 +1,6 @@
 using Hotline.Repository.SqlSugar.DataPermissions;
 using Hotline.Settings;
-using Hotline.Share.Enums;
+using Hotline.Share.Enums.Settings;
 using SqlSugar;
 using XF.Domain.Dependency;
 

+ 4 - 3
src/Hotline.Repository.SqlSugar/System/SystemOrganizeRepository.cs

@@ -15,7 +15,7 @@ namespace Hotline.Repository.SqlSugar.System
         public async Task<IReadOnlyList<SystemOrganize>> GetOrgJson()
         {
             var list = await Db.Queryable<SystemOrganize>()
-                .ToTreeAsync(x => x.children, it => it.ParentId, "");
+                .ToTreeAsync(x => x.Children, it => it.ParentId, "");
             return list;
         }
 
@@ -23,7 +23,7 @@ namespace Hotline.Repository.SqlSugar.System
         {
             var list = await Db.Queryable<SystemOrganize>()
                 .Where(x => x.IsEnable)
-                .ToTreeAsync(x => x.children, it => it.ParentId, "");
+                .ToTreeAsync(x => x.Children, it => it.ParentId, "");
             return list;
         }
 
@@ -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) + 1).ToString("000");
             }
             //如果不存在下级
             else

+ 0 - 9
src/Hotline.Repository.SqlSugar/System/TrunkIvrManagerRepository.cs

@@ -17,14 +17,5 @@ namespace Hotline.Repository.SqlSugar.System
         }
 
 
-        public async Task<IReadOnlyList<TrunkIvrManager>> GetTrunkList()
-        {
-            return await Db.Queryable<TrunkIvrManager>()
-                    .Includes(d => d.WorkCategoryModel)
-                    .Includes(d => d.RestCategoryModel)
-                    //.Includes(d=> d.WorkToGroupModel.MappingField(z=>z.No,()=>d.WorkToGroup))
-                    .Includes(d=> d.RestToGroupModel.MappingField(z=>z.No,()=>d.RestToGroup))
-                    .ToListAsync();
-        }
     }
 }

+ 5 - 1
src/Hotline.Share/Dtos/Dic/DicDto.cs

@@ -12,14 +12,18 @@ namespace Hotline.Share.Dtos.Dic
         public string DicTypeId { get; set; }
         public string DicDataName { get; set; }
         public string DicDataValue { get; set; }
+
+        public string? ParentId { get; set; }
     }
     public record UpdateDicDataDto: AddDicDataDto
     {
         public string Id { get; set; }
     }
 
-    public record GetSysDicDataDto:PagedRequest
+    public record GetSysDicDataDto
     {
         public string typeid { get; set; }
+
+        public string datavalue { get; set; }
     }
 }

+ 8 - 1
src/Hotline.Share/Dtos/FlowEngine/DefinitionDto.cs

@@ -36,10 +36,17 @@ namespace Hotline.Share.Dtos.FlowEngine
         public string Description { get; set; }
 
         public List<StepDefineDto> Steps { get; set; }
+        
+        public string ExternalData { get; set; }
+
+        public string ModuleId { get; set; }
 
+        /// <summary>
+        /// 业务模块名称
+        /// </summary>
         public string ModuleName { get; set; }
 
-        public string ExternalData { get; set; }
+        public string ModuleCode { 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();
+    }
 }

+ 22 - 0
src/Hotline.Share/Dtos/Order/OrderDto.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Dtos.Order
+{
+    public class OrderDto : UpdateOrderDto
+    {
+        public string CreationTime { get; set; }
+    }
+
+    public class UpdateOrderDto : AddOrderDto
+    {
+        public string Id { get; set; }
+    }
+
+    public class AddOrderDto
+    {
+    }
+}

+ 1 - 1
src/Hotline.Share/Dtos/Roles/RoleAuthorityDto.cs

@@ -1,4 +1,4 @@
-using Hotline.Share.Enums;
+using Hotline.Share.Enums.Settings;
 
 namespace Hotline.Share.Dtos.Roles
 {

+ 0 - 17
src/Hotline.Share/Enums/EWorkflowBusinessModule.cs

@@ -1,17 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Hotline.Share.Enums;
-
-/// <summary>
-/// 工作流业务模块
-/// </summary>
-public enum EWorkflowBusinessModule
-{
-    [Display(Name = "工单办理")]
-    Order = 0,
-}

+ 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>

+ 3 - 4
src/Hotline.Share/Enums/EAuthorityType.cs → src/Hotline.Share/Enums/Settings/EAuthorityType.cs

@@ -1,7 +1,6 @@
-
-using System.ComponentModel;
+using System.ComponentModel;
 
-namespace Hotline.Share.Enums
+namespace Hotline.Share.Enums.Settings
 {
     public enum EAuthorityType
     {
@@ -19,7 +18,7 @@ namespace Hotline.Share.Enums
         /// 本部及以下
         /// </summary>
         [Description("本部及以下")]
-        OrgAndBelow =2,
+        OrgAndBelow = 2,
         /// <summary>
         /// 创建人
         /// </summary>

+ 5 - 0
src/Hotline.Share/Enums/Settings/EOrgType.cs

@@ -0,0 +1,5 @@
+namespace Hotline.Settings;
+
+public enum EOrgType
+{
+}

+ 0 - 0
src/Hotline/Settings/EValueType.cs → src/Hotline.Share/Enums/Settings/EValueType.cs


+ 0 - 4
src/Hotline.Share/Hotline.Share.csproj

@@ -11,8 +11,4 @@
     <PackageReference Include="XF.Utility.EnumExtensions" Version="1.0.2" />
   </ItemGroup>
 
-  <ItemGroup>
-    <Folder Include="Dtos\Order\" />
-  </ItemGroup>
-
 </Project>

+ 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; }
-    }
 }

+ 36 - 4
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;
@@ -12,7 +14,7 @@ public class Definition : CreationEntity
     public string Name { get; set; }
 
     /// <summary>
-    /// 模板编码(唯一索引)
+    /// 模板编码
     /// </summary>
     public string Code { get; set; }
 
@@ -26,10 +28,40 @@ 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; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? ModuleId { get; set; }
+
+    /// <summary>
+    /// 业务模块名称
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ModuleName { get; set; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? ModuleCode { 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; }
+
 }

+ 17 - 1
src/Hotline/FlowEngine/Definitions/StepDefine.cs

@@ -1,4 +1,7 @@
-namespace Hotline.FlowEngine.Definitions;
+using Hotline.Share.Enums.FlowEngine;
+using XF.Domain.Entities;
+
+namespace Hotline.FlowEngine.Definitions;
 
 /// <summary>
 /// 模板定义节点
@@ -6,4 +9,17 @@
 public class StepDefine : StepBasic
 {
     public List<NextStepDefine> NextSteps { get; set; }
+
+    public EFlowAssignType GFlowAssignType()
+    {
+        return HandlerType switch
+        {
+            EHandlerType.Role => EFlowAssignType.User,
+            EHandlerType.OrgLevel => EFlowAssignType.Org,
+            EHandlerType.OrgType => EFlowAssignType.Org,
+            EHandlerType.AssignUser => EFlowAssignType.User,
+            EHandlerType.AssignOrg => EFlowAssignType.Org,
+            _ => throw new ArgumentOutOfRangeException()
+        };
+    }
 }

+ 0 - 13
src/Hotline/FlowEngine/Notifies/NextStepNotify.cs

@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Hotline.Share.Dtos.FlowEngine;
-using MediatR;
-
-namespace Hotline.FlowEngine.Notifies
-{
-    public record NextStepNotify(string WorkflowId, BasicWorkflowDto Data) : INotification;
-    public record PreviousStepNotify(PreviousWorkflowDto Data) : INotification;
-}

+ 18 - 0
src/Hotline/FlowEngine/Notifies/WorkflowNotify.cs

@@ -0,0 +1,18 @@
+using Hotline.FlowEngine.Workflows;
+using Hotline.Share.Dtos.FlowEngine;
+using MediatR;
+using XF.Domain.Entities;
+
+namespace Hotline.FlowEngine.Notifies;
+
+public record WorkflowNotify(Workflow Workflow, EFlowAssignType FlowAssignType) : INotification;
+
+public record StartWorkflowNotify(Workflow Workflow, EFlowAssignType FlowAssignType, BasicWorkflowDto Dto) : WorkflowNotify(Workflow, FlowAssignType);
+
+public record NextStepNotify(Workflow Workflow, EFlowAssignType FlowAssignType, BasicWorkflowDto Dto) : WorkflowNotify(Workflow, FlowAssignType);
+
+public record AcceptWorkflowNotify(Workflow Workflow) : INotification;
+
+public record CountersignEndAssigned(Workflow Workflow) : INotification;
+
+public record PreviousStepNotify(PreviousWorkflowDto Data) : INotification;

+ 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 - 18
src/Hotline/FlowEngine/Workflows/IWorkflowDomainService.cs

@@ -5,30 +5,17 @@ namespace Hotline.FlowEngine.Workflows
 {
     public interface IWorkflowDomainService
     {
-        ///// <summary>
-        ///// 获取当前节点(针对某操作人的当前节点)
-        ///// </summary>
-        ///// <returns></returns>
-        //Task<(StepBox, Step)> GetStepAsync(string workflowId, string depCode, string userId, CancellationToken cancellationToken);
-        //(StepBox, Step) GetStep(List<StepBox> stepBoxes, string depCode, string userId);
-
-        /// <summary>
-        /// 获取可能前往的下一节点
-        /// </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, CancellationToken cancellationToken);
 
         /// <summary>
         /// 进行流程的开始节点
         /// </summary>
         Task StartAsync(Workflow workflow, BasicWorkflowDto dto, CancellationToken cancellationToken);
 
-        Task<Workflow> GetWorkflowAsync(string workflowId, CancellationToken cancellationToken);
+        /// <summary>
+        /// 查询工作流
+        /// </summary>
+        Task<Workflow> GetWorkflowAsync(string workflowId, bool withDefine = false, bool withSteps = false, bool withTraces = false, bool withSupplements = false, CancellationToken cancellationToken = default);
 
         /// <summary>
         /// 受理,接办
@@ -62,6 +49,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
+    
 }

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

@@ -9,6 +9,20 @@ public class Workflow : CreationEntity
 {
     public string DefinitionId { get; set; }
 
+    [SugarColumn(IsNullable = true)]
+    public string? ModuleId { get; set; }
+
+    /// <summary>
+    /// 业务模块名称
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ModuleName { get; set; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? ModuleCode { get; set; }
+
+    public string Title { get; set; }
+
     /// <summary>
     /// 到期时间
     /// </summary>
@@ -19,7 +33,7 @@ public class Workflow : CreationEntity
     public EWorkflowStatus Status { get; set; }
 
     /// <summary>
-    /// 当前节点名称(会签状态此字段保存开启会签时办理节点名称)
+    /// 当前节点名称(会签状态此字段保存最外层会签办理节点名称)
     /// </summary>
     public string CurrentStepName { get; set; }
 
@@ -29,9 +43,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 +58,7 @@ public class Workflow : CreationEntity
     public List<WorkflowSupplement> Supplements { get; set; }
 
     /// <summary>
-    /// 主节点,依据流转进度动态生成
+    /// 主节点,依据流转进度动态生成或删除
     /// </summary>
     [SugarColumn(IsIgnore = true)]
     public List<WorkflowStep> StepBoxes { get; set; }
@@ -52,12 +66,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 +91,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
 }

+ 292 - 181
src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs

@@ -42,31 +42,14 @@ namespace Hotline.FlowEngine.Workflows
             _mediator = mediator;
         }
 
-        /// <summary>
-        /// 根据操作人获取当前未完成节点
-        /// </summary>
-        /// <returns></returns>
-        public async Task<(WorkflowStep, WorkflowStep)> GetStepAsync(string workflowId, string orgCode, string userId, CancellationToken cancellationToken)
-        {
-            var workflow = await GetWorkflowAsync(workflowId, cancellationToken);
-            return GetUnCompleteStep(workflow.StepBoxes, orgCode, userId);
-        }
-
-        /// <summary>
-        /// 获取可能前往的下一节点
-        /// </summary>
-        /// <returns></returns>
-        public async Task<IReadOnlyList<NextStepDefine>> GetNextStepsAsync(QueryWorkflowDto dto, CancellationToken cancellationToken)
-        {
-            var (_, currentStep) = await GetStepAsync(dto.WorkflowId, dto.OrgCode, dto.UserId, cancellationToken);
-            //获取人工判定的所有下一节点
-            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, CancellationToken cancellationToken)
         {
             var workflow = new Workflow
             {
+                Title = title,
+                ModuleId = definition.ModuleId,
+                ModuleName = definition.ModuleName,
+                ModuleCode = definition.ModuleCode,
                 DefinitionId = definition.Id,
                 Status = EWorkflowStatus.Runnable,
                 ExpiredTime = GenerateExpiredTime(definition.Code),
@@ -89,39 +72,53 @@ 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);
+
+            //publish
+            await _mediator.Publish(new StartWorkflowNotify(workflow, nextStepBoxDefine.GFlowAssignType(), dto), cancellationToken);
         }
 
-        public async Task<Workflow> GetWorkflowAsync(string workflowId, CancellationToken cancellationToken)
+        public async Task<Workflow> GetWorkflowAsync(string workflowId,
+            bool withDefine = false, bool withSteps = false,
+            bool withTraces = false, bool withSupplements = false,
+            CancellationToken cancellationToken = default)
         {
-            var workflow = await _workflowRepository.Queryable()
-                .Includes(d => d.Definition)
-                .FirstAsync(d => d.Id == workflowId);
+            var query = _workflowRepository.Queryable();
+            if (withDefine)
+                query = query.Includes(d => d.Definition);
+            if (withSupplements)
+                query = query.Includes(d => d.Supplements);
+
+            var workflow = await query.FirstAsync(d => d.Id == workflowId);
             if (workflow is null)
                 throw new UserFriendlyException("无效workflowId");
 
-            var steps = await _workflowStepRepository.Queryable()
-                .Where(d => d.WorkflowId == workflowId)
-                .OrderBy(d => d.CreationTime)
-                .ToTreeAsync(d => d.Steps, d => d.ParentId, null);
-
-            //var traces = await _workflowTraceRepository.Queryable()
-            //    .Where(d => d.WorkflowId == workflowId)
-            //    .ToTreeAsync(d => d.Traces, d => d.ParentId, null);
+            if (withSteps)
+            {
+                var steps = await _workflowStepRepository.Queryable()
+                    .Where(d => d.WorkflowId == workflowId)
+                    .OrderBy(d => d.CreationTime)
+                    .ToTreeAsync(d => d.Steps, d => d.ParentId, null);
+                workflow.StepBoxes = steps;
+            }
 
-            workflow.StepBoxes = steps;
-            //workflow.Traces = traces;
+            if (withTraces)
+            {
+                var traces = await _workflowTraceRepository.Queryable()
+                    .Where(d => d.WorkflowId == workflowId)
+                    .ToTreeAsync(d => d.Traces, d => d.ParentId, null);
+                workflow.Traces = traces;
+            }
 
             return workflow;
         }
@@ -137,6 +134,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("当前流程已流转到最终步骤");
 
@@ -146,7 +153,7 @@ namespace Hotline.FlowEngine.Workflows
 
             await AcceptTraceAsync(workflow, currentStepBox, currentStep, cancellationToken);
 
-            //todo publish accept
+            await _mediator.Publish(new AcceptWorkflowNotify(workflow), cancellationToken);
         }
 
         /// <summary>
@@ -156,41 +163,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 +221,88 @@ 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);
 
-            //不在会签中的流程,更新当前节点名称、时间、会签节点code
-            if (!workflow.IsInCountersign())
-                SetWorkflowCurrentStepInfo(workflow, isCountersign, nextStepBox);
+            //下一节点为汇总节点时,检查下一节点是否可办理
+            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);
+                        await _mediator.Publish(new CountersignEndAssigned(workflow), cancellationToken);
+                    }
+
+                }
+                else
+                {
+                    await UpdateNextCountersignEndAssignedAsync(nextStepBox, currentStep, cancellationToken);
+                    await _mediator.Publish(new CountersignEndAssigned(workflow), cancellationToken);
+                }
+            }
+
+            //更新当前节点名称、时间、会签节点code 等字段
+            workflow.SetWorkflowCurrentStepInfo(isStartCountersign, nextStepBox);
 
             await _workflowRepository.UpdateAsync(workflow, cancellationToken);
 
-            //trace
+            #endregion
+
+            #region 流转记录
+
             await NextTraceAsync(workflow, dto, currentStep, cancellationToken);
 
-            await _mediator.Publish(new NextStepNotify(workflow.Id, dto), cancellationToken);
+            #endregion
+
+            await _mediator.Publish(new NextStepNotify(workflow, nextStepBoxDefine.GFlowAssignType(), dto), cancellationToken);
+        }
+
+        /// <summary>
+        /// 更新下级汇总节点可办理状态
+        /// </summary>
+        /// <param name="nextStepBox"></param>
+        /// <param name="currentStep"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        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);
         }
 
+
         /// <summary>
         /// 退回(返回前一节点)
         /// </summary>
@@ -269,19 +353,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 +392,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 +414,20 @@ 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);
+
+            //todo publish
+        }
+
         /// <summary>
         /// 根据stepCode查询流程配置中对应的节点
         /// </summary>
@@ -337,32 +435,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, _) = 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 +547,6 @@ namespace Hotline.FlowEngine.Workflows
             return parentTrace;
         }
 
-
-
         private async Task RecallAsync(Workflow workflow, RecallDto dto, WorkflowStep targetStepBox, CancellationToken cancellationToken)
         {
             //update uncompleted traces
@@ -456,7 +554,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 +566,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 +586,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.HandlerType is EHandlerType.AssignUser or EHandlerType.AssignDep)
+            if (stepBoxDefine.StepType is EStepType.CountersignEnd)
             {
-                var subSteps = CreateSubSteps(nextStepBox, nextStepBox.HandlerClassifies, dto.NextMainHandler, previousStepId);
-                nextStepBox.Steps.AddRange(subSteps);
+                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);
+                }
+            }
+
+            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(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 +717,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 +740,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; }
     }
 }

+ 1 - 1
src/Hotline/KnowledgeBase/IKnowledgeService.cs → src/Hotline/KnowledgeBase/IKnowledgeDomainService.cs

@@ -2,7 +2,7 @@
 
 namespace Hotline.KnowledgeBase
 {
-    public interface IKnowledgeService
+    public interface IKnowledgeDomainService
     {
         /// <summary>
         /// 根据用户部门ID获取当前用户所属部门及下级部门Id

+ 2 - 2
src/Hotline/KnowledgeBase/KnowledgeService.cs → src/Hotline/KnowledgeBase/KnowledgeDomainService.cs

@@ -7,7 +7,7 @@ using XF.Domain.Exceptions;
 
 namespace Hotline.KnowledgeBase
 {
-    public class KnowledgeService : IKnowledgeService, IScopeDependency
+    public class KnowledgeDomainService : IKnowledgeDomainService, IScopeDependency
     {
         private readonly IUserRepository _userRepository;
         private readonly IKnowledgeRepository _knowledgeRepository;
@@ -23,7 +23,7 @@ namespace Hotline.KnowledgeBase
         /// <param name="knowledgeTempRepository"></param>
         /// <param name="mapper"></param>
         /// <param name="knowledgePVRepository"></param>
-        public KnowledgeService(IUserRepository userRepository, IKnowledgeRepository knowledgeRepository, IKnowledgeTempRepository knowledgeTempRepository, IMapper mapper, IKnowledgePVRepository knowledgePVRepository)
+        public KnowledgeDomainService(IUserRepository userRepository, IKnowledgeRepository knowledgeRepository, IKnowledgeTempRepository knowledgeTempRepository, IMapper mapper, IKnowledgePVRepository knowledgePVRepository)
         {
             _userRepository = userRepository;
             _knowledgeRepository = knowledgeRepository;

+ 11 - 12
src/Hotline/Orders/Order.cs

@@ -157,8 +157,17 @@ namespace Hotline.Orders
         public string Content { get; set; }
 
         #endregion
+        
 
-        public string CurrentStep { get; set; }
+        /// <summary>
+        /// 当前节点名称
+        /// </summary>
+        public string CurrentStepName { get; set; }
+
+        /// <summary>
+        /// 到达当前节点时间
+        /// </summary>
+        public DateTime CurrentStepTime { get; set; }
 
         /// <summary>
         /// 工单状态
@@ -173,7 +182,7 @@ namespace Hotline.Orders
         /// <summary>
         /// 过期时间
         /// </summary>
-        public DateTime? EndTime { get; set; }
+        public DateTime? ExpiredTime { get; set; }
     }
 
     /// <summary>
@@ -186,15 +195,5 @@ namespace Hotline.Orders
         /// </summary>
         [Navigate(NavigateType.OneToOne, nameof(Id))]
         public OrderComplain OrderComplain { get; set; }
-
-
-    }
-
-    /// <summary>
-    /// 工单扩展信息 12315-举报
-    /// </summary>
-    public class OrderExt1
-    {
-
     }
 }

+ 4 - 233
src/Hotline/Orders/OrderComplain.cs

@@ -6,143 +6,15 @@ namespace Hotline.Orders;
 /// <summary>
 /// 工单扩展信息(12315-投诉)
 /// </summary>
-public class OrderComplain : FullStateEntity
+public class OrderComplain : OrderExtensionEntity
 {
-    #region 投诉人信息
-
-    /// <summary>
-    /// 证件类型
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? LicenceTypeCode { get; set; }
-    [SugarColumn(IsNullable = true)]
-    public string? LicenceType { get; set; }
-
-    /// <summary>
-    /// 证件号码, 证件类型已选的情况为必填,否则非必填
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? LicenceNo { get; set; }
-
-    /// <summary>
-    /// 提供方类型(投诉人类型)
-    /// </summary>
-    public EProviderType? ProviderType { get; set; }
-
-    /// <summary>
-    /// 提供方身份(投诉人身份)
-    /// </summary>
-    public ECitizenStatus1? ProviderStatus { get; set; }
-
-    /// <summary>
-    /// 国籍或地区
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public ENationality? Nationality { get; set; }
-
-    /// <summary>
-    /// 民族
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? NationCode { get; set; }
-    [SugarColumn(IsNullable = true)]
-    public string? Nation { get; set; }
-
-    /// <summary>
-    /// 邮政编码
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? PostalCode { get; set; }
-
-    /// <summary>
-    /// 邮箱
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? Email { get; set; }
-
-    /// <summary>
-    /// 其他联系方式
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? OtherContact { get; set; }
-
-    #endregion
-
-    #region 投诉对象信息
-
-    /// <summary>
-    /// 企业名称
-    /// </summary>
-    public string EnterpriseName { get; set; }
-
-    /// <summary>
-    /// 统一社会信用代码
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? UnifiedSocialCreditCode { get; set; }
-
-    /// <summary>
-    /// 注册地址
-    /// </summary>
-    public string RegisterAddress { get; set; }
-
-    /// <summary>
-    /// 注册号
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? RegisterNumber { get; set; }
-
-    /// <summary>
-    /// 企业联系人
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? EnterpriseContact { get; set; }
-
-    /// <summary>
-    /// 市场主体类型
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? MarketTypeCode { get; set; }
-    [SugarColumn(IsNullable = true)]
-    public string? MarketType { get; set; }
-
-    /// <summary>
-    /// 行业分类
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? IndustryClassifyCode { get; set; }
-    [SugarColumn(IsNullable = true)]
-    public string? IndustryClassify { get; set; }
-
-    #endregion
-
     #region 投诉详情
 
-    /// <summary>
-    /// 商品品牌编码
-    /// </summary>
-    public string BrandCode { get; set; }
-
-    /// <summary>
-    /// 商品分类/品牌
-    /// </summary>
-    public string Brand { get; set; }
-
-    /// <summary>
-    /// 消费金额,只能填写数字,且只能填写非负数
-    /// </summary>
-    public decimal Amount { get; set; }
-
-    /// <summary>
-    /// 客体类别
-    /// </summary>
-    public string ObjectClassifyCode { get; set; }
-    public string ObjectClassify { get; set; }
-
     /// <summary>
     /// 投诉问题类别
     /// </summary>
     public string ComplainClassifyCode { get; set; }
+
     public string ComplainClassify { get; set; }
 
     /// <summary>
@@ -150,29 +22,10 @@ public class OrderComplain : FullStateEntity
     /// </summary>
     public DateTime OccurrenceTime { get; set; }
 
-    /// <summary>
-    /// 销售方式
-    /// </summary>
-    public ESalesMode SalesMode { get; set; }
-
     /// <summary>
     /// 投诉目标,销售方式为“网购”时展示该字段且必填
     /// </summary>
-    public EComplainTarget? ComplainTarget { get; set; }
-
-    /// <summary>
-    /// 电商平台,销售方式为“网购”时展示该字段且必填
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? ECommercePlatformCode { get; set; }
-    [SugarColumn(IsNullable = true)]
-    public string? ECommercePlatform { get; set; }
-
-    /// <summary>
-    /// 外部订单号,销售方式为“ 网购”时展示该字段且必填
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? ExternalOrderNo { get; set; }
+    public EOrderTarget? ComplainTarget { get; set; }
 
     /// <summary>
     /// 经营地址,销售方式为“现场”时必填
@@ -183,88 +36,6 @@ public class OrderComplain : FullStateEntity
     [SugarColumn(IsNullable = true)]
     public string? BussinessAddress { get; set; }
 
-    /// <summary>
-    /// 具体渠道
-    /// <remarks>
-    /// 销售方式为“电视购物”“电话购物”“邮购”时展示该字段,且必填
-    /// 电视购物:请填写购物的电视频道
-    /// 电话购物:请填写商品销售者的热线号码
-    /// 邮购:请填写宣传商品的邮政公司或来件地址
-    /// </remarks>
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? Channel { get; set; }
-
-    /// <summary>
-    /// 专利权人,“投诉问题类别”为“专利”时展示该字段,选填
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? Patentee { get; set; }
-
-    /// <summary>
-    /// 专利名称,“投诉问题类别”为“专利”时展示该字段,选填
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? PatentName { get; set; }
-
-    /// <summary>
-    /// 专利类型,“投诉问题类别”为“专利”时展示该字段,选填
-    /// </summary>
-    public EPatentType? PatentType { get; set; }
-
-    /// <summary>
-    /// 专利号,“投诉问题类别”为“专利”时展示该字段,选填
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? PatentNo { get; set; }
-
-    /// <summary>
-    /// 产品名称
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? ProductName { get; set; }
-
-    /// <summary>
-    /// 批准文号
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? ApprovalNumber { get; set; }
-
-    /// <summary>
-    /// 产品批号
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? ProductBatchNo { get; set; }
-
-    /// <summary>
-    /// 产品规格
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? ProductStandard { get; set; }
-
-    /// <summary>
-    /// 产品有效期
-    /// </summary>
-    public DateTime ProductExpriedTime { get; set; }
-
-    /// <summary>
-    /// 生产厂家
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? Manufacturer { get; set; }
-
-    /// <summary>
-    /// 销售企业
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? SalesEnterprise { get; set; }
-
-    /// <summary>
-    /// 消费者地址
-    /// </summary>
-    [SugarColumn(IsNullable = true)]
-    public string? ConsumerAddress { get; set; }
-
     /// <summary>
     /// 诉求类型,多选
     /// </summary>
@@ -299,7 +70,7 @@ public enum EProviderType
     //生产企业、销售企业、服务企业、其他企业、个体工商户、自然人、群诉、其他类型
 }
 
-public enum EComplainTarget
+public enum EOrderTarget
 {
     //平台、入驻商家
 }

+ 2 - 2
src/Hotline/Orders/OrderDomainService.cs

@@ -23,12 +23,12 @@ public class OrderDomainService : IOrderDomainService
         if (!string.IsNullOrEmpty(order.Contact))
             order.ContactMask = order.Contact.MaskPhoneNumber();
 
-        order.No = GetNewOrderNo();
+        order.No = GenerateNewOrderNo();
 
         return await _orderRepository.AddAsync(order, cancellationToken);
     }
 
-    private string GetNewOrderNo()
+    private string GenerateNewOrderNo()
     {
         var today = DateTime.Today;
         var cacheKey = $"{OrderNoPrefix}{today:yyyyMMdd}";

+ 242 - 0
src/Hotline/Orders/OrderExtensionEntity.cs

@@ -0,0 +1,242 @@
+using SqlSugar;
+using XF.Domain.Repository;
+
+namespace Hotline.Orders;
+
+public class OrderExtensionEntity : FullStateEntity
+{
+    #region 投诉人信息
+
+    /// <summary>
+    /// 证件类型
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? LicenceTypeCode { get; set; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? LicenceType { get; set; }
+
+    /// <summary>
+    /// 证件号码, 证件类型已选的情况为必填,否则非必填
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? LicenceNo { get; set; }
+
+    /// <summary>
+    /// 提供方类型(投诉人类型)
+    /// </summary>
+    public EProviderType? ProviderType { get; set; }
+
+    /// <summary>
+    /// 提供方身份(投诉人身份)
+    /// </summary>
+    public ECitizenStatus1? ProviderStatus { get; set; }
+
+    /// <summary>
+    /// 国籍或地区
+    /// </summary>
+    public ENationality? Nationality { get; set; }
+
+    /// <summary>
+    /// 民族
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? NationCode { get; set; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? Nation { get; set; }
+
+    /// <summary>
+    /// 邮政编码
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? PostalCode { get; set; }
+
+    /// <summary>
+    /// 邮箱
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? Email { get; set; }
+
+    /// <summary>
+    /// 其他联系方式
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? OtherContact { get; set; }
+
+    #endregion
+
+    #region 投诉对象信息
+
+    /// <summary>
+    /// 企业名称
+    /// </summary>
+    public string EnterpriseName { get; set; }
+
+    /// <summary>
+    /// 统一社会信用代码
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? UnifiedSocialCreditCode { get; set; }
+
+    /// <summary>
+    /// 注册地址
+    /// </summary>
+    public string RegisterAddress { get; set; }
+
+    /// <summary>
+    /// 注册号
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? RegisterNumber { get; set; }
+
+    /// <summary>
+    /// 企业联系人
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? EnterpriseContact { get; set; }
+
+    /// <summary>
+    /// 市场主体类型
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? MarketTypeCode { get; set; }
+
+    [SugarColumn(IsNullable = true)]
+    public string? MarketType { get; set; }
+
+    /// <summary>
+    /// 行业分类
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? IndustryClassifyCode { get; set; }
+    [SugarColumn(IsNullable = true)]
+    public string? IndustryClassify { get; set; }
+
+    #endregion
+
+    #region 投诉/举报 详情
+
+
+    /// <summary>
+    /// 商品分类/品牌
+    /// </summary>
+    public string BrandCode { get; set; }
+
+    public string Brand { get; set; }
+
+    /// <summary>
+    /// 消费金额,只能填写数字,且只能填写非负数
+    /// </summary>
+    public decimal Amount { get; set; }
+
+    /// <summary>
+    /// 客体类别
+    /// </summary>
+    public string ObjectClassifyCode { get; set; }
+
+    public string ObjectClassify { get; set; }
+
+    /// <summary>
+    /// 销售方式
+    /// </summary>
+    public ESalesMode SalesMode { get; set; }
+
+    /// <summary>
+    /// 电商平台,销售方式为“网购”时展示该字段且必填
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ECommercePlatformCode { get; set; }
+    [SugarColumn(IsNullable = true)]
+    public string? ECommercePlatform { get; set; }
+
+    /// <summary>
+    /// 外部订单号,销售方式为“ 网购”时展示该字段且必填
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ExternalOrderNo { get; set; }
+
+    /// <summary>
+    /// 具体渠道
+    /// <remarks>
+    /// 销售方式为“电视购物”“电话购物”“邮购”时展示该字段,且必填
+    /// 电视购物:请填写购物的电视频道
+    /// 电话购物:请填写商品销售者的热线号码
+    /// 邮购:请填写宣传商品的邮政公司或来件地址
+    /// </remarks>
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? Channel { get; set; }
+
+    /// <summary>
+    /// 专利权人,“投诉问题类别”为“专利”时展示该字段,选填
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? Patentee { get; set; }
+
+    /// <summary>
+    /// 专利名称,“投诉问题类别”为“专利”时展示该字段,选填
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? PatentName { get; set; }
+
+    /// <summary>
+    /// 专利类型,“投诉问题类别”为“专利”时展示该字段,选填
+    /// </summary>
+    public EPatentType? PatentType { get; set; }
+
+    /// <summary>
+    /// 专利号,“投诉问题类别”为“专利”时展示该字段,选填
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? PatentNo { get; set; }
+
+    /// <summary>
+    /// 产品名称
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ProductName { get; set; }
+
+    /// <summary>
+    /// 批准文号
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ApprovalNumber { get; set; }
+
+    /// <summary>
+    /// 产品批号
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ProductBatchNo { get; set; }
+
+    /// <summary>
+    /// 产品规格
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ProductStandard { get; set; }
+
+    /// <summary>
+    /// 产品有效期
+    /// </summary>
+    public DateTime ProductExpriedTime { get; set; }
+
+    /// <summary>
+    /// 生产厂家
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? Manufacturer { get; set; }
+
+    /// <summary>
+    /// 销售企业
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? SalesEnterprise { get; set; }
+
+    /// <summary>
+    /// 消费者地址
+    /// </summary>
+    [SugarColumn(IsNullable = true)]
+    public string? ConsumerAddress { get; set; }
+
+    #endregion
+}

+ 25 - 0
src/Hotline/Orders/OrderReport.cs

@@ -0,0 +1,25 @@
+using SqlSugar;
+
+namespace Hotline.Orders;
+
+/// <summary>
+/// 工单扩展信息 12315-举报
+/// </summary>
+public class OrderReport : OrderExtensionEntity
+{
+    #region 举报详情
+
+    /// <summary>
+    /// 举报问题类别
+    /// </summary>
+    public string ReportClassifyCode { get; set; }
+
+    public string ReportClassify { get; set; }
+
+    /// <summary>
+    /// 举报目标,销售方式为“网购”时展示该字段且必填
+    /// </summary>
+    public EOrderTarget? ReportTarget { get; set; }
+
+    #endregion
+}

+ 1 - 1
src/Hotline/Settings/ISystemDataAuthorityRepository.cs

@@ -1,4 +1,4 @@
-using Hotline.Share.Enums;
+using Hotline.Share.Enums.Settings;
 using XF.Domain.Repository;
 
 namespace Hotline.Settings

+ 0 - 1
src/Hotline/Settings/ITrunkIvrManagerRepository.cs

@@ -4,6 +4,5 @@ namespace Hotline.Settings
 {
     public interface ITrunkIvrManagerRepository: IRepository<TrunkIvrManager>
     {
-        Task<IReadOnlyList<TrunkIvrManager>> GetTrunkList();
     }
 }

+ 11 - 1
src/Hotline/Settings/SysDicData.cs

@@ -1,4 +1,5 @@
-using System.ComponentModel;
+using SqlSugar;
+using System.ComponentModel;
 using XF.Domain.Repository;
 
 namespace Hotline.Settings
@@ -25,5 +26,14 @@ namespace Hotline.Settings
         /// 字典值
         /// </summary>
         public string DicDataValue { get; set; }
+
+        /// <summary>
+        /// 上级ID
+        /// </summary>
+        [SugarColumn(IsNullable = true)]
+        public string? ParentId { get; set; }
+
+        [SugarColumn(IsIgnore = true)]
+        public List<SysDicData> children { get; set; }
     }
 }

+ 1 - 2
src/Hotline/Settings/SystemDataAuthority.cs

@@ -1,5 +1,4 @@
-
-using Hotline.Share.Enums;
+using Hotline.Share.Enums.Settings;
 using System.ComponentModel;
 using XF.Domain.Entities;
 using XF.Domain.Repository;

+ 21 - 2
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,16 @@ public class SystemOrganize: CreationEntity
     /// </summary>
     public string OrgCode { get; set; }
 
+    /// <summary>
+    /// 部门级别
+    /// </summary>
+    public int OrgLevel { get; set; }
+
+    /// <summary>
+    /// 部门类型
+    /// </summary>
+    public EOrgType OrgType { get; set; }
+
     /// <summary>
     /// 上级ID
     /// </summary>
@@ -36,5 +47,13 @@ public class SystemOrganize: CreationEntity
     public bool IsEnable { get; set; }
 
     [SugarColumn(IsIgnore = true)]
-    public List<SystemOrganize> children { get; set; }
+    public List<SystemOrganize> Children { get; set; }
+
+
+    public void InitOrgLevel()
+    {
+        if (OrgCode.Length % 3 != 0)
+            throw new UserFriendlyException("非法部门Code");
+        OrgLevel = OrgCode.Length / 3;
+    }
 }

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

@@ -0,0 +1,45 @@
+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 SqlSugar;
+using XF.Domain.Repository;
+
+namespace Hotline.Settings;
+
+/// <summary>
+/// 工作流业务模块
+/// </summary>
+//[SugarIndex("unique_account_username", nameof(WorkflowModule.Code), OrderByType.Asc, true)]
+//public class WorkflowModule : CreationEntity
+//{
+//    public string Name { get; set; }
+
+//    public string Code { get; set; }
+
+//    public string DefinitionCode { get; set; }
+//}
+
+public class WorkflowModule
+{
+    public WorkflowModule()
+    {
+        Modules = new Dictionary<string, string>
+        {
+            { WorkflowModuleConsts.Order, "工单审批" },
+        };
+    }
+
+    public Dictionary<string, string> Modules { get; init; }
+}
+
+public class WorkflowModuleConsts
+{
+    /// <summary>
+    /// 工单审批
+    /// </summary>
+    public const string Order = "Order";
+}

+ 66 - 6
src/XF.Domain.Repository/Entity.cs

@@ -139,11 +139,41 @@ public abstract class WorkflowEntity : FullStateEntity, IWorkflow
     [SugarColumn(IsNullable = true)]
     public string? WorkflowId { get; set; }
 
-    [SugarColumn(ColumnDataType = "varchar(4000)", IsJson = true)]
-    public List<string> AssignDepCodes { get; set; } = new();
+    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
+    public List<string> AssignOrgCodes { get; set; } = new();
 
-    [SugarColumn(ColumnDataType = "varchar(4000)", IsJson = true)]
+    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
     public List<string> AssignUserIds { get; set; } = new();
+
+    public void Assign(EFlowAssignType type, string id)
+    {
+        switch (type)
+        {
+            case EFlowAssignType.Org:
+                AssignOrgCodes.Add(id);
+                break;
+            case EFlowAssignType.User:
+                AssignUserIds.Add(id);
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(type), type, null);
+        }
+    }
+
+    public void Assign(EFlowAssignType type, IEnumerable<string> ids)
+    {
+        switch (type)
+        {
+            case EFlowAssignType.Org:
+                AssignOrgCodes.AddRange(ids);
+                break;
+            case EFlowAssignType.User:
+                AssignUserIds.AddRange(ids);
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(type), type, null);
+        }
+    }
 }
 
 public abstract class PositionEntity : FullStateEntity
@@ -187,9 +217,39 @@ public abstract class PositionWorkflowEntity : PositionEntity, IWorkflow
     [SugarColumn(IsNullable = true)]
     public string? WorkflowId { get; set; }
 
-    [SugarColumn(ColumnDataType = "varchar(3000)", IsJson = true)]
-    public List<string> AssignDepCodes { get; set; } = new();
+    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
+    public List<string> AssignOrgCodes { get; set; } = new();
 
-    [SugarColumn(ColumnDataType = "varchar(3000)", IsJson = true)]
+    [SugarColumn(ColumnDataType = "varchar(2000)", IsJson = true)]
     public List<string> AssignUserIds { get; set; } = new();
+
+    public void Assign(EFlowAssignType type, string id)
+    {
+        switch (type)
+        {
+            case EFlowAssignType.Org:
+                AssignOrgCodes.Add(id);
+                break;
+            case EFlowAssignType.User:
+                AssignUserIds.Add(id);
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(type), type, null);
+        }
+    }
+
+    public void Assign(EFlowAssignType type, IEnumerable<string> ids)
+    {
+        switch (type)
+        {
+            case EFlowAssignType.Org:
+                AssignOrgCodes.AddRange(ids);
+                break;
+            case EFlowAssignType.User:
+                AssignUserIds.AddRange(ids);
+                break;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(type), type, null);
+        }
+    }
 }

+ 23 - 1
src/XF.Domain/Entities/IDataPermission.cs

@@ -17,10 +17,32 @@ namespace XF.Domain.Entities
     public interface IWorkflow
     {
         string? WorkflowId { get; set; }
+
         /// <summary>
         /// 指派部门编码
         /// </summary>
-        List<string> AssignDepCodes { get; set; }
+        List<string> AssignOrgCodes { get; set; }
+
+        /// <summary>
+        /// 指派用户Id
+        /// </summary>
         List<string> AssignUserIds { get; set; }
+
+        void Assign(EFlowAssignType type, string id);
+
+        void Assign(EFlowAssignType type, IEnumerable<string> ids);
+    }
+
+    public enum EFlowAssignType
+    {
+        /// <summary>
+        /// 指派到部门
+        /// </summary>
+        Org = 0,
+
+        /// <summary>
+        /// 指派到用户
+        /// </summary>
+        User = 1,
     }
 }