Pārlūkot izejas kodu

Merge branch 'test' into lib/test

libin 2 nedēļas atpakaļ
vecāks
revīzija
5421aa38f1
40 mainītis faili ar 1295 papildinājumiem un 202 dzēšanām
  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. 11 9
      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. 5 0
      src/Hotline.Share/Requests/PagedKeywordRequest.cs
  29. 33 0
      src/Hotline/BatchTask/Apptask.cs
  30. 180 0
      src/Hotline/BatchTask/ApptaskDomainService.cs
  31. 71 0
      src/Hotline/BatchTask/ApptaskItem.cs
  32. 48 0
      src/Hotline/BatchTask/IApptaskDomainService.cs
  33. 19 0
      src/Hotline/BatchTask/IApptaskExecutor.cs
  34. 1 0
      src/Hotline/CallCenter/Configs/XingTangConfiguration.cs
  35. 1 12
      src/Hotline/FlowEngine/Workflows/IWorkflowDomainService.cs
  36. 40 13
      src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs
  37. 5 1
      src/Hotline/Orders/OrderDelay.cs
  38. 39 0
      src/Hotline/Validators/BatchTask/ApptaskValidator.cs
  39. 1 1
      test/Hotline.Tests/Application/OrderVisitApplicationTest.cs
  40. 5 1
      test/Hotline.Tests/Application/SnapshotApplicationTest.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)

+ 11 - 9
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;
@@ -985,7 +986,8 @@ public class OrderApplication : IOrderApplication, IScopeDependency
                 d.Order.SourceChannelCode == "SZMHD" && d.Order.IsProvince == true) //政民互动
             .WhereIF(!string.IsNullOrEmpty(dto.ProvinceChannel) && dto.ProvinceChannel == "3", d => d.Order.Source == ESource.ProvinceStraight &&
                 d.Order.SourceChannelCode == "S12345" && d.Order.IsProvince == true) //省12345
-            .OrderByIF(string.IsNullOrEmpty(dto.SortField), d => d.CreationTime, OrderByType.Desc)
+            .WhereIF(dto.PublishState.HasValue , d=>d.PublishState == dto.PublishState.Value) //发布范围 是否公开
+			.OrderByIF(string.IsNullOrEmpty(dto.SortField), d => d.CreationTime, OrderByType.Desc)
             .OrderByIF(dto is { SortRule: 0, SortField: "creationTime" }, d => d.Order.CreationTime, OrderByType.Asc)   //受理时间
             .OrderByIF(dto is { SortRule: 1, SortField: "creationTime" }, d => d.Order.CreationTime, OrderByType.Desc)
             .OrderByIF(dto is { SortRule: 0, SortField: "filedTime" }, d => d.Order.FiledTime, OrderByType.Asc)  //办结时间
@@ -4562,14 +4564,14 @@ public class OrderApplication : IOrderApplication, IScopeDependency
 
             .OrderByIF(_appOptions.Value.IsYiBin && dto.VisitStateQuery != EVisitStateQuery.Visited, d => d.Order.IsUrgent, OrderByType.Desc)
             .OrderByIF(_appOptions.Value.IsZiGong == false, d => d.PublishTime, OrderByType.Desc)
-            .OrderByIF(dto is { SortField: "publishTime", SortRule: 0 }, x => x.PublishTime, OrderByType.Asc) // 发布时间升序
-            .OrderByIF(dto is { SortField: "publishTime", SortRule: 1 }, x => x.PublishTime, OrderByType.Desc)// 发布时间升序
-            .OrderByIF(dto is { SortField: "order.creationTime", SortRule: 0 }, x => x.Order.CreationTime, OrderByType.Asc) // 受理时间升序
-            .OrderByIF(dto is { SortField: "order.creationTime", SortRule: 1 }, x => x.Order.CreationTime, OrderByType.Desc) // 受理时间升序
-            .OrderByIF(dto is { SortField: "order.filedTime", SortRule: 0 }, x => x.Order.FiledTime, OrderByType.Asc) // 办结时间升序
-            .OrderByIF(dto is { SortField: "order.filedTime", SortRule: 1 }, x => x.Order.FiledTime, OrderByType.Desc) // 办结时间升序
-            .OrderByIF(dto is { SortField: "visitTime", SortRule: 0 }, x => x.VisitTime, OrderByType.Asc) // 回访时间升序
-            .OrderByIF(dto is { SortField: "visitTime", SortRule: 1 }, x => x.VisitTime, OrderByType.Desc) // 回访时间升序
+            .OrderByIF(dto is { SortField: "publishTime", SortRule: 0 }, d => d.PublishTime, OrderByType.Asc) // 发布时间升序 
+            .OrderByIF(dto is { SortField: "publishTime", SortRule: 1 }, d => d.PublishTime, OrderByType.Desc)// 发布时间升序
+            .OrderByIF(dto is { SortField: "order.creationTime", SortRule: 0 }, d => d.Order.CreationTime, OrderByType.Asc) // 受理时间升序
+            .OrderByIF(dto is { SortField: "order.creationTime", SortRule: 1 }, d => d.Order.CreationTime, OrderByType.Desc) // 受理时间升序
+            .OrderByIF(dto is { SortField: "order.filedTime", SortRule: 0 }, d => d.Order.FiledTime, OrderByType.Asc) // 办结时间升序
+            .OrderByIF(dto is { SortField: "order.filedTime", SortRule: 1 }, d => d.Order.FiledTime, OrderByType.Desc) // 办结时间升序
+            .OrderByIF(dto is { SortField: "visitTime", SortRule: 0 }, d => d.VisitTime, OrderByType.Asc) // 回访时间升序
+            .OrderByIF(dto is { SortField: "visitTime", SortRule: 1 }, d => d.VisitTime, OrderByType.Desc) // 回访时间升序
             .OrderByIF(_appOptions.Value.IsZiGong && string.IsNullOrEmpty(dto.SortField), d => d.PublishTime, OrderByType.Desc)
             ;
         return query;

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

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

@@ -318,6 +318,11 @@ public record PublishedPagedRequest : PagedKeywordRequest
     /// 省来源分类 1:政民互动直派 2:政民互动  3:省12345
     /// </summary>
     public string? ProvinceChannel { get; set; }
+
+	/// <summary>
+	/// 发布范围 是否公开
+	/// </summary>
+	public bool? PublishState { get; set; }
 }
 
 public record HotspotSubtotalReportPagedRequest : ReportPagedRequest

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

+ 5 - 1
test/Hotline.Tests/Application/SnapshotApplicationTest.cs

@@ -8,6 +8,7 @@ using Hotline.Identity.Accounts;
 using Hotline.Identity.Roles;
 using Hotline.Orders;
 using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Repository.SqlSugar.Snapshot;
 using Hotline.Settings;
 using Hotline.Share.Dtos.Article;
 using Hotline.Share.Dtos.Snapshot;
@@ -41,6 +42,7 @@ public class SnapshotApplicationTest : TestBase
     private readonly IIndustryRepository _industryRepository;
     private readonly IFileRepository _fileRepository;
     private readonly OrderServiceMock _orderServiceMock;
+    private readonly IInviteCodeRecordRepository _inviteCodeRecordRepository;
     private readonly IOrderRepository _orderRepository;
     private readonly IOrderSnapshotRepository _orderSnapshotRepository;
     private readonly ISessionContext _sessionContext;
@@ -52,7 +54,7 @@ public class SnapshotApplicationTest : TestBase
     private readonly IOrderSnapshotApplication _orderSnapshotApplication;
     private readonly ICitizenRepository _citizenRepository;
 
-    public SnapshotApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, ISnapshotApplication snapshotApplication, IIdentityAppService identityAppService, IRepository<RedPackRecord> redPackRecordRepository, IIndustryApplication industryApplication, IIndustryRepository industryRepository, IFileRepository fileRepository, OrderServiceMock orderServiceMock, IOrderRepository orderRepository, IOrderSnapshotRepository orderSnapshotRepository, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount, ISessionContext sessionContext, IGuiderSystemService guiderSystemService, ISystemSettingCacheManager systemSettingCacheManager, ICommunityInfoRepository communityInfoRepository, IIndustryLogRepository industryLogRepository, IRedPackApplication redPackApplication, IOrderSnapshotApplication orderSnapshotApplication, ITypedCache<SystemSetting> cacheSettingData, ThirdAccounSupplierFactory thirdAccountDomainFactory, ICitizenRepository citizenRepository, IServiceProvider serviceProvider) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount, cacheSettingData, thirdAccountDomainFactory, serviceProvider)
+    public SnapshotApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, ISnapshotApplication snapshotApplication, IIdentityAppService identityAppService, IRepository<RedPackRecord> redPackRecordRepository, IIndustryApplication industryApplication, IIndustryRepository industryRepository, IFileRepository fileRepository, OrderServiceMock orderServiceMock, IOrderRepository orderRepository, IOrderSnapshotRepository orderSnapshotRepository, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount, ISessionContext sessionContext, IGuiderSystemService guiderSystemService, ISystemSettingCacheManager systemSettingCacheManager, ICommunityInfoRepository communityInfoRepository, IIndustryLogRepository industryLogRepository, IRedPackApplication redPackApplication, IOrderSnapshotApplication orderSnapshotApplication, ITypedCache<SystemSetting> cacheSettingData, ThirdAccounSupplierFactory thirdAccountDomainFactory, ICitizenRepository citizenRepository, IServiceProvider serviceProvider, IInviteCodeRecordRepository inviteCodeRecordRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount, cacheSettingData, thirdAccountDomainFactory, serviceProvider)
     {
         _snapshotApplication = snapshotApplication;
         _identityAppService = identityAppService;
@@ -72,6 +74,7 @@ public class SnapshotApplicationTest : TestBase
         _redPackApplication = redPackApplication;
         _orderSnapshotApplication = orderSnapshotApplication;
         _citizenRepository = citizenRepository;
+        _inviteCodeRecordRepository = inviteCodeRecordRepository;
     }
 
     /// <summary>
@@ -376,6 +379,7 @@ public class SnapshotApplicationTest : TestBase
     public async Task SaveInvitationCode_Test()
     {
         var code = new Random().Next(100, 200).ToString();
+        await _inviteCodeRecordRepository.RemoveAsync(m => m.InviteCode == code);
         try
         {
             await _citizenRepository.Updateable()