Browse Source

Merge branch 'test' of http://110.188.24.182:10023/Fengwo/hotline into test

tangjiang 2 tuần trước cách đây
mục cha
commit
6aedf07bc5
38 tập tin đã thay đổi với 1275 bổ sung192 xóa
  1. 2 1
      src/Hotline.Api/Controllers/Bi/BiOrderController.cs
  2. 45 0
      src/Hotline.Api/Controllers/OrderApi/OrderDelayController.cs
  3. 1 0
      src/Hotline.Api/Controllers/OrderController.cs
  4. 32 3
      src/Hotline.Api/Controllers/TestController.cs
  5. 1 1
      src/Hotline.Api/StartupExtensions.cs
  6. 20 13
      src/Hotline.Api/StartupHelper.cs
  7. 13 12
      src/Hotline.Api/config/appsettings.Development.json
  8. 52 0
      src/Hotline.Application/Jobs/ApptaskJob.cs
  9. 32 0
      src/Hotline.Application/Mappers/BatchTaskMapperConfigs.cs
  10. 12 1
      src/Hotline.Application/Mappers/OrderMapperConfigs.cs
  11. 1 0
      src/Hotline.Application/OrderApp/OrderApplication.cs
  12. 23 0
      src/Hotline.Application/OrderApp/OrderDelayApp/IOrderDelayApplication.cs
  13. 216 0
      src/Hotline.Application/OrderApp/OrderDelayApp/OrderDelayApplication.cs
  14. 20 0
      src/Hotline.Application/OrderApp/OrderDelayApp/OrderDelayAuditTaskExecutor.cs
  15. 1 1
      src/Hotline.Application/OrderApp/OrderVisitApp/IOrderVisitApplication.cs
  16. 1 1
      src/Hotline.Application/OrderApp/OrderVisitApp/OrderVisitApplication.cs
  17. 83 0
      src/Hotline.Application/OrderApp/OrderVisitApp/VoiceVisitTaskExecutor.cs
  18. 50 0
      src/Hotline.Share/Dtos/BatchTask/AddApptaskRequest.cs
  19. 13 0
      src/Hotline.Share/Dtos/BatchTask/ApptaskProgressDto.cs
  20. 138 0
      src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayDto.cs
  21. 33 0
      src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayReviewRequest.cs
  22. 6 0
      src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayStepIdDto.cs
  23. 1 0
      src/Hotline.Share/Dtos/Order/OrderDto.cs
  24. 1 0
      src/Hotline.Share/Dtos/Order/OrderStartFlowDto.cs
  25. 0 132
      src/Hotline.Share/Dtos/Order/QueryOrderDto.cs
  26. 13 0
      src/Hotline.Share/Enums/BatchTask/ETaskStatus.cs
  27. 27 0
      src/Hotline.Share/Enums/BatchTask/ETaskType.cs
  28. 33 0
      src/Hotline/BatchTask/Apptask.cs
  29. 180 0
      src/Hotline/BatchTask/ApptaskDomainService.cs
  30. 71 0
      src/Hotline/BatchTask/ApptaskItem.cs
  31. 48 0
      src/Hotline/BatchTask/IApptaskDomainService.cs
  32. 19 0
      src/Hotline/BatchTask/IApptaskExecutor.cs
  33. 1 0
      src/Hotline/CallCenter/Configs/XingTangConfiguration.cs
  34. 1 12
      src/Hotline/FlowEngine/Workflows/IWorkflowDomainService.cs
  35. 40 13
      src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs
  36. 5 1
      src/Hotline/Orders/OrderDelay.cs
  37. 39 0
      src/Hotline/Validators/BatchTask/ApptaskValidator.cs
  38. 1 1
      test/Hotline.Tests/Application/OrderVisitApplicationTest.cs

+ 2 - 1
src/Hotline.Api/Controllers/Bi/BiOrderController.cs

@@ -23,6 +23,7 @@ using Hotline.Share.Dtos.Bi;
 using Hotline.Share.Dtos.Bigscreen;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.Order.OrderDelay;
 using Hotline.Share.Enums.CallCenter;
 using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Order;
@@ -47,11 +48,11 @@ using Hotline.Share.Dtos.File;
 using Hotline.File;
 using Hotline.KnowledgeBase;
 using DocumentFormat.OpenXml.Vml.Spreadsheet;
-using Hotline.Application.OrderApp;
 using Hotline.Share.Tools;
 using MediatR;
 using static Hotline.AppDefaults;
 using Hotline.Ai.Visit;
+using Hotline.Application.OrderApp.OrderVisitApp;
 
 namespace Hotline.Api.Controllers.Bi
 {

+ 45 - 0
src/Hotline.Api/Controllers/OrderApi/OrderDelayController.cs

@@ -0,0 +1,45 @@
+using Hotline.Application.OrderApp.OrderDelayApp;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.Order.OrderDelay;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hotline.Api.Controllers.OrderApi;
+
+public class OrderDelayController : BaseController
+{
+    private readonly IOrderDelayApplication _orderDelayApplication;
+    private readonly IMapper _mapper;
+
+    public OrderDelayController(
+        IOrderDelayApplication orderDelayApplication,
+        IMapper mapper)
+    {
+        _orderDelayApplication = orderDelayApplication;
+        _mapper = mapper;
+    }
+
+    /// <summary>
+    /// 查询延期待审批
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("waited")]
+    public async Task<IReadOnlyList<OrderDelayStepIdDto>> QueryWaited([FromQuery] DelayListDto dto)
+    {
+        var delays = await _orderDelayApplication.QueryWaited(dto)
+            .ToPageListWithoutTotalAsync(dto, HttpContext.RequestAborted);
+        var list = _mapper.Map<IReadOnlyList<OrderDelayStepIdDto>>(delays);
+        return list;
+    }
+
+    /// <summary>
+    /// 延期审核
+    /// </summary>
+    [HttpPost("review")]
+    public Task ReviewAsync([FromBody]OrderDelayReviewRequest request) =>
+        _orderDelayApplication.ReviewAsync(request, HttpContext.RequestAborted);
+
+
+}

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

@@ -84,6 +84,7 @@ using OrderDto = Hotline.Share.Dtos.Order.OrderDto;
 using UserInfo = Hotline.Share.Dtos.FlowEngine.UserInfo;
 using Hotline.Caching.Services;
 using Hotline.Early;
+using Hotline.Share.Dtos.Order.OrderDelay;
 using MathNet.Numerics.Distributions;
 
 namespace Hotline.Api.Controllers;

+ 32 - 3
src/Hotline.Api/Controllers/TestController.cs

@@ -64,6 +64,7 @@ using NETCore.Encrypt;
 using NETCore.Encrypt.Internal;
 using SqlSugar;
 using System.Text;
+using Hotline.BatchTask;
 using XC.RSAUtil;
 using XF.Domain.Authentications;
 using XF.Domain.Cache;
@@ -75,6 +76,9 @@ using XF.Domain.Repository;
 using Order = Hotline.Orders.Order;
 using OrderDto = Hotline.Share.Dtos.Order.OrderDto;
 using Hotline.Schedulings;
+using Hotline.Share.Dtos.BatchTask;
+using Hotline.Share.Enums.BatchTask;
+using Hotline.Application.OrderApp.OrderVisitApp;
 
 namespace Hotline.Api.Controllers;
 
@@ -138,6 +142,7 @@ public class TestController : BaseController
     private readonly IRepository<OrderSecondaryHandling> _orderSecondaryHandlingRepository;
     private readonly IRepository<Message> _messageRepository; 
     private readonly IRepository<Scheduling> _schedulingRepository;
+    private readonly IApptaskDomainService _apptaskDomainService;
 
 
     public TestController(
@@ -196,9 +201,9 @@ public class TestController : BaseController
         IRepository<OrderVisit> orderVisitRepository,
         IServiceProvider serviceProvider,
         IRepository<OrderSecondaryHandling> orderSecondaryHandlingRepository,
-        IRepository<Message> messageRepository
-,
-        IRepository<Scheduling> schedulingRepository)
+        IRepository<Message> messageRepository,
+        IRepository<Scheduling> schedulingRepository,
+        IApptaskDomainService apptaskDomainService)
     {
         _logger = logger;
         _options = options;
@@ -253,6 +258,7 @@ public class TestController : BaseController
         _orderSecondaryHandlingRepository = orderSecondaryHandlingRepository;
         _messageRepository = messageRepository;
         _schedulingRepository = schedulingRepository;
+        _apptaskDomainService = apptaskDomainService;
     }
     /// <summary>
     /// 测试
@@ -1602,5 +1608,28 @@ public class TestController : BaseController
 
         return $"共计: {total}, 成功:{successed}";
     }
+
+    [AllowAnonymous]
+    [HttpGet("addapptask")]
+    public async Task AddAppTask()
+    {
+        await _apptaskDomainService.AddAsync(new AddApptaskRequest
+        {
+            TaskType = ETaskType.VoiceVisit,
+            TryLimit = 5,
+            ApptaskItems = new List<AddApptaskItemRequest>
+            {
+                new AddApptaskItemRequest
+                {
+                    BusinessId = Ulid.NewUlid().ToString(),
+                    TaskParams = new VoiceVisitRequest
+                    {
+                        PhoneNo = "15881089499",
+                        VisitId = Ulid.NewUlid().ToString()
+                    }
+                }
+            }
+        }, HttpContext.RequestAborted);
+    }
 }
 

+ 1 - 1
src/Hotline.Api/StartupExtensions.cs

@@ -242,7 +242,7 @@ internal static class StartupExtensions
     internal static WebApplication ConfigurePipelines(this WebApplication app)
     {
         app.UseSerilogRequestLogging();
-
+        
         // Configure the HTTP request pipeline.
         var swaggerEnable = app.Configuration.GetSection("Swagger").Get<bool>();
         if (swaggerEnable)

+ 20 - 13
src/Hotline.Api/StartupHelper.cs

@@ -260,7 +260,7 @@ namespace Hotline.Api
             return services;
         }
 
-        public static IServiceCollection RegisterJob(this IServiceCollection services,AppConfiguration appConfiguration )
+        public static IServiceCollection RegisterJob(this IServiceCollection services, AppConfiguration appConfiguration)
         {
             services.AddQuartz(d =>
             {
@@ -269,8 +269,15 @@ namespace Hotline.Api
                 d.InterruptJobsOnShutdownWithWait = true;
                 d.MaxBatchSize = 3;
 
-                //load send order job
-                
+                var apptaskJob = new JobKey(nameof(ApptaskJob));
+                d.AddJob<ApptaskJob>(apptaskJob);
+                d.AddTrigger(t => t
+                    .WithIdentity("apptask-trigger")
+                    .ForJob(apptaskJob)
+                    .StartNow()
+                    .WithCronSchedule("0/3 * * * * ?")
+                );
+
 
                 //即将超期和超期短信
                 //var autoSendOverTimeSmsKey = new JobKey(nameof(SendOverTimeSmsJob), "send overtime order task");
@@ -282,7 +289,7 @@ namespace Hotline.Api
                 //    .WithCronSchedule("0 30 09,14 * * ?"));
 
                 var autoSendOrderKey = new JobKey(nameof(SendOrderJob), "send order task");
-				switch (appConfiguration.AppScope)
+                switch (appConfiguration.AppScope)
                 {
                     //智能化任务
                     case AppDefaults.AppScope.YiBin:
@@ -303,15 +310,15 @@ namespace Hotline.Api
                         //);
                         break;
                     case AppDefaults.AppScope.ZiGong:
-	                    //d.AddJob<SendOrderJob>(autoSendOrderKey);
-	                    //d.AddTrigger(t => t
-		                   // .WithIdentity("task-send-order-trigger")
-		                   // .ForJob(autoSendOrderKey)
-		                   // .StartNow()
-		                   // .WithCronSchedule("0 10 9 * * ?")
-	                    //);
-						break;
-					default:
+                        //d.AddJob<SendOrderJob>(autoSendOrderKey);
+                        //d.AddTrigger(t => t
+                        // .WithIdentity("task-send-order-trigger")
+                        // .ForJob(autoSendOrderKey)
+                        // .StartNow()
+                        // .WithCronSchedule("0 10 9 * * ?")
+                        //);
+                        break;
+                    default:
                         break;
                 }
 

+ 13 - 12
src/Hotline.Api/config/appsettings.Development.json

@@ -62,14 +62,15 @@
       "Ip": "222.213.23.229"
     },
     "XingTang": {
-        //"DbConnectionString": "server=123.56.10.71;Database=callcenter_db;Uid=root;Pwd=Lhw1981!(*!"
-        //"DbConnectionString": "server=110.188.24.182;Database=callcenter_xingtang;Uid=dev;Pwd=fengwo11!!"
-        "DbConnectionString": "PORT=50143;server=110.188.24.182;Database=callcenter_db;Uid=dev;Pwd=fengwo123!@#;"
+      //"DbConnectionString": "server=123.56.10.71;Database=callcenter_db;Uid=root;Pwd=Lhw1981!(*!"
+      //"DbConnectionString": "server=110.188.24.182;Database=callcenter_xingtang;Uid=dev;Pwd=fengwo11!!"
+      "DbConnectionString": "PORT=50143;server=110.188.24.182;Database=callcenter_db;Uid=dev;Pwd=fengwo123!@#;",
+      "Address": "http://123.56.10.71:6090"
     }
   },
-    "ConnectionStrings": {
-        "Hotline": "PORT=5432;DATABASE=hotline;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;"
-    },
+  "ConnectionStrings": {
+    "Hotline": "PORT=5432;DATABASE=hotline;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;"
+  },
   "Cache": {
     "Host": "110.188.24.182",
     "Port": 50179,
@@ -77,7 +78,7 @@
     "Database": 3 //hl:3, dev:5, test:2, demo:4
   },
   "Swagger": true,
-  "AccLog":  false,
+  "AccLog": false,
   "Cors": {
     "Origins": [ "http://localhost:8888", "http://admin.hotline.fw.com", "http://localhost:80", "http://localhost:8113" ]
   },
@@ -118,11 +119,11 @@
     "UseDashBoard": true,
     "FailedRetryCount": 5,
     "RabbitMq": {
-        "UserName": "dev",
-        "Password": "123456",
-        "HostName": "110.188.24.182",
-        "VirtualHost": "fwt-dev"
-        // "VirtualHost": "fwt-unittest"
+      "UserName": "dev",
+      "Password": "123456",
+      "HostName": "110.188.24.182",
+      "VirtualHost": "fwt-dev"
+      // "VirtualHost": "fwt-unittest"
     }
   },
   "FwClient": {

+ 52 - 0
src/Hotline.Application/Jobs/ApptaskJob.cs

@@ -0,0 +1,52 @@
+using Hotline.BatchTask;
+using Quartz;
+using Hotline.Share.Enums.BatchTask;
+using Hotline.Share.Dtos.Order;
+using Microsoft.Extensions.DependencyInjection;
+using Hotline.Application.OrderApp.OrderVisitApp;
+
+namespace Hotline.Application.Jobs
+{
+    public class ApptaskJob : IJob, IDisposable
+    {
+        private readonly IApptaskDomainService _apptaskDomainService;
+        private readonly IServiceProvider _serviceProvider;
+
+        public ApptaskJob(IApptaskDomainService apptaskDomainService,
+            IServiceProvider serviceProvider)
+        {
+            _apptaskDomainService = apptaskDomainService;
+            _serviceProvider = serviceProvider;
+        }
+
+        public async Task Execute(IJobExecutionContext context)
+        {
+            //Console.WriteLine($"执行ApptaskJob: {DateTime.Now}");
+            var task = await _apptaskDomainService.GetWaitingTaskAsync(context.CancellationToken);
+            if (task is null) return;
+            switch (task.TaskType)
+            {
+                case ETaskType.Delay:
+                    //var delayExecutor = _serviceProvider.GetService<IApptaskExecutor<BatchDelayNextFlowDto>>();
+                    //await _apptaskDomainService.ExecuteAsync(delayExecutor, task, context.CancellationToken);
+                    break;
+                case ETaskType.Screen:
+                    break;
+                case ETaskType.VoiceVisit:
+                    var vvExecutor = _serviceProvider.GetService<IApptaskExecutor<VoiceVisitRequest>>();
+                    await _apptaskDomainService.ExecuteAsync(vvExecutor, task, context.CancellationToken);
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+        }
+
+        /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+        public void Dispose()
+        {
+            GC.SuppressFinalize(this);
+        }
+
+
+    }
+}

+ 32 - 0
src/Hotline.Application/Mappers/BatchTaskMapperConfigs.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.BatchTask;
+using Hotline.Share.Dtos.BatchTask;
+using Hotline.Share.Enums.BatchTask;
+using Mapster;
+using XF.Domain.Exceptions;
+
+namespace Hotline.Application.Mappers
+{
+    public class BatchTaskMapperConfigs : IRegister
+    {
+        public void Register(TypeAdapterConfig config)
+        {
+            config.ForType<Apptask, ApptaskProgressDto>()
+                .BeforeMapping((s, d) =>
+                {
+                    if (!s.ApptaskItems.Any())
+                        throw new UserFriendlyException("任务明细无数据");
+                })
+                .Map(d => d.Total, s => s.ApptaskItems.Count)
+                .Map(d => d.Waiting, s => s.ApptaskItems.Count(i => i.TaskStatus == ETaskStatus.Waiting))
+                .Map(d => d.Processing, s => s.ApptaskItems.Count(i => i.TaskStatus == ETaskStatus.Processing))
+                .Map(d => d.Succeeded, s => s.ApptaskItems.Count(i => i.TaskStatus == ETaskStatus.Succeeded))
+                .Map(d => d.Failed, s => s.ApptaskItems.Count(i => i.TaskStatus == ETaskStatus.Failed));
+
+        }
+    }
+}

+ 12 - 1
src/Hotline.Application/Mappers/OrderMapperConfigs.cs

@@ -5,6 +5,7 @@ using Hotline.Orders;
 using Hotline.Share.Dtos.Ai;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Dtos.Order.Detail;
+using Hotline.Share.Dtos.Order.OrderDelay;
 using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Order;
@@ -164,13 +165,23 @@ public class OrderMapperConfigs : IRegister
             .Map(d => d.VoiceEvaluate, s => s.VoiceEvaluate)
             .Map(d => d.SeatEvaluate, s => s.SeatEvaluate);
 
-        config.ForType<OrderDelay, OrderDelayDto>()
+        config.ForType<OrderDelay, Hotline.Share.Dtos.Order.OrderDelay.OrderDelayDto>()
             //.Inherits<Order, OrderDto>()
             .Map(d => d.CurrentStepName, s => s.Workflow.ActualHandleStepName)
             .Map(d => d.ActualHandlerName, s => s.Workflow.ActualHandlerName)
             .IgnoreIf((s, d) => s.Workflow == null, d => d.CurrentStepName)
             .IgnoreIf((s, d) => s.Workflow == null, d => d.ActualHandlerName);
 
+        config.ForType<OrderDelay, OrderDelayStepIdDto>()
+            .Map(d => d.StepId, s => s.WorkflowSteps.First().Id)
+            .Map(d => d.CurrentStepName, s => s.WorkflowSteps.First().Name)
+            .Map(d => d.ActualHandlerName, s => s.WorkflowSteps.First().HandlerName)
+            .IgnoreIf((s, d) => s.WorkflowSteps == null || !s.WorkflowSteps.Any(),
+                d => d.StepId,
+                d => d.CurrentStepName,
+                d => d.ActualHandlerName)
+            ;
+
         config.ForType<ExpiredTimeWithConfig, UpdateOrderDto>()
             .Map(d => d.TimeLimit, s => s.TimeText)
             .Map(d => d.TimeLimitCount, s => s.Count)

+ 1 - 0
src/Hotline.Application/OrderApp/OrderApplication.cs

@@ -5,6 +5,7 @@ using DocumentFormat.OpenXml.Spreadsheet;
 using DotNetCore.CAP;
 using FluentValidation;
 using Hotline.Application.FlowEngine;
+using Hotline.Application.OrderApp.OrderVisitApp;
 using Hotline.Application.Quality;
 using Hotline.Article;
 using Hotline.Authentications;

+ 23 - 0
src/Hotline.Application/OrderApp/OrderDelayApp/IOrderDelayApplication.cs

@@ -0,0 +1,23 @@
+using Hotline.Orders;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.Order.OrderDelay;
+using SqlSugar;
+
+namespace Hotline.Application.OrderApp.OrderDelayApp;
+
+public interface IOrderDelayApplication
+{
+    /// <summary>
+    /// 查询延期待审批列表
+    /// </summary>
+    /// <returns></returns>
+    ISugarQueryable<OrderDelay> QueryWaited(DelayListDto dto);
+
+    /// <summary>
+    /// 延期审核
+    /// </summary>
+    /// <param name="request"></param>
+    /// <param name="cancellation"></param>
+    /// <returns></returns>
+    Task ReviewAsync(OrderDelayReviewRequest request, CancellationToken cancellation);
+}

+ 216 - 0
src/Hotline.Application/OrderApp/OrderDelayApp/OrderDelayApplication.cs

@@ -0,0 +1,216 @@
+using Hotline.FlowEngine.Workflows;
+using Hotline.Share.Dtos.FlowEngine;
+using Hotline.Share.Dtos.FlowEngine.Workflow;
+using Hotline.Share.Dtos.Order.OrderDelay;
+using Hotline.Share.Enums.FlowEngine;
+using MapsterMapper;
+using Microsoft.AspNetCore.Http;
+using System.Threading;
+using Hotline.BatchTask;
+using Hotline.Orders;
+using Hotline.Share.Dtos.BatchTask;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.BatchTask;
+using Hotline.Share.Enums.Order;
+using SqlSugar;
+using XF.Domain.Authentications;
+using XF.Domain.Dependency;
+using XF.Domain.Entities;
+using XF.Domain.Exceptions;
+
+namespace Hotline.Application.OrderApp.OrderDelayApp;
+
+public class OrderDelayApplication : IOrderDelayApplication, IScopeDependency
+{
+    private readonly IOrderDomainService _orderDomainService;
+    private readonly IOrderDelayRepository _orderDelayRepository;
+    private readonly IWorkflowDomainService _workflowDomainService;
+    private readonly IApptaskDomainService _apptaskDomainService;
+    private readonly ISessionContext _sessionContext;
+    private readonly IMapper _mapper;
+
+    public OrderDelayApplication(
+        IOrderDomainService orderDomainService,
+        IOrderDelayRepository orderDelayRepository,
+        IWorkflowDomainService workflowDomainService,
+        IApptaskDomainService apptaskDomainService,
+        ISessionContext sessionContext,
+        IMapper mapper)
+    {
+        _orderDomainService = orderDomainService;
+        _orderDelayRepository = orderDelayRepository;
+        _workflowDomainService = workflowDomainService;
+        _apptaskDomainService = apptaskDomainService;
+        _sessionContext = sessionContext;
+        _mapper = mapper;
+    }
+
+    /// <summary>
+    /// 查询延期待审批列表
+    /// </summary>
+    /// <returns></returns>
+    public ISugarQueryable<OrderDelay> QueryWaited(DelayListDto dto)
+    {
+        var isAdmin = _orderDomainService.IsCheckAdmin();
+        var hasHandled = dto.IsApply.HasValue && dto.IsApply.Value;
+        var query = _orderDelayRepository.Queryable()
+            .Includes(d => d.WorkflowSteps.Where(step =>
+                    ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) &&
+                      step.HandlerId == _sessionContext.RequiredUserId) ||
+                     (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) &&
+                      step.HandlerOrgId == _sessionContext.RequiredOrgId) ||
+                     (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) &&
+                      _sessionContext.Roles.Contains(step.RoleId)) ||
+                     (step.FlowAssignType == EFlowAssignType.OrgAndRole && !string.IsNullOrEmpty(step.RoleId) &&
+                      _sessionContext.Roles.Contains(step.RoleId)
+                      && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == _sessionContext.RequiredOrgId)))
+                .OrderByDescending(step => step.CreationTime)
+                .Take(1)
+                .ToList()
+            );
+
+        if (!isAdmin)
+        {
+            if (hasHandled)
+            {
+                query.Where(d => d.WorkflowSteps
+                    .Any(step =>
+                        ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) &&
+                          step.HandlerId == _sessionContext.RequiredUserId) ||
+                         (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) &&
+                          step.HandlerOrgId == _sessionContext.RequiredOrgId) ||
+                         (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) &&
+                          _sessionContext.Roles.Contains(step.RoleId)) ||
+                         (step.FlowAssignType == EFlowAssignType.OrgAndRole && !string.IsNullOrEmpty(step.RoleId) &&
+                          _sessionContext.Roles.Contains(step.RoleId)
+                          && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == _sessionContext.RequiredOrgId))
+                        && step.Status == EWorkflowStepStatus.Handled));
+            }
+            else
+            {
+                query.Where(d => d.WorkflowSteps
+                    .Any(step =>
+                        ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) &&
+                          step.HandlerId == _sessionContext.RequiredUserId) ||
+                         (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) &&
+                          step.HandlerOrgId == _sessionContext.RequiredOrgId) ||
+                         (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) &&
+                          _sessionContext.Roles.Contains(step.RoleId)) ||
+                         (step.FlowAssignType == EFlowAssignType.OrgAndRole && !string.IsNullOrEmpty(step.RoleId) &&
+                          _sessionContext.Roles.Contains(step.RoleId)
+                          && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == _sessionContext.RequiredOrgId))
+                        && step.Status < EWorkflowStepStatus.Handled));
+            }
+        }
+
+
+        query.Includes(d => d.Order)
+            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Order.Title.Contains(dto.Keyword!) || d.No.Contains(dto.Keyword!))
+            .WhereIF(dto.IsApply == false, d => d.DelayState == EDelayState.Examining)
+            .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.Order.No.Contains(dto.No)) //工单编号
+            .WhereIF(dto.IsProvince.HasValue && dto.IsProvince == true, d => d.Order.IsProvince == true) //是否省工单
+            .WhereIF(dto.IsProvince.HasValue && dto.IsProvince == false, d => d.Order.IsProvince == false)
+            .WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Order.Title.Contains(dto.Title)) //工单标题
+            .WhereIF(!string.IsNullOrEmpty(dto.Channel), d => d.Order.SourceChannelCode == dto.Channel) //来源渠道
+            .WhereIF(dto.CreationTimeStart.HasValue, d => d.Order.CreationTime >= dto.CreationTimeStart) //受理时间Start
+            .WhereIF(dto.CreationTimeEnd.HasValue, d => d.Order.CreationTime <= dto.CreationTimeEnd) //受理时间End
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptorName), d => d.Order.AcceptorName == dto.AcceptorName!) //受理人
+            .WhereIF(!string.IsNullOrEmpty(dto.Hotspot),
+                d => d.Order.HotspotSpliceName != null && d.Order.HotspotSpliceName.Contains(dto.Hotspot)) //热点
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptTypeCode), d => d.Order.AcceptTypeCode == dto.AcceptTypeCode) //受理类型
+            .WhereIF(!string.IsNullOrEmpty(dto.OrgLevelOneName), d => d.Order.OrgLevelOneName.Contains(dto.OrgLevelOneName)) //一级部门
+            .WhereIF(!string.IsNullOrEmpty(dto.CurrentHandleOrgName), d => d.Order.CurrentHandleOrgName.Contains(dto.CurrentHandleOrgName)) //接办部门
+            .WhereIF(dto.CurrentHandleTimeStart.HasValue, d => d.Order.CurrentHandleTime >= dto.CurrentHandleTimeStart) //接办时间Start
+            .WhereIF(dto.CurrentHandleTimeEnd.HasValue, d => d.Order.CurrentHandleTime <= dto.CurrentHandleTimeEnd) //接办时间End
+            .WhereIF(dto.ApplyTimeStart.HasValue, d => d.CreationTime >= dto.ApplyTimeStart) //延期申请时间Start
+            .WhereIF(dto.ApplyTimeEnd.HasValue, d => d.CreationTime <= dto.ApplyTimeEnd) //延期申请时间End
+            .WhereIF(!string.IsNullOrEmpty(dto.ApplyName), d => d.CreatorName.Contains(dto.ApplyName)) //延期申请人
+            .WhereIF(!string.IsNullOrEmpty(dto.ApplyOrgName), d => d.CreatorOrgName.Contains(dto.ApplyOrgName)) //延期申请部门
+            .WhereIF(dto.DelayNum.HasValue, d => d.DelayNum == dto.DelayNum) //延期申请时限
+            .WhereIF(dto.DelayUnit.HasValue, d => d.DelayUnit == dto.DelayUnit) //延期申请单位
+            .WhereIF(!string.IsNullOrEmpty(dto.DelayReason), d => d.DelayReason.Contains(dto.DelayReason)) //申请理由
+            .WhereIF(dto.BeforeDelayStart.HasValue, d => d.BeforeDelay >= dto.BeforeDelayStart) //申请前期满时间Start
+            .WhereIF(dto.BeforeDelayEnd.HasValue, d => d.BeforeDelay <= dto.BeforeDelayEnd) //申请前期满时间End
+            .OrderByDescending(d => d.ApplyDelayTime)
+            ;
+
+        return query;
+    }
+
+    /// <summary>
+    /// 延期审核
+    /// </summary>
+    /// <param name="request"></param>
+    /// <param name="cancellation"></param>
+    /// <returns></returns>
+    public async Task ReviewAsync(OrderDelayReviewRequest request, CancellationToken cancellation)
+    {
+        /*
+         * 查询delay
+         * 查询workflow
+         * 查询当前节点(待受理or待办理)
+         * 查询nextStep(_workflowApplication.GetNextStepsAsync)
+         * 非省工单删除省审批节点
+         *!_sessionContext.OrgIsCenter && currentStep.Name != "中心初审"删除中心终审
+         * 从nextStep中查找与dto传入的nextStepName相同的节点,workflow.NextStepCode = step.Key;workflow.NextStepName = step.Value;
+         * 通过:nextAsync,不同意:rejectAsync
+         * endhandler:
+         *  1.更新orderDelay.DelayState
+         *  2.审批通过:
+         *      a.更新工单办理期满时间(_orderApplication.DelayOrderExpiredTimeAsync)
+         *      b.更新工单未办理节点的期满时间(_workflowDomainService.UpdateUnhandleExpiredTimeAsync)
+         *      c.cap publish EventNames.HotlineOrderExpiredTimeUpdate
+         */
+
+        request.NextWorkflow.ReviewResult = request.IsPass ? EReviewResult.Approval : EReviewResult.Failed;
+        if (request.IsPass)
+        {
+            await _workflowDomainService.NextAsync(request.NextWorkflow, cancellationToken: cancellation);
+        }
+        else
+        {
+            var reject = _mapper.Map<RejectDto>(request.NextWorkflow);
+            await _workflowDomainService.RejectAsync(reject, cancellation);
+        }
+    }
+
+    /// <summary>
+    /// 批量审核
+    /// </summary>
+    /// <returns></returns>
+    public async Task BatchReviewAsync(BatchOrderDelayReviewRequest request, CancellationToken cancellation)
+    {
+        var delayIds = request.DelayWithStepIds.Select(d => d.DelayId).Distinct().ToList();
+        var delays = await _orderDelayRepository.Queryable()
+            .Where(d => delayIds.Contains(d.Id))
+            .ToListAsync(cancellation);
+
+        var apptaskItems = new List<ApptaskItem>();
+        foreach (var delay in delays)
+        {
+            var stepId = request.DelayWithStepIds.First(d => d.DelayId == delay.Id).StepId;
+            request.NextWorkflow.StepId = stepId;
+            apptaskItems.Add(new ApptaskItem
+            {
+                BusinessId = delay.Id,
+                TaskType = ETaskType.Delay,
+                TaskParams = System.Text.Json.JsonSerializer.Serialize(request.NextWorkflow)
+            });
+        }
+
+        var apptask = new Apptask
+        {
+            TaskType = ETaskType.Delay,
+            ApptaskItems = apptaskItems,
+        };
+        
+        //
+        //var addApptask = new AddApptaskRequest
+        //{
+            
+        //}
+        
+        
+        //_apptaskDomainService.AddAsync()
+    }
+}

+ 20 - 0
src/Hotline.Application/OrderApp/OrderDelayApp/OrderDelayAuditTaskExecutor.cs

@@ -0,0 +1,20 @@
+//using Hotline.BatchTask;
+//using Hotline.Share.Dtos.Order;
+//using XF.Domain.Dependency;
+
+//namespace Hotline.Application.OrderApp.OrderDelayApp;
+
+//public class OrderDelayAuditTaskExecutor : IApptaskExecutor<BatchDelayNextFlowDto>, IScopeDependency
+//{
+//    /// <summary>
+//    /// 执行任务
+//    /// </summary>
+//    /// <param name="request"></param>
+//    /// <param name="cancellation"></param>
+//    /// <returns>是否成功执行</returns>
+//    public async Task<ApptaskExecuteResult> ExecuteAsync(BatchDelayNextFlowDto? request, CancellationToken cancellation)
+//    {
+
+//    }
+
+//}

+ 1 - 1
src/Hotline.Application/OrderApp/IOrderVisitApplication.cs → src/Hotline.Application/OrderApp/OrderVisitApp/IOrderVisitApplication.cs

@@ -1,6 +1,6 @@
 using Hotline.Share.Dtos.Order;
 
-namespace Hotline.Application.OrderApp;
+namespace Hotline.Application.OrderApp.OrderVisitApp;
 
 public interface IOrderVisitApplication
 {

+ 1 - 1
src/Hotline.Application/OrderApp/OrderVisitApplication.cs → src/Hotline.Application/OrderApp/OrderVisitApp/OrderVisitApplication.cs

@@ -9,7 +9,7 @@ using SqlSugar;
 using XF.Domain.Dependency;
 using XF.Domain.Repository;
 
-namespace Hotline.Application.OrderApp;
+namespace Hotline.Application.OrderApp.OrderVisitApp;
 
 /// <summary>
 /// 回访服务

+ 83 - 0
src/Hotline.Application/OrderApp/OrderVisitApp/VoiceVisitTaskExecutor.cs

@@ -0,0 +1,83 @@
+using Hotline.BatchTask;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+using XF.Domain.Dependency;
+using Hotline.CallCenter.Configs;
+
+namespace Hotline.Application.OrderApp.OrderVisitApp
+{
+    public class VoiceVisitTaskExecutor : IApptaskExecutor<VoiceVisitRequest>, IScopeDependency
+    {
+        private readonly IHttpClientFactory _httpClientFactory;
+        private readonly IOptionsSnapshot<CallCenterConfiguration> _callcenterOptions;
+
+        public VoiceVisitTaskExecutor(
+            IHttpClientFactory httpClientFactory,
+            IOptionsSnapshot<CallCenterConfiguration> callcenterOptions)
+        {
+            _httpClientFactory = httpClientFactory;
+            _callcenterOptions = callcenterOptions;
+        }
+
+        /// <summary>
+        /// 执行任务
+        /// </summary>
+        /// <param name="request"></param>
+        /// <param name="cancellation"></param>
+        /// <returns>是否成功执行</returns>
+        public async Task<ApptaskExecuteResult> ExecuteAsync(VoiceVisitRequest? request, CancellationToken cancellation)
+        {
+            Console.WriteLine($"执行vv: {DateTime.Now}");
+            if (request == null)
+                return new ApptaskExecuteResult
+                {
+                    IsSuccess = false,
+                    Message = "请求参数为空"
+                };
+
+            var baseAddress = _callcenterOptions.Value.XingTang.Address;
+            if (string.IsNullOrEmpty(baseAddress))
+                return new ApptaskExecuteResult
+                {
+                    IsSuccess = false,
+                    Message = "未配置请求地址"
+                };
+            var client = _httpClientFactory.CreateClient();
+            client.BaseAddress = new Uri(baseAddress);
+            var url = $"{baseAddress}/groupcall?content=&called1={request.PhoneNo}&called2=&called3=&called4=&called5=&caller=&customerid={request.VisitId}";
+            var result = await client.GetAsync(url, cancellation);
+            if (result.IsSuccessStatusCode)
+            {
+                return new ApptaskExecuteResult
+                {
+                    IsSuccess = true,
+                    Message = "成功"
+                };
+            }
+            else
+            {
+                return new ApptaskExecuteResult
+                {
+                    IsSuccess = false,
+                    Message = "请求失败"
+                };
+            }
+
+            return new ApptaskExecuteResult
+            {
+                IsSuccess = true,
+                Message = "成功"
+            };
+        }
+    }
+
+    public class VoiceVisitRequest
+    {
+        public string PhoneNo { get; set; }
+        public string VisitId { get; set; }
+    }
+}

+ 50 - 0
src/Hotline.Share/Dtos/BatchTask/AddApptaskRequest.cs

@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.Share.Enums.BatchTask;
+
+namespace Hotline.Share.Dtos.BatchTask
+{
+    public class AddApptaskRequest
+    {
+        /// <summary>
+        /// 任务名称
+        /// </summary>
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// 任务描述
+        /// </summary>
+        public string? Description { get; set; }
+
+        /// <summary>
+        /// 任务类型
+        /// </summary>
+        public ETaskType TaskType { get; set; }
+
+        /// <summary>
+        /// 执行次数上限
+        /// </summary>
+        public int TryLimit { get; set; } = 1;
+
+        /// <summary>
+        /// 优先级
+        /// 由高到低(0-9)
+        /// </summary>
+        public int? Priority { get; set; }
+
+        public List<AddApptaskItemRequest> ApptaskItems { get; set; }
+    }
+
+    public class AddApptaskItemRequest
+    {
+        /// <summary>
+        /// 业务id(冗余)
+        /// </summary>
+        public string BusinessId { get; set; }
+        
+        public object? TaskParams { get; set; }
+    }
+}

+ 13 - 0
src/Hotline.Share/Dtos/BatchTask/ApptaskProgressDto.cs

@@ -0,0 +1,13 @@
+namespace Hotline.Share.Dtos.BatchTask;
+
+/// <summary>
+/// 任务进度
+/// </summary>
+public class ApptaskProgressDto
+{
+    public int Total { get; set; }
+    public int Waiting { get; set; }
+    public int Processing { get; set; }
+    public int Succeeded { get; set; }
+    public int Failed { get; set; }
+}

+ 138 - 0
src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayDto.cs

@@ -0,0 +1,138 @@
+using Hotline.Share.Dtos.File;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Enums.Settings;
+using XF.Utility.EnumExtensions;
+
+namespace Hotline.Share.Dtos.Order.OrderDelay;
+
+public class OrderDelayDto
+{
+    /// <summary>
+    /// 工单编号(冗余)
+    /// </summary>
+    public string No { get; set; }
+
+    /// <summary>
+    /// 工单ID
+    /// </summary>
+    public string OrderId { get; set; }
+
+    public OrderDto? Order { get; set; }
+
+    /// <summary>
+    /// 延期申请时间
+    /// </summary>
+    public DateTime ApplyDelayTime { get; set; }
+
+    /// <summary>
+    /// 申请部门Code
+    /// </summary>
+    public string ApplyOrgCode { get; set; }
+
+    /// <summary>
+    /// 申请部门名称
+    /// </summary>
+    public string ApplyOrgName { get; set; }
+
+    /// <summary>
+    /// 申请人
+    /// </summary>
+    public string EmployeeId { get; set; }
+
+    /// <summary>
+    /// 
+    /// </summary>
+    public string? EmployeeName { get; set; }
+
+    /// <summary>
+    /// 延期申请时限
+    /// </summary>
+    public int DelayNum { get; set; }
+
+    /// <summary>
+    /// 延期申请单位
+    /// </summary>
+    public ETimeType DelayUnit { get; set; }
+
+    public string DelayUnitText => DelayUnit.GetDescription();
+
+    /// <summary>
+    /// 延期申请理由
+    /// </summary>
+    public string DelayReason { get; set; }
+
+    /// <summary>
+    /// 申请前期满时间
+    /// </summary>
+    public DateTime? BeforeDelay { get; set; }
+
+    /// <summary>
+    /// 申请后期满时间
+    /// </summary>
+    public DateTime? AfterDelay { get; set; }
+
+    /// <summary>
+    /// 审批状态
+    /// </summary>
+    public EDelayState DelayState { get; set; }
+
+    public string DelayStateText => DelayState.GetDescription();
+
+    /// <summary>
+    /// 是否省延期
+    /// </summary>
+    public bool IsProDelay { get; set; }
+
+    /// <summary>
+    /// 流程ID
+    /// </summary>
+    public string? WorkflowId { get; set; }
+
+    public DateTime CreationTime { get; set; }
+
+    public string Id { get; set; }
+
+    public string? CreatorId { get; set; }
+
+    public string? CreatorName { get; set; }
+
+    public string? CreatorOrgId { get; set; }
+
+    public string? CreatorOrgName { get; set; }
+
+    public int CreatorOrgLevel { get; set; }
+
+    /// <summary>
+    /// 一级部门Id
+    /// </summary>
+    public string? AreaId { get; set; }
+
+    /// <summary>
+    /// 当前办理节点
+    /// </summary>
+    public string? CurrentStepName { get; set; }
+
+    /// <summary>
+    /// 当前办理人
+    /// </summary>
+    public string? ActualHandlerName { get; set; }
+
+    /// <summary>
+    /// 是否可办理
+    /// </summary>
+    public bool IsCanHandle { get; set; }
+
+    public List<FileDto> Files { get; set; }
+
+    public List<FileJson> FileJson { get; set; }
+
+    /// <summary>
+    /// 办理 true  审批 false 
+    /// </summary>
+    public bool Handle { get; set; }
+
+    /// <summary>
+    /// 自动延期次数
+    /// </summary>
+    public int? AutomaticDelayNum { get; set; }
+}

+ 33 - 0
src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayReviewRequest.cs

@@ -0,0 +1,33 @@
+using Hotline.Share.Dtos.FlowEngine.Workflow;
+
+namespace Hotline.Share.Dtos.Order.OrderDelay;
+
+public class OrderDelayReviewRequest
+{
+    public string DelayId { get; set; }
+
+    /// <summary>
+    /// 是否通过
+    /// </summary>
+    public bool IsPass { get; set; }
+    
+    public NextWorkflowDto NextWorkflow { get; set; }
+}
+
+public class BatchOrderDelayReviewRequest
+{
+    public List<DelayWithStepId> DelayWithStepIds { get; set; }
+
+    /// <summary>
+    /// 是否通过
+    /// </summary>
+    public bool IsPass { get; set; }
+
+    public NextWorkflowDto NextWorkflow { get; set; }
+}
+
+public class DelayWithStepId
+{
+    public string DelayId { get; set; }
+    public string StepId { get; set; }
+}

+ 6 - 0
src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayStepIdDto.cs

@@ -0,0 +1,6 @@
+namespace Hotline.Share.Dtos.Order.OrderDelay;
+
+public class OrderDelayStepIdDto : OrderDelayDto
+{
+    public string StepId { get; set; }
+}

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

@@ -2,6 +2,7 @@
 using Hotline.Share.Dtos.File;
 using Hotline.Share.Dtos.FlowEngine.Workflow;
 using Hotline.Share.Dtos.Hotspots;
+using Hotline.Share.Dtos.Order.OrderDelay;
 using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Order;

+ 1 - 0
src/Hotline.Share/Dtos/Order/OrderStartFlowDto.cs

@@ -2,6 +2,7 @@
 using Hotline.Share.Dtos.Early;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.FlowEngine.Workflow;
+using Hotline.Share.Dtos.Order.OrderDelay;
 using Hotline.Share.Dtos.Org;
 using Hotline.Share.Enums.Early;
 using Hotline.Share.Enums.FlowEngine;

+ 0 - 132
src/Hotline.Share/Dtos/Order/QueryOrderDto.cs

@@ -4,7 +4,6 @@ using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Settings;
 using Hotline.Share.Requests;
-using XF.Utility.EnumExtensions;
 
 namespace Hotline.Share.Dtos.Order
 {
@@ -423,137 +422,6 @@ namespace Hotline.Share.Dtos.Order
         public List<FileJson> FileJson { get; set; }
     }
 
-    public record OrderDelayDto
-    {
-        /// <summary>
-        /// 工单编号(冗余)
-        /// </summary>
-        public string No { get; set; }
-
-        /// <summary>
-        /// 工单ID
-        /// </summary>
-        public string OrderId { get; set; }
-
-        public OrderDto? Order { get; set; }
-
-        /// <summary>
-        /// 延期申请时间
-        /// </summary>
-        public DateTime ApplyDelayTime { get; set; }
-
-        /// <summary>
-        /// 申请部门Code
-        /// </summary>
-        public string ApplyOrgCode { get; set; }
-
-        /// <summary>
-        /// 申请部门名称
-        /// </summary>
-        public string ApplyOrgName { get; set; }
-
-        /// <summary>
-        /// 申请人
-        /// </summary>
-        public string EmployeeId { get; set; }
-
-        /// <summary>
-        /// 
-        /// </summary>
-        public string? EmployeeName { get; set; }
-
-        /// <summary>
-        /// 延期申请时限
-        /// </summary>
-        public int DelayNum { get; set; }
-
-        /// <summary>
-        /// 延期申请单位
-        /// </summary>
-        public ETimeType DelayUnit { get; set; }
-
-        public string DelayUnitText => DelayUnit.GetDescription();
-
-        /// <summary>
-        /// 延期申请理由
-        /// </summary>
-        public string DelayReason { get; set; }
-
-        /// <summary>
-        /// 申请前期满时间
-        /// </summary>
-        public DateTime? BeforeDelay { get; set; }
-
-        /// <summary>
-        /// 申请后期满时间
-        /// </summary>
-        public DateTime? AfterDelay { get; set; }
-
-        /// <summary>
-        /// 审批状态
-        /// </summary>
-        public EDelayState DelayState { get; set; }
-
-        public string DelayStateText => DelayState.GetDescription();
-
-        /// <summary>
-        /// 是否省延期
-        /// </summary>
-        public bool IsProDelay { get; set; }
-
-        /// <summary>
-        /// 流程ID
-        /// </summary>
-        public string? WorkflowId { get; set; }
-
-        public DateTime CreationTime { get; set; }
-
-        public string Id { get; set; }
-
-        public string? CreatorId { get; set; }
-
-        public string? CreatorName { get; set; }
-
-        public string? CreatorOrgId { get; set; }
-
-        public string? CreatorOrgName { get; set; }
-
-        public int CreatorOrgLevel { get; set; }
-
-        /// <summary>
-        /// 一级部门Id
-        /// </summary>
-        public string? AreaId { get; set; }
-
-        /// <summary>
-        /// 当前办理节点
-        /// </summary>
-        public string? CurrentStepName { get; set; }
-        /// <summary>
-        /// 当前办理人
-        /// </summary>
-        public string? ActualHandlerName { get; set; }
-
-        /// <summary>
-        /// 是否可办理
-        /// </summary>
-        public bool IsCanHandle { get; set; }
-
-        public List<FileDto> Files { get; set; }
-
-        public List<FileJson> FileJson { get; set; }
-
-        /// <summary>
-        /// 办理 true  审批 false 
-        /// </summary>
-        public bool Handle { get; set; }
-
-        /// <summary>
-        /// 自动延期次数
-        /// </summary>
-        public int? AutomaticDelayNum { get; set; }
-    }
-
 
     public record DelayCalcEndTimeDto
     {

+ 13 - 0
src/Hotline.Share/Enums/BatchTask/ETaskStatus.cs

@@ -0,0 +1,13 @@
+namespace Hotline.Share.Enums.BatchTask;
+
+/// <summary>
+/// 任务状态
+/// </summary>
+public enum ETaskStatus
+{
+    Waiting = 0,
+    Processing = 1,
+    Succeeded = 2,
+    Failed = 3,
+    Terminated = 9,
+}

+ 27 - 0
src/Hotline.Share/Enums/BatchTask/ETaskType.cs

@@ -0,0 +1,27 @@
+using System.ComponentModel;
+
+namespace Hotline.Share.Enums.BatchTask;
+
+/// <summary>
+/// 任务类型
+/// </summary>
+public enum ETaskType
+{
+    /// <summary>
+    /// 延期
+    /// </summary>
+    [Description("延期任务")]
+    Delay = 1,
+
+    /// <summary>
+    /// 甄别
+    /// </summary>
+    [Description("甄别任务")]
+    Screen = 2,
+
+    /// <summary>
+    /// 语音回访
+    /// </summary>
+    [Description("语音回访")]
+    VoiceVisit = 3,
+}

+ 33 - 0
src/Hotline/BatchTask/Apptask.cs

@@ -0,0 +1,33 @@
+using Exam.Infrastructure.Extensions;
+using Hotline.Orders;
+using Hotline.Share.Enums.BatchTask;
+using SqlSugar;
+using XF.Domain.Repository;
+
+namespace Hotline.BatchTask;
+
+/// <summary>
+/// 系统任务
+/// </summary>
+public class Apptask : CreationEntity
+{
+    /// <summary>
+    /// 任务名称
+    /// </summary>
+    public string Name { get; set; }
+
+    /// <summary>
+    /// 任务描述
+    /// </summary>
+    public string? Description { get; set; }
+
+    /// <summary>
+    /// 任务类型
+    /// </summary>
+    public ETaskType TaskType { get; set; }
+
+    [Navigate(NavigateType.OneToMany, nameof(ApptaskItem.ApptaskId))]
+    public List<ApptaskItem> ApptaskItems { get; set; }
+
+    public void CreateName() => Name = $"{TaskType.GetDescription()}-{DateTime.Now:yyyyMMddHHmmssfff}";
+}

+ 180 - 0
src/Hotline/BatchTask/ApptaskDomainService.cs

@@ -0,0 +1,180 @@
+using FluentValidation;
+using Hotline.Share.Dtos.BatchTask;
+using Hotline.Share.Enums.BatchTask;
+using Hotline.Validators.BatchTask;
+using MapsterMapper;
+using Microsoft.Extensions.Logging;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+using static Lucene.Net.Util.Fst.Util;
+
+namespace Hotline.BatchTask;
+
+public class ApptaskDomainService : IApptaskDomainService, IScopeDependency
+{
+    private readonly IRepository<Apptask> _apptaskRepository;
+    private readonly IRepository<ApptaskItem> _apptaskItemRepository;
+    private readonly IMapper _mapper;
+    private readonly ILogger<ApptaskDomainService> _logger;
+
+    public ApptaskDomainService(
+        IRepository<Apptask> apptaskRepository,
+        IRepository<ApptaskItem> apptaskItemRepository,
+        IMapper mapper,
+        ILogger<ApptaskDomainService> logger)
+    {
+        _apptaskRepository = apptaskRepository;
+        _apptaskItemRepository = apptaskItemRepository;
+        _mapper = mapper;
+        _logger = logger;
+    }
+
+    /// <summary>
+    /// 新增任务
+    /// </summary>
+    public async Task<string> AddAsync(AddApptaskRequest request, CancellationToken cancellation)
+    {
+        var validator = new ApptaskValidator();
+        var result = validator.Validate(request);
+        if (!result.IsValid)
+            throw new ValidationException(result.Errors.FirstOrDefault()?.ErrorMessage);
+
+        var apptask = new Apptask
+        {
+            Name = request.Name,
+            Description = request.Description,
+            TaskType = request.TaskType,
+            ApptaskItems = request.ApptaskItems.Select(d => new ApptaskItem
+            {
+                BusinessId = d.BusinessId,
+                TaskType = request.TaskType,
+                TaskStatus = ETaskStatus.Waiting,
+                TaskParams = d.TaskParams is null ? null : System.Text.Json.JsonSerializer.Serialize(d.TaskParams),
+                TryLimit = request.TryLimit,
+                Priority = request.Priority ?? 9,
+            }).ToList()
+        };
+
+        if (string.IsNullOrEmpty(apptask.Name))
+            apptask.CreateName();
+
+        await _apptaskRepository.AddNav(apptask)
+            .Include(d => d.ApptaskItems)
+            .ExecuteCommandAsync();
+
+        return apptask.Id;
+    }
+
+    /// <summary>
+    /// 查询任务进度
+    /// </summary>
+    /// <returns></returns>
+    public async Task<ApptaskProgressDto> GetProgressAsync(string taskId, CancellationToken cancellation)
+    {
+        var apptask = await _apptaskRepository.Queryable()
+            .Includes(d => d.ApptaskItems)
+            .FirstAsync(d => d.Id == taskId, cancellation);
+        if (apptask is null)
+            throw new UserFriendlyException("无效任务编号");
+        return _mapper.Map<ApptaskProgressDto>(apptask);
+    }
+
+    /// <summary>
+    /// 终止任务
+    /// </summary>
+    /// <param name="taskId"></param>
+    /// <param name="cancellation"></param>
+    /// <returns></returns>
+    public async Task TerminalTaskAsync(string taskId, CancellationToken cancellation)
+    {
+        var apptask = await _apptaskRepository.Queryable()
+            .Includes(d => d.ApptaskItems.Where(x => x.Tries < x.TryLimit
+                                                   && (x.TaskStatus == ETaskStatus.Waiting || x.TaskStatus == ETaskStatus.Failed)).ToList())
+            .FirstAsync(d => d.Id == taskId, cancellation);
+        if (apptask is null)
+            throw new UserFriendlyException("无效任务编号");
+        if (apptask.ApptaskItems.Count == 0) return;
+        var Succeed = 0;
+        foreach (var item in apptask.ApptaskItems)
+        {
+            item.TaskStatus = ETaskStatus.Terminated;
+            var result = await _apptaskItemRepository.Updateable(item).ExecuteCommandWithOptLockAsync();
+            if (result != 0) Succeed++;
+        }
+
+        if (Succeed != apptask.ApptaskItems.Count)
+            await TerminalTaskAsync(taskId, cancellation);
+    }
+
+    /// <summary>
+    /// 获取一个待执行的任务
+    /// </summary>
+    /// <param name="cancellation"></param>
+    /// <returns></returns>
+    public async Task<ApptaskItem?> GetWaitingTaskAsync(CancellationToken cancellation)
+    {
+        var taskItems = await _apptaskItemRepository.Queryable()
+            .Where(d => d.Tries < d.TryLimit
+                        && (d.TaskStatus == ETaskStatus.Waiting || d.TaskStatus == ETaskStatus.Failed))
+            .OrderBy(d => d.CreationTime)
+            .Take(10)
+            .ToListAsync(cancellation);
+
+        if (taskItems.Count == 0) return null;
+
+        foreach (var item in taskItems)
+        {
+            item.SetProcessing();
+            item.Tries++;
+            var result = await _apptaskItemRepository.Updateable(item).ExecuteCommandWithOptLockAsync();
+            if (result != 0)
+                return item;
+        }
+
+        return await GetWaitingTaskAsync(cancellation);
+    }
+
+    /// <summary>
+    /// 执行任务
+    /// </summary>
+    /// <param name="executor"></param>
+    /// <param name="apptaskItem"></param>
+    /// <param name="cancellation"></param>
+    /// <returns></returns>
+    public async Task ExecuteAsync<TRequest>(IApptaskExecutor<TRequest> executor, ApptaskItem apptaskItem, CancellationToken cancellation)
+    {
+        try
+        {
+            TRequest request = default;
+            if (!string.IsNullOrEmpty(apptaskItem.TaskParams))
+            {
+                request = System.Text.Json.JsonSerializer.Deserialize<TRequest>(apptaskItem.TaskParams);
+                if (request is null)
+                    throw new UserFriendlyException("任务参数反序列化异常");
+            }
+            var result = await executor.ExecuteAsync(request, cancellation);
+            apptaskItem.TaskStatus = result.IsSuccess ? ETaskStatus.Succeeded : ETaskStatus.Failed;
+            apptaskItem.Message = result.Message;
+            apptaskItem.TaskEndTime = DateTime.Now;
+        }
+        catch (Exception e)
+        {
+            _logger.LogError("批量任务执行异常:{err}", e.Message);
+            apptaskItem.TaskStatus = ETaskStatus.Failed;
+            apptaskItem.Message = "批量任务执行异常";
+            apptaskItem.TaskEndTime = DateTime.Now;
+        }
+        finally
+        {
+            await _apptaskItemRepository.Updateable(apptaskItem)
+                .UpdateColumns(d => new
+                {
+                    d.TaskStatus,
+                    d.Message,
+                    d.TaskEndTime
+                })
+                .ExecuteCommandAsync(cancellation);
+        }
+    }
+}

+ 71 - 0
src/Hotline/BatchTask/ApptaskItem.cs

@@ -0,0 +1,71 @@
+using Hotline.Share.Enums.BatchTask;
+using SqlSugar;
+using XF.Domain.Repository;
+
+namespace Hotline.BatchTask;
+
+[SugarIndex("index_apptaskitem_creationtime", nameof(ApptaskItem.CreationTime), OrderByType.Asc)]
+public class ApptaskItem : CreationEntity
+{
+    /// <summary>
+    /// 任务id
+    /// </summary>
+    public string ApptaskId { get; set; }
+
+    /// <summary>
+    /// 业务id(冗余)
+    /// </summary>
+    public string BusinessId { get; set; }
+
+    /// <summary>
+    /// 任务类型(冗余)
+    /// </summary>
+    public ETaskType TaskType { get; set; }
+
+    /// <summary>
+    /// 任务状态
+    /// </summary>
+    public ETaskStatus TaskStatus { get; set; }
+
+    /// <summary>
+    /// 优先级
+    /// 由高到低(0-9)
+    /// </summary>
+    public int Priority { get; set; } = 9;
+
+    /// <summary>
+    /// 任务执行时间
+    /// </summary>
+    public DateTime? TaskStartTime { get; set; }
+    public DateTime? TaskEndTime { get; set; }
+
+    /// <summary>
+    /// 参数
+    /// </summary>
+    [SugarColumn(ColumnDataType = "varchar(8000)")]
+    public string? TaskParams { get; set; }
+
+    /// <summary>
+    /// 执行次数
+    /// </summary>
+    public int Tries { get; set; }
+
+    /// <summary>
+    /// 执行次数上限
+    /// </summary>
+    public int TryLimit { get; set; } = 1;
+
+    public string? Message { get; set; }
+
+    [SugarColumn(IsEnableUpdateVersionValidation = true)]
+    public string Ver { get; set; } = Guid.NewGuid().ToString();
+
+    [Navigate(NavigateType.OneToOne, nameof(ApptaskId))]
+    public Apptask Apptask { get; set; }
+
+    public void SetProcessing()
+    {
+        TaskStatus = ETaskStatus.Processing;
+        TaskStartTime = DateTime.Now;
+    }
+}

+ 48 - 0
src/Hotline/BatchTask/IApptaskDomainService.cs

@@ -0,0 +1,48 @@
+using Hotline.Share.Dtos.BatchTask;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.BatchTask
+{
+    public interface IApptaskDomainService
+    {
+        /// <summary>
+        /// 新增任务
+        /// </summary>
+        Task<string> AddAsync(AddApptaskRequest request, CancellationToken cancellation);
+
+        /// <summary>
+        /// 查询任务进度
+        /// </summary>
+        /// <returns></returns>
+        Task<ApptaskProgressDto> GetProgressAsync(string taskId, CancellationToken cancellation);
+
+        /// <summary>
+        /// 终止任务
+        /// </summary>
+        /// <param name="taskId"></param>
+        /// <param name="cancellation"></param>
+        /// <returns></returns>
+        Task TerminalTaskAsync(string taskId, CancellationToken cancellation);
+
+        /*job request*/
+        /// <summary>
+        /// 获取一个待执行的任务
+        /// </summary>
+        /// <param name="cancellation"></param>
+        /// <returns></returns>
+        Task<ApptaskItem?> GetWaitingTaskAsync(CancellationToken cancellation);
+
+        /// <summary>
+        /// 执行任务
+        /// </summary>
+        /// <param name="executor"></param>
+        /// <param name="apptaskItem"></param>
+        /// <param name="cancellation"></param>
+        /// <returns></returns>
+        Task ExecuteAsync<TRequest>(IApptaskExecutor<TRequest> executor, ApptaskItem apptaskItem, CancellationToken cancellation);
+    }
+}

+ 19 - 0
src/Hotline/BatchTask/IApptaskExecutor.cs

@@ -0,0 +1,19 @@
+namespace Hotline.BatchTask;
+
+public interface IApptaskExecutor<TRequest>
+{
+    /// <summary>
+    /// 执行任务
+    /// </summary>
+    /// <param name="request"></param>
+    /// <param name="cancellation"></param>
+    /// <returns>是否成功执行</returns>
+    Task<ApptaskExecuteResult> ExecuteAsync(TRequest? request, CancellationToken cancellation);
+}
+
+
+public class ApptaskExecuteResult
+{
+    public bool IsSuccess { get; set; }
+    public string? Message { get; set; }
+}

+ 1 - 0
src/Hotline/CallCenter/Configs/XingTangConfiguration.cs

@@ -3,5 +3,6 @@
     public class XingTangConfiguration
     {
         public string DbConnectionString { get; set; }
+        public string Address { get; set; }
     }
 }

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

@@ -171,23 +171,12 @@ namespace Hotline.FlowEngine.Workflows
             bool isOrderFiled, DateTime? expiredTime, EHandleMode handleMode, EFlowAssignType? flowAssignType = EFlowAssignType.User,
             CancellationToken cancellationToken = default);
 
-        ///// <summary>
-        ///// 跳转(直接将流程跳转至任意节点)
-        ///// </summary>
-        //Task JumpAsync(Workflow workflow, RecallDto dto, StepDefine targetStepDefine, FlowAssignInfo flowAssignInfo,
-        //    CancellationToken cancellationToken);
-
-        ///// <summary>
-        ///// 重办
-        ///// </summary>
-        //Task RedoAsync(Workflow workflow, RecallDto dto, StepDefine targetStepDefine, FlowAssignInfo flowAssignInfo,
-        //    CancellationToken cancellationToken);
-
         ///// <summary>
         ///// 否决(审批流程不通过)
         ///// </summary>
         ///// <returns></returns>
         //Task RejectAsync(Workflow workflow, BasicWorkflowDto dto, CancellationToken cancellationToken);
+        Task RejectAsync(RejectDto dto, CancellationToken cancellationToken);
 
         /// <summary>
         /// 补充

+ 40 - 13
src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs

@@ -1317,20 +1317,30 @@ namespace Hotline.FlowEngine.Workflows
         public async Task UpdateUnhandleExpiredTimeAsync(string workflowId, DateTime? expiredTime,
             CancellationToken cancellation)
         {
-            var steps = await _workflowStepRepository.Queryable()
-                .Includes(d => d.WorkflowTrace)
-                .Where(d => d.WorkflowId == workflowId &&
-                            d.Status < EWorkflowStepStatus.Handled)
-                .ToListAsync(cancellation);
-            foreach (var step in steps)
-            {
-                step.StepExpiredTime = expiredTime;
-                step.WorkflowTrace.StepExpiredTime = expiredTime;
-            }
+            //var steps = await _workflowStepRepository.Queryable()
+            //    .Includes(d => d.WorkflowTrace)
+            //    .Where(d => d.WorkflowId == workflowId &&
+            //                d.Status < EWorkflowStepStatus.Handled)
+            //    .ToListAsync(cancellation);
+            //foreach (var step in steps)
+            //{
+            //    step.StepExpiredTime = expiredTime;
+            //    step.WorkflowTrace.StepExpiredTime = expiredTime;
+            //}
 
-            await _workflowStepRepository.UpdateNav(steps)
-                .Include(d => d.WorkflowTrace)
-                .ExecuteCommandAsync();
+            //await _workflowStepRepository.UpdateNav(steps)
+            //    .Include(d => d.WorkflowTrace)
+            //    .ExecuteCommandAsync();
+
+            await _workflowStepRepository.Updateable()
+                .SetColumns(d => d.StepExpiredTime == expiredTime)
+                .Where(d => d.WorkflowId == workflowId && d.Status < EWorkflowStepStatus.Handled)
+                .ExecuteCommandAsync(cancellation);
+
+            await _workflowTraceRepository.Updateable()
+                .SetColumns(d => d.StepExpiredTime == expiredTime)
+                .Where(d => d.WorkflowId == workflowId && d.Status < EWorkflowStepStatus.Handled)
+                .ExecuteCommandAsync(cancellation);
         }
 
         /// <summary>
@@ -2056,6 +2066,23 @@ namespace Hotline.FlowEngine.Workflows
             return (new(isPaiDan, workflow));
         }
 
+        public async Task RejectAsync(RejectDto dto, CancellationToken cancellationToken)
+        {
+            var workflow = await GetWorkflowAsync(dto.WorkflowId, withDefine: true,
+                cancellationToken: cancellationToken);
+            
+            var endStepDefine = workflow.WorkflowDefinition.FindEndStepDefine();
+            var nextDto = _mapper.Map<NextWorkflowDto>(dto);
+            nextDto.ReviewResult = EReviewResult.Failed;
+            nextDto.NextStepCode = endStepDefine.Code;
+            nextDto.NextStepName = endStepDefine.Name;
+            nextDto.FlowDirection = _sessionContext.OrgIsCenter
+                ? EFlowDirection.CenterToFile
+                : EFlowDirection.OrgToFile;
+
+            await NextAsync(nextDto, cancellationToken: cancellationToken);
+        }
+
         /// <summary>
         /// 补充
         /// </summary>

+ 5 - 1
src/Hotline/Orders/OrderDelay.cs

@@ -148,5 +148,9 @@ namespace Hotline.Orders
 		[SugarColumn(DefaultValue = "0")]
 		public int? AutomaticDelayNum { get; set; }
 
-	}
+
+        [Navigate(NavigateType.OneToMany, nameof(WorkflowStep.ExternalId))]
+        public List<WorkflowStep> WorkflowSteps { get; set; }
+
+    }
 }

+ 39 - 0
src/Hotline/Validators/BatchTask/ApptaskValidator.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FluentValidation;
+using Hotline.BatchTask;
+using Hotline.Share.Dtos.BatchTask;
+
+namespace Hotline.Validators.BatchTask;
+
+public class ApptaskValidator : AbstractValidator<AddApptaskRequest>
+{
+    public ApptaskValidator()
+    {
+        RuleFor(d => d.TaskType)
+            .Cascade(CascadeMode.Stop)
+            .NotEmpty()
+            .IsInEnum()
+            .WithMessage("任务类型不能为空");
+
+        RuleFor(d => d.ApptaskItems)
+            .NotEmpty()
+            .WithMessage("任务明细不能为空");
+
+        RuleForEach(d => d.ApptaskItems)
+            .SetValidator(new ApptaskItemValidator());
+    }
+}
+
+public class ApptaskItemValidator : AbstractValidator<AddApptaskItemRequest>
+{
+    public ApptaskItemValidator()
+    {
+        RuleFor(d => d.BusinessId)
+            .NotEmpty()
+            .WithMessage("业务id不能为空");
+    }
+}

+ 1 - 1
test/Hotline.Tests/Application/OrderVisitApplicationTest.cs

@@ -1,4 +1,4 @@
-using Hotline.Application.OrderApp;
+using Hotline.Application.OrderApp.OrderVisitApp;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Tools;
 using Shouldly;