Sfoglia il codice sorgente

Merge branch 'test' into test_Guardian

田爽 2 settimane fa
parent
commit
9634fbcbbb
61 ha cambiato i file con 1820 aggiunte e 284 eliminazioni
  1. 2 1
      src/Hotline.Api/Controllers/Bi/BiOrderController.cs
  2. 2 2
      src/Hotline.Api/Controllers/Exam/QuestionController.cs
  3. 12 0
      src/Hotline.Api/Controllers/Exam/UserExamController.cs
  4. 11 2
      src/Hotline.Api/Controllers/IPPbxController.cs
  5. 45 0
      src/Hotline.Api/Controllers/OrderApi/OrderDelayController.cs
  6. 24 0
      src/Hotline.Api/Controllers/OrderApi/OrderVisitController.cs
  7. 1 0
      src/Hotline.Api/Controllers/OrderController.cs
  8. 33 3
      src/Hotline.Api/Controllers/TestController.cs
  9. 1 1
      src/Hotline.Api/StartupExtensions.cs
  10. 20 13
      src/Hotline.Api/StartupHelper.cs
  11. 13 12
      src/Hotline.Api/config/appsettings.Development.json
  12. 14 0
      src/Hotline.Application/Exam/Interface/ExamManages/IUserExamService.cs
  13. 12 1
      src/Hotline.Application/Exam/QueryExtensions/ExamManages/ExamQuestionQueryExtensions.cs
  14. 188 5
      src/Hotline.Application/Exam/Service/ExamManages/UserExamService.cs
  15. 19 16
      src/Hotline.Application/Exam/Service/Trains/TrainRecordService.cs
  16. 53 0
      src/Hotline.Application/Jobs/ApptaskJob.cs
  17. 32 0
      src/Hotline.Application/Mappers/BatchTaskMapperConfigs.cs
  18. 12 1
      src/Hotline.Application/Mappers/OrderMapperConfigs.cs
  19. 46 46
      src/Hotline.Application/OrderApp/OrderApplication.cs
  20. 23 0
      src/Hotline.Application/OrderApp/OrderDelayApp/IOrderDelayApplication.cs
  21. 216 0
      src/Hotline.Application/OrderApp/OrderDelayApp/OrderDelayApplication.cs
  22. 20 0
      src/Hotline.Application/OrderApp/OrderDelayApp/OrderDelayAuditTaskExecutor.cs
  23. 8 1
      src/Hotline.Application/OrderApp/OrderVisitApp/IOrderVisitApplication.cs
  24. 49 2
      src/Hotline.Application/OrderApp/OrderVisitApp/OrderVisitApplication.cs
  25. 67 0
      src/Hotline.Application/OrderApp/OrderVisitApp/VoiceVisitTaskExecutor.cs
  26. 2 1
      src/Hotline.Repository.SqlSugar/CallCenter/TrCallRecordRepository.cs
  27. 50 0
      src/Hotline.Share/Dtos/BatchTask/AddApptaskRequest.cs
  28. 13 0
      src/Hotline.Share/Dtos/BatchTask/ApptaskProgressDto.cs
  29. 2 0
      src/Hotline.Share/Dtos/CallCenter/BiQueryCallsDto.cs
  30. 45 0
      src/Hotline.Share/Dtos/ExamManages/ExamQuestionDto.cs
  31. 138 0
      src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayDto.cs
  32. 33 0
      src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayReviewRequest.cs
  33. 6 0
      src/Hotline.Share/Dtos/Order/OrderDelay/OrderDelayStepIdDto.cs
  34. 1 0
      src/Hotline.Share/Dtos/Order/OrderDto.cs
  35. 1 0
      src/Hotline.Share/Dtos/Order/OrderStartFlowDto.cs
  36. 14 0
      src/Hotline.Share/Dtos/Order/OrderVisit/BatchVoiceVisitRequest.cs
  37. 7 0
      src/Hotline.Share/Dtos/Order/OrderVisit/VoiceVisitRequest.cs
  38. 0 132
      src/Hotline.Share/Dtos/Order/QueryOrderDto.cs
  39. 13 0
      src/Hotline.Share/Dtos/Questions/QuestionAnswerDto.cs
  40. 9 0
      src/Hotline.Share/Dtos/Questions/QuestionOptionsDto.cs
  41. 1 1
      src/Hotline.Share/Dtos/Questions/QuestionSourcewareDto.cs
  42. 13 0
      src/Hotline.Share/Enums/BatchTask/ETaskStatus.cs
  43. 27 0
      src/Hotline.Share/Enums/BatchTask/ETaskType.cs
  44. 3 3
      src/Hotline.Share/Requests/Exam/ExamQuestionRequest.cs
  45. 12 0
      src/Hotline.Share/ViewResponses/Exam/UserExamResultViewResponse.cs
  46. 33 0
      src/Hotline/BatchTask/Apptask.cs
  47. 180 0
      src/Hotline/BatchTask/ApptaskDomainService.cs
  48. 71 0
      src/Hotline/BatchTask/ApptaskItem.cs
  49. 48 0
      src/Hotline/BatchTask/IApptaskDomainService.cs
  50. 19 0
      src/Hotline/BatchTask/IApptaskExecutor.cs
  51. 1 0
      src/Hotline/CallCenter/Configs/XingTangConfiguration.cs
  52. 1 12
      src/Hotline/FlowEngine/Workflows/IWorkflowDomainService.cs
  53. 40 13
      src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs
  54. 9 1
      src/Hotline/Orders/IOrderVisitDomainService.cs
  55. 5 1
      src/Hotline/Orders/OrderDelay.cs
  56. 31 12
      src/Hotline/Orders/OrderVisitDomainService.cs
  57. 39 0
      src/Hotline/Validators/BatchTask/ApptaskValidator.cs
  58. 21 0
      src/Hotline/Validators/Order/OrderVisit/BatchVoiceVisitRequestValidator.cs
  59. 1 1
      test/Hotline.Tests/Application/OrderVisitApplicationTest.cs
  60. 5 1
      test/Hotline.Tests/Application/SnapshotApplicationTest.cs
  61. 3 0
      test/Hotline.Tests/Domain/OrderVisitDomainServiceTest.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
 {

+ 2 - 2
src/Hotline.Api/Controllers/Exam/QuestionController.cs

@@ -117,9 +117,9 @@ namespace Hotline.Api.Controllers.Exam
         /// </summary>
         /// <returns></returns>
         [HttpPost(ExamManageApiRoute.ImportExcel)]
-        public async Task ImportExcel(IFormFile files)
+        public async Task ImportExcel(IFormFile file)
         {
-            await _questionService.ImportExcel(files, HttpContext.RequestAborted);
+            await _questionService.ImportExcel(file, HttpContext.RequestAborted);
         }
 
         /// <summary>

+ 12 - 0
src/Hotline.Api/Controllers/Exam/UserExamController.cs

@@ -199,5 +199,17 @@ namespace Hotline.Api.Controllers.Exam
 
             return await _userExamService.GetUserExamResults(userExamResultReportPagedRequest); 
         }
+
+        /// <summary>
+        /// 查看考试试题
+        /// </summary>
+        /// <param name="id"></param>
+        /// <param name="questionId"></param>
+        /// <returns></returns>
+        [HttpGet(UserExamApiRoute.View)]
+        public async Task<ViewExamQuestionDto> View([FromQuery] string id,string questionId)
+        {
+            return await _userExamService.View(id, questionId);
+        }
     }
 }

+ 11 - 2
src/Hotline.Api/Controllers/IPPbxController.cs

@@ -74,6 +74,7 @@ namespace Hotline.Api.Controllers
         private readonly IRepository<TelOperation> _telOperationRepository;
         private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
         private readonly IRepository<VoiceEvaluationButton> _voiceEvaluationButtonRepository;
+        private readonly IOrderVisitDomainService _orderVisitDomainService;
 
         public IPPbxController(IMapper mapper, IUserDomainService userDomainService,
             ISessionContext sessionContext, IRepository<TrCallRecord> trCallRecordRepository,
@@ -94,7 +95,8 @@ namespace Hotline.Api.Controllers
             ICallTelClient callTelClient,
             IRepository<TelOperation> telOperationRepository,
            IOptionsSnapshot<AppConfiguration> appOptions,
-           IRepository<VoiceEvaluationButton> voiceEvaluationButtonRepository)
+           IRepository<VoiceEvaluationButton> voiceEvaluationButtonRepository,
+           IOrderVisitDomainService orderVisitDomainService)
         {
             _mapper = mapper;
             _userDomainService = userDomainService;
@@ -125,6 +127,7 @@ namespace Hotline.Api.Controllers
             _telOperationRepository = telOperationRepository;
             _appOptions = appOptions;
             _voiceEvaluationButtonRepository = voiceEvaluationButtonRepository;
+            _orderVisitDomainService = orderVisitDomainService;
         }
 
         #region 添添呼
@@ -708,7 +711,13 @@ namespace Hotline.Api.Controllers
         {
             var data = _mapper.Map<VoiceEvaluationButton>(dto);
             await _voiceEvaluationButtonRepository.AddAsync(data, HttpContext.RequestAborted);
-
+            try
+            {
+                await _orderVisitDomainService.UpdateVoiceReplyAsync(data);
+            }
+            catch (Exception)
+            {
+            }
         }
 
         #endregion

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

+ 24 - 0
src/Hotline.Api/Controllers/OrderApi/OrderVisitController.cs

@@ -0,0 +1,24 @@
+using Hotline.Application.OrderApp.OrderVisitApp;
+using Hotline.BatchTask;
+using Hotline.Share.Dtos.Order.OrderVisit;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hotline.Api.Controllers.OrderApi;
+
+public class OrderVisitController : BaseController
+{
+    private readonly IOrderVisitApplication _orderVisitApplication;
+
+    public OrderVisitController(IOrderVisitApplication orderVisitApplication)
+    {
+        _orderVisitApplication = orderVisitApplication;
+    }
+
+    /// <summary>
+    /// 批量语音回访
+    /// </summary>
+    /// <returns></returns>
+    [HttpPost("batch-voice")]
+    public Task BatchVoiceVisit([FromBody] BatchVoiceVisitRequest request) => 
+        _orderVisitApplication.AddTaskBatchVoiceVisitAsync(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;

+ 33 - 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,10 @@ 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;
+using Hotline.Share.Dtos.Order.OrderVisit;
 
 namespace Hotline.Api.Controllers;
 
@@ -138,6 +143,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 +202,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 +259,7 @@ public class TestController : BaseController
         _orderSecondaryHandlingRepository = orderSecondaryHandlingRepository;
         _messageRepository = messageRepository;
         _schedulingRepository = schedulingRepository;
+        _apptaskDomainService = apptaskDomainService;
     }
     /// <summary>
     /// 测试
@@ -1602,5 +1609,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": {

+ 14 - 0
src/Hotline.Application/Exam/Interface/ExamManages/IUserExamService.cs

@@ -117,5 +117,19 @@ namespace Hotline.Application.Exam.Interface.ExamManages
         /// <param name="examUserQueryRequest"></param>
         /// <returns></returns>
         Task<List<ExamUserViewResponse>> GetUserListAsync(ExamUserQueryRequest examUserQueryRequest);
+        
+        /// <summary>
+        /// 查看考试
+        /// </summary>
+        /// <param name="id"></param>
+        /// <returns></returns>
+        Task<ViewExamQuestionDto> View(string id,string questionId);
+
+        /// <summary>
+        /// 查看考试
+        /// </summary>
+        /// <param name="id"></param>
+        /// <returns></returns>
+        Task<List<ViewExamQuestionDto>> View(string id);
     }
 }

+ 12 - 1
src/Hotline.Application/Exam/QueryExtensions/ExamManages/ExamQuestionQueryExtensions.cs

@@ -30,7 +30,18 @@ namespace Hotline.Application.Exam.QueryExtensions.ExamManages
 
             expression = ExpressionableUtility.CreateExpression<ExamQuestionBak>()
                 .AndIF(examQuestionRequest.QuestionId.IsNotNullOrEmpty(), x => x.QuestionId == examQuestionRequest.QuestionId)
-                .AndIF(examQuestionRequest.ExamId.IsNotNullOrEmpty(), x => x.ExamId == examQuestionRequest.ExamId)
+                .ToExpression();
+
+            return expression;
+        }
+
+        public static Expression<Func<ExamUserExam, bool>> GetUserExamExpression(this ExamQuestionRequest examQuestionRequest)
+        {
+            Expression<Func<ExamUserExam, bool>> expression = m => m.Id != null;
+
+            expression = ExpressionableUtility.CreateExpression<ExamUserExam>()
+                .AndIF(examQuestionRequest.UserExamId.IsNotNullOrEmpty(), x => x.Id == examQuestionRequest.UserExamId
+                )
                 .ToExpression();
 
             return expression;

+ 188 - 5
src/Hotline.Application/Exam/Service/ExamManages/UserExamService.cs

@@ -43,6 +43,9 @@ using Hotline.Share.Enums.Exams;
 using DocumentFormat.OpenXml.Wordprocessing;
 using Hotline.Repository.SqlSugar.Exam.Repositories.ExamManages;
 using Hotline.Exams.Questions;
+using Hotline.Exams.Sourcewares;
+using Hotline.Share.Dtos.Questions;
+using DocumentFormat.OpenXml.Office2010.Excel;
 
 namespace Hotline.Application.Exam.Service.ExamManages
 {
@@ -91,13 +94,17 @@ namespace Hotline.Application.Exam.Service.ExamManages
         public async Task<ExamQuestionDto> GetExamQuestionDto(ExamQuestionRequest examQuestionRequest)
         {
             var expression = examQuestionRequest.GetExpression();
-            var question = await new ExamRepository<Exams.ExamManages.ExamQuestionBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable().Where(expression).FirstAsync();
-
+            var userUserExpression = examQuestionRequest.GetUserExamExpression();
+            //var question = await new ExamRepository<Exams.ExamManages.ExamQuestionBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable().Where(expression).FirstAsync();
+            var querable = new ExamRepository<Exams.ExamManages.ExamQuestionBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable().Where(expression);
+            var userExamTable = new ExamRepository<Exams.ExamManages.ExamUserExam>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable().Where(userUserExpression);
+            querable = querable.InnerJoin(userExamTable, (q, u) => q.ExamId == u.ExamId).Select((q, u) => q);
+            var question = await querable.FirstAsync();
             if (question != null)
             {
                 var examQuestionDto = _mapper.Map<ExamQuestionDto>(question);
 
-                var questionScore = await new ExamRepository<ExamQuestionScoreBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).GetAsync(x=>x.QuestionType == question.QuestionType && x.ExamManageId == examQuestionRequest.ExamId);
+                var questionScore = await new ExamRepository<ExamQuestionScoreBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).GetAsync(x=>x.QuestionType == question.QuestionType && x.ExamManageId == question.ExamId);
 
                 if (questionScore != null)
                 {
@@ -151,7 +158,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             var userExamItemRepository = new ExamRepository<ExamUserExamItem>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
 
             var userExamItemTable = userExamItemRepository.Queryable().Where(i => i.QuestionId == examQuestionRequest.QuestionId);
-            var userExamTable = _repository.Queryable().Where(u => u.UserId == _sessionContext.UserId && u.ExamId == examQuestionRequest.ExamId);
+            var userExamTable = _repository.Queryable().Where(u => u.UserId == _sessionContext.UserId && u.Id == examQuestionRequest.UserExamId);
             var examAnswerTable = examAnswerRepository.Queryable();
 
             var examAnswers = await examAnswerTable.InnerJoin(userExamItemTable, (e, i) => e.UserExamItemId == i.Id)
@@ -681,6 +688,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
         public async Task<UnExamUserPageViewResponse> GetUnExamUsers(UnExamUserReportPagedRequest unExamUserReportPagedRequest)
         {
+            if(unExamUserReportPagedRequest.EndTime == null)
             unExamUserReportPagedRequest.ResolveEndTime();
 
             var examManageRepository = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
@@ -714,6 +722,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
         public async Task<UserExamResultPageViewResponse> GetUserExamResults(UserExamResultReportPagedRequest userExamResultReportPagedRequest)
         {
+            if(userExamResultReportPagedRequest.EndTime == null)
             userExamResultReportPagedRequest.ResolveEndTime();
 
             var examManageRepository = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
@@ -1306,10 +1315,184 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 TimeSpan = e.TimeSpan,
                 ExamType = e.ExamType,
                 ExamId = e.Id,
-                IsCheck = u.IsCheck
+                IsCheck = u.IsCheck,
+                IsReExam = u.IsReExam,
+                CanReExam = SqlFunc.Subqueryable<ExamUserExamItem>().Where(x=>x.UserExamId == u.Id).Count()< e.Count
             });
             return queryable;
         }
+
+        public async Task<List<ViewExamQuestionDto>> View(string id)
+        {
+           
+            List<ViewExamQuestionDto> viewExamQuestionDtos = await GetViewExamQuestion(id,null);
+
+            var questionIds = viewExamQuestionDtos.Select(x => x.QuestionId).ToList();
+
+            List<ViewQuestionAnswerDto> questionAnswers = await GetQuestionAnswers(id, questionIds);
+            List<ViewQuestionOptionDto> questionOptions = await GetQuestionOptions( questionIds);
+            List<QuestionKnowladgeDto> questionKnowladges = await GetQuestionKnowladges( questionIds);
+            List<QuestionSourcewareDto> sourcewares = await GetSourcewares(questionIds);
+
+            viewExamQuestionDtos.ForEach(item =>
+            {
+                item.QuestionKnowladgeDtos = questionKnowladges.Where(x => x.QuestionId == item.QuestionId).ToList();
+                item.QuestionSourcewareDtos = sourcewares.Where(x => x.QuestionId == item.QuestionId).ToList();
+                if (!item.QuestionType.CheckSelectType())
+                {
+                    var questionAnswer = questionAnswers.FirstOrDefault(x => x.QuestionId == item.QuestionId);
+                    item.Answer = questionAnswer?.Answer ?? string.Empty;
+                    item.CorrectAnswer = questionAnswer?.CorrectAnswer ?? string.Empty;
+                }
+                else
+                {
+                    item.QuestionOptions = questionOptions.Where(x => x.QuestionId == item.QuestionId).ToList();
+                    item.CorrectAnswer = string.Join(",", item.QuestionOptions.Where(x => x.IsAnswer).Select(x => x.Label));
+                }
+                
+            });
+
+
+            return viewExamQuestionDtos;
+
+        }
+
+
+
+        public async Task<ViewExamQuestionDto> View(string id, string questionId)
+        {
+            List<ViewExamQuestionDto> viewExamQuestionDtos = await GetViewExamQuestion(id, questionId);
+
+            var questionIds = viewExamQuestionDtos.Select(x => x.QuestionId).ToList();
+
+            List<ViewQuestionAnswerDto> questionAnswers = await GetQuestionAnswers(id, questionIds);
+            List<ViewQuestionOptionDto> questionOptions = await GetQuestionOptions(questionIds);
+            List<QuestionKnowladgeDto> questionKnowladges = await GetQuestionKnowladges(questionIds);
+            List<QuestionSourcewareDto> sourcewares = await GetSourcewares(questionIds);
+
+            viewExamQuestionDtos.ForEach(item =>
+            {
+                item.QuestionKnowladgeDtos = questionKnowladges.Where(x => x.QuestionId == item.QuestionId).ToList();
+                item.QuestionSourcewareDtos = sourcewares.Where(x => x.QuestionId == item.QuestionId).ToList();
+                if (!item.QuestionType.CheckSelectType())
+                {
+                    var questionAnswer = questionAnswers.FirstOrDefault(x => x.QuestionId == item.QuestionId);
+                    item.Answer = questionAnswer?.Answer ?? string.Empty;
+                    item.CorrectAnswer = questionAnswer?.CorrectAnswer ?? string.Empty;
+                }
+                else
+                {
+                    item.QuestionOptions = questionOptions.Where(x => x.QuestionId == item.QuestionId).ToList();
+                    item.CorrectAnswer = string.Join(",", item.QuestionOptions.Where(x => x.IsAnswer).Select(x => x.Label));
+                }
+
+            });
+
+
+            return viewExamQuestionDtos.FirstOrDefault();
+        }
+
+        private  async Task<List<QuestionSourcewareDto>> GetSourcewares(List<string> questionIds)
+        {
+            var questionSourcewareTable = new ExamRepository<ExamQuestionSourcewareBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
+            var sourcewareTable = new ExamRepository<ExamSourceware>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
+
+            var querable = questionSourcewareTable.InnerJoin<ExamQuestionSourceware>((qsb,qs)=>qsb.SourcewareId == qs.Id).InnerJoin(sourcewareTable, (qsb,qs, s) => qs.SourcewareId == s.Id).Select((qsb, qs, s) => new QuestionSourcewareDto
+            {
+                Id = qs.Id,
+                Name = s.Name,
+                QuestionId = qsb.ExamQuestionId,
+                SourcewareId = s.Id,
+                AttachmentId = s.AttachmentId
+            }).MergeTable().Where(x => questionIds.Contains(x.QuestionId));
+
+            return await querable.ToListAsync();
+        }
+
+        private  async Task<List<QuestionKnowladgeDto>> GetQuestionKnowladges(List<string> questionIds)
+        {
+            var questionKnowladgeTable = new ExamRepository<ExamQuestionKnowladgeBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
+            
+
+            return await questionKnowladgeTable.Where(x => questionIds.Contains(x.ExamQuestionId))
+                .InnerJoin<ExamQuestionKnowladge>((kb,k)=>kb.KnowladgeId == k.Id).Select((kb, k) => new QuestionKnowladgeDto
+            {
+                Id = kb.Id,
+                QuestionId = kb.ExamQuestionId,
+                KnowladgeId = k.KnowladgeId,
+                Title = kb.Title
+            }).ToListAsync();
+        }
+
+        private  async Task<List<ViewQuestionOptionDto>> GetQuestionOptions(List<string> questionIds)
+        {
+            var userExamItemOptionTable = _userExamItemOptionRepository.Queryable();
+            var questionOptionTable = new ExamRepository<ExamQuestionOptionsBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
+
+            var querable = questionOptionTable.Where(x => questionIds.Contains(x.ExamQuestionId))
+                .LeftJoin(userExamItemOptionTable, (op, uio) => op.Id == uio.QuestionOptionId)
+                .Select((op, uio) => new ViewQuestionOptionDto
+                {
+                    Content = op.Content,
+                    Id = op.Id,
+                    IsAnswer = op.IsAnswer,
+                    Label = op.Label,
+                    QuestionId = op.ExamQuestionId,
+                    IsSelected = uio.Id != null
+                }).MergeTable().OrderBy(x=>x.Label);
+
+            return await querable.Distinct()
+                .ToListAsync();
+        }
+
+        private  async Task<List<ViewQuestionAnswerDto>> GetQuestionAnswers(string id, List<string> questionIds)
+        {
+            var userExamTable = _repository.Queryable().Where(x => x.Id == id);
+            var userExamItemTable = _userExamItemRepository.Queryable();
+            var examAnswerTable = _examAnswerRepository.Queryable();
+            var questionTable = new ExamRepository<ExamQuestionBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
+            var questionAnswerTable = new ExamRepository<ExamQuestionAnswerBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
+
+            var querable = examAnswerTable
+                .InnerJoin(userExamItemTable, (a, ui) => ui.Id == a.UserExamItemId &&  ui.UserExamId == id)
+                .LeftJoin(questionAnswerTable, (a, ui, qab) => qab.QuestionId == ui.QuestionId  )
+                .LeftJoin<ExamQuestionAnswer>((a, ui, qab ,qa) => qab.QuestionAnswerId == qa.Id )
+                .LeftJoin(questionTable, (a, ui, qab, qa,q)=>qab.QuestionId == q.QuestionId && questionIds.Contains(q.Id))
+                .Select((a, ui, qab, qa,q) => new ViewQuestionAnswerDto
+                {
+                    Answer = a.Answer,
+                    CorrectAnswer = qa.Answer != null ? qa.Answer : string.Empty,
+                    QuestionId = q.Id
+                }).MergeTable();
+
+            return await querable.ToListAsync();
+        }
+
+        private  async Task<List<ViewExamQuestionDto>> GetViewExamQuestion(string id,string questionId)
+        {
+            var userExamTable = _repository.Queryable().Where(x => x.Id == id);
+            var userExamItemTable = _userExamItemRepository.Queryable();
+            var examManageTable = _examManageRepository.Queryable();
+            var questionTable = new ExamRepository<ExamQuestionBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable().WhereIF(!string.IsNullOrEmpty(questionId), x => x.QuestionId == questionId);
+            var questionScoreTable = new ExamRepository<ExamQuestionScoreBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
+
+
+            var querable = userExamItemTable.InnerJoin(userExamTable, (ui, u) => ui.UserExamId == u.Id)
+                            .InnerJoin(questionTable, (ui, u, q) => ui.QuestionId == q.QuestionId && u.ExamId == q.ExamId)
+                            .InnerJoin(examManageTable, (ui, u, q, e) => u.ExamId == e.Id)
+                            .InnerJoin(questionScoreTable, (ui, u, q, e, s) => q.QuestionType == s.QuestionType && s.ExamManageId == e.Id)
+                            .Select((ui, u, q, e, s) => new ViewExamQuestionDto
+                            {
+                                Id = ui.Id,
+                                Score = s.Score,
+                                RealScore = ui.Score,
+                                Title = q.Title,
+                                QuestionType = q.QuestionType,
+                                QuestionId = q.Id,
+                            }).MergeTable().OrderBy(x=>x.QuestionType).OrderBy(x=>x.Id);
+
+            return await querable.Distinct().ToListAsync();
+        }
         #endregion
 
     }

+ 19 - 16
src/Hotline.Application/Exam/Service/Trains/TrainRecordService.cs

@@ -68,7 +68,7 @@ namespace Hotline.Application.Exam.Service.Trains
         public async Task CompleteTrainRecordAsync(CompleteTrainRecordDto completeTrainRecordDto, CancellationToken cancellationToken)
         {
             var trainRecord = await _repository.GetAsync(x => x.Id == completeTrainRecordDto.Id);
-            trainRecord= _mapper.Map<CompleteTrainRecordDto,ExamTrainRecord>(completeTrainRecordDto, trainRecord);
+            trainRecord = _mapper.Map<CompleteTrainRecordDto, ExamTrainRecord>(completeTrainRecordDto, trainRecord);
             trainRecord.ToUpdate(_sessionContext);
 
             await _repository.UpdateWithValidateAsync(trainRecord, cancellationToken);
@@ -80,7 +80,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
             trainKnowladge.ToUpdate(_sessionContext);
 
-            await _trainKnowladgeRepository.UpdateWithValidateAsync(trainKnowladge,cancellationToken);
+            await _trainKnowladgeRepository.UpdateWithValidateAsync(trainKnowladge, cancellationToken);
 
         }
 
@@ -113,12 +113,12 @@ namespace Hotline.Application.Exam.Service.Trains
             SqlSugar.ISugarQueryable<TrainRecordViewResponse> queryResult = QueryResult(queryRequest);
 
             var total = await queryResult.CountAsync();
-            var items = await queryResult.ToPageListAsync(queryRequest.PageIndex,queryRequest.PageSize);
+            var items = await queryResult.ToPageListAsync(queryRequest.PageIndex, queryRequest.PageSize);
 
             return new TrainRecordPageViewResponse
             {
                 Items = items,
-                Pagination = new Pagination(queryRequest.PageIndex,queryRequest.PageSize,total)
+                Pagination = new Pagination(queryRequest.PageIndex, queryRequest.PageSize, total)
             };
 
         }
@@ -127,7 +127,7 @@ namespace Hotline.Application.Exam.Service.Trains
         {
             await GetTrainPracticeKnowladge(addTrainDto, cancellationToken);
 
-            await AddTrainRecordItem(addTrainDto,cancellationToken);
+            await AddTrainRecordItem(addTrainDto, cancellationToken);
 
             await AddTrainRecordAnswer(addTrainDto, cancellationToken);
 
@@ -138,7 +138,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
         private async Task GetTrainPracticeKnowladge(AddTrainDto addTrainDto, CancellationToken cancellationToken)
         {
-            var trainPracticeKnowladeRepository = new TrainPracticeKnowladgeRepository(_uow,_dataPermissionFilterBuilder,_serviceProvider);
+            var trainPracticeKnowladeRepository = new TrainPracticeKnowladgeRepository(_uow, _dataPermissionFilterBuilder, _serviceProvider);
             var trainPracticeKnowladge = await trainPracticeKnowladeRepository.GetAsync(x => x.TrainPracticeId == addTrainDto.TrainPracticeId);
 
             if (trainPracticeKnowladge != null)
@@ -208,6 +208,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
         public async Task<TrainResultPagedViewResponse> AnalysisTrainResult(TrainResultReportPagedRequest trainResultReportPagedRequest)
         {
+            if(trainResultReportPagedRequest.EndTime == null)
             trainResultReportPagedRequest.ResolveEndTime();
 
             var expression = trainResultReportPagedRequest.GetExpression();
@@ -227,7 +228,7 @@ namespace Hotline.Application.Exam.Service.Trains
             var queryResult = trainRecordTable.InnerJoin(trainPlanTable, (r, p) => r.TrainPlanId == p.Id)
                 .InnerJoin(trainPlanTemplateTable, (r, p, pt) => p.Id == pt.TrainPlanId)
                 .InnerJoin(trainTemplateTable, (r, p, pt, t) => pt.TrainTemplateId == t.Id)
-                .InnerJoin(userTable,(r,p,pt,t,u)=>r.UserId == u.Id)
+                .InnerJoin(userTable, (r, p, pt, t, u) => r.UserId == u.Id)
                 .Select((r, p, pt, t, u) => new TrainResultViewResponse
                 {
                     TrainName = t.Name,
@@ -250,7 +251,8 @@ namespace Hotline.Application.Exam.Service.Trains
 
         public async Task<TrainResultRateViewResponse> CalcuteAnalysisRate(TrainResultReportPagedRequest trainResultReportPagedRequest)
         {
-            trainResultReportPagedRequest.ResolveEndTime();
+            if (trainResultReportPagedRequest.EndTime == null)
+                trainResultReportPagedRequest.ResolveEndTime();
 
             var expression = trainResultReportPagedRequest.GetExpression();
             var templateExpression = trainResultReportPagedRequest.GetTemplateExpression();
@@ -278,7 +280,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
             var calcuteRateResult = new TrainResultRateViewResponse
             {
-                Complete = await queryResult.CountAsync(x=>x.IsComplete),
+                Complete = await queryResult.CountAsync(x => x.IsComplete),
                 Trainning = await queryResult.CountAsync(x => !x.IsComplete && x.TrainTime > DateTime.Now),
                 UnComplete = await queryResult.CountAsync(x => !x.IsComplete && x.TrainTime <= DateTime.Now),
             };
@@ -369,8 +371,9 @@ namespace Hotline.Application.Exam.Service.Trains
             var knowledgeTable = knowledgeRepository.Queryable();
 
             var queryable = questionTable.InnerJoin(questionKnowladgeTable, (r, p, tpt, tp, ExamPracticeQuestionKnowledge) => tp.QuestionId == ExamPracticeQuestionKnowledge.QuestionId)
-                .InnerJoin(knowledgeTable, (r, p, tpt, tp, ExamPracticeQuestionKnowledge,kl) => ExamPracticeQuestionKnowledge.KnowladgeId == kl.Id)
-                .GroupBy((r, p, tpt, tp, ExamPracticeQuestionKnowledge, kl) => new {
+                .InnerJoin(knowledgeTable, (r, p, tpt, tp, ExamPracticeQuestionKnowledge, kl) => ExamPracticeQuestionKnowledge.KnowladgeId == kl.Id)
+                .GroupBy((r, p, tpt, tp, ExamPracticeQuestionKnowledge, kl) => new
+                {
                     Title = kl.Title,
                     Id = kl.Id
                 })
@@ -382,7 +385,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
             return await queryable.ToListAsync();
         }
-       
+
 
         private async Task AddTrainRecordAnswer(AddTrainDto addTrainDto, CancellationToken cancellationToken)
         {
@@ -407,7 +410,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
             trainRecordAnswers.ToInsert(_sessionContext);
 
-            await _trainRecordAnswerRepository.AddWithValidateAsync(trainRecordAnswers,cancellationToken);
+            await _trainRecordAnswerRepository.AddWithValidateAsync(trainRecordAnswers, cancellationToken);
         }
 
         //private async Task<TrainPracticeDto> GetTrainQuestionOptions(AddTrainDto addTrainDto)
@@ -442,7 +445,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
             var trainRecordTable = _repository.Queryable().Where(x => x.Id == trainPracticeRequest.Id);
             var trainPlanTable = trainPlanRepository.Queryable();
-            var trainPracticeTable = trainPracticeRepostitory.Queryable().WhereIF(!string.IsNullOrEmpty(trainPracticeRequest.TrainPracticeId),x=>x.Id == trainPracticeRequest.TrainPracticeId);
+            var trainPracticeTable = trainPracticeRepostitory.Queryable().WhereIF(!string.IsNullOrEmpty(trainPracticeRequest.TrainPracticeId), x => x.Id == trainPracticeRequest.TrainPracticeId);
             var tranPlanTemplateTable = trainPlanTemplateRepository.Queryable();
 
             var trainPractices = trainPracticeTable.InnerJoin(tranPlanTemplateTable, (tp, tpt) => tp.TrainTemplateId == tpt.TrainTemplateId)
@@ -462,7 +465,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
             var trainPracticeOptions = await trainPracticeOptionsRepository.Queryable()
                 .InnerJoin(trainPractices, (tpo, tp) => tpo.TrainPracticeId == tp.Id)
-                .Where((tpo, tp)=> tp.Id == trainPracticeRequest.TrainPracticeId)
+                .Where((tpo, tp) => tp.Id == trainPracticeRequest.TrainPracticeId)
                 .Select((tpo, tp) => tpo).ToListAsync();
 
             var queryable = trainRecordAnswerRepository.Queryable()
@@ -478,7 +481,7 @@ namespace Hotline.Application.Exam.Service.Trains
             var trainPracticeOptionsDtos = new List<SimpleTrainPracticeOptionsDto>();
 
             trainPracticeOptions.ForEach(x =>
-            {              
+            {
                 var trainPracticeOptionsDto = _mapper.Map<SimpleTrainPracticeOptionsDto>(x);
 
                 trainPracticeOptionsDto.IsSelected = trainRecordOptions.Any(m => m.QuestionOptionId == x.QuestionOptionId);

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

@@ -0,0 +1,53 @@
+using Hotline.BatchTask;
+using Quartz;
+using Hotline.Share.Enums.BatchTask;
+using Hotline.Share.Dtos.Order;
+using Microsoft.Extensions.DependencyInjection;
+using Hotline.Application.OrderApp.OrderVisitApp;
+using Hotline.Share.Dtos.Order.OrderVisit;
+
+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)

+ 46 - 46
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,8 +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
-            .WhereIF(dto.PublishState.HasValue , d=>d.PublishState == dto.PublishState.Value) //发布范围 是否公开
-			.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)  //办结时间
@@ -1302,20 +1303,20 @@ public class OrderApplication : IOrderApplication, IScopeDependency
                 canUpdateOrderSender);
             //TODO发送短信即将超期
             //_capPublisher.PublishDelay(expiredTimeConfig.NearlyExpiredTime - DateTime.Now, EventNames.HotlineOrderNearlyExpiredTimeSms, new PublishNearlyExpiredTimeSmsDto() { OrderId = order.Id });
-            //自动延期订阅
-            var enabled = _systemSettingCacheManager.GetSetting(SettingConstants.EnabledAutomaticDelay)?.SettingValue[0];
-            if (bool.Parse(enabled))
-            {
-                //自动延期催办短信发送
-                await _capPublisher.PublishDelayAsync(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(2),
-                    EventNames.HotlineOrderAutomaticSendSmsDelay, new PublishAutomaticDelayDto() { OrderId = order.Id }, cancellationToken: cancellationToken);
-
-                await _capPublisher.PublishDelayAsync(
-                    expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(1),
-                    EventNames.HotlineOrderAutomaticDelay,
-                    new PublishAutomaticDelayDto() { OrderId = order.Id },
-                    cancellationToken: cancellationToken);
-            }
+            ////自动延期订阅
+            //var enabled = _systemSettingCacheManager.GetSetting(SettingConstants.EnabledAutomaticDelay)?.SettingValue[0];
+            //if (bool.Parse(enabled))
+            //{
+            //    //自动延期催办短信发送
+            //    await _capPublisher.PublishDelayAsync(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(2),
+            //        EventNames.HotlineOrderAutomaticSendSmsDelay, new PublishAutomaticDelayDto() { OrderId = order.Id }, cancellationToken: cancellationToken);
+
+            //    await _capPublisher.PublishDelayAsync(
+            //        expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(1),
+            //        EventNames.HotlineOrderAutomaticDelay,
+            //        new PublishAutomaticDelayDto() { OrderId = order.Id },
+            //        cancellationToken: cancellationToken);
+            //}
         }
         else if (dto.Workflow.FlowDirection is EFlowDirection.CenterToOrg)
         {
@@ -1340,16 +1341,16 @@ public class OrderApplication : IOrderApplication, IScopeDependency
             //TODO发送短信即将超期
             //_capPublisher.PublishDelay(expiredTimeConfig.NearlyExpiredTime - DateTime.Now, EventNames.HotlineOrderNearlyExpiredTimeSms, new PublishNearlyExpiredTimeSmsDto() { OrderId = order.Id });
             //自动延期订阅
-            var enabled = _systemSettingCacheManager.GetSetting(SettingConstants.EnabledAutomaticDelay)?.SettingValue[0];
-            if (bool.Parse(enabled))
-            {
-                //自动延期催办短信发送
-                await _capPublisher.PublishDelayAsync(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(2),
-                    EventNames.HotlineOrderAutomaticSendSmsDelay, new PublishAutomaticDelayDto() { OrderId = order.Id }, cancellationToken: cancellationToken);
+            //var enabled = _systemSettingCacheManager.GetSetting(SettingConstants.EnabledAutomaticDelay)?.SettingValue[0];
+            //if (bool.Parse(enabled))
+            //{
+            //    //自动延期催办短信发送
+            //    await _capPublisher.PublishDelayAsync(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(2),
+            //        EventNames.HotlineOrderAutomaticSendSmsDelay, new PublishAutomaticDelayDto() { OrderId = order.Id }, cancellationToken: cancellationToken);
 
-                _capPublisher.PublishDelay(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(1), EventNames.HotlineOrderAutomaticDelay,
-                    new PublishAutomaticDelayDto() { OrderId = order.Id });
-            }
+            //    _capPublisher.PublishDelay(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(1), EventNames.HotlineOrderAutomaticDelay,
+            //        new PublishAutomaticDelayDto() { OrderId = order.Id });
+            //}
         }
         else if (dto.Workflow.FlowDirection is EFlowDirection.CenterToCenter)
         {
@@ -1363,16 +1364,16 @@ public class OrderApplication : IOrderApplication, IScopeDependency
                 //TODO发送短信即将超期
                 //_capPublisher.PublishDelay(expiredTimeConfig.NearlyExpiredTime - DateTime.Now, EventNames.HotlineOrderNearlyExpiredTimeSms, new PublishNearlyExpiredTimeSmsDto() { OrderId = order.Id });
                 //自动延期订阅
-                var enabled = _systemSettingCacheManager.GetSetting(SettingConstants.EnabledAutomaticDelay)?.SettingValue[0];
-                if (bool.Parse(enabled))
-                {
-                    //自动延期催办短信发送
-                    await _capPublisher.PublishDelayAsync(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(2),
-                        EventNames.HotlineOrderAutomaticSendSmsDelay, new PublishAutomaticDelayDto() { OrderId = order.Id }, cancellationToken: cancellationToken);
-
-                    _capPublisher.PublishDelay(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(1), EventNames.HotlineOrderAutomaticDelay,
-                        new PublishAutomaticDelayDto() { OrderId = order.Id });
-                }
+                //var enabled = _systemSettingCacheManager.GetSetting(SettingConstants.EnabledAutomaticDelay)?.SettingValue[0];
+                //if (bool.Parse(enabled))
+                //{
+                //    //自动延期催办短信发送
+                //    await _capPublisher.PublishDelayAsync(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(2),
+                //        EventNames.HotlineOrderAutomaticSendSmsDelay, new PublishAutomaticDelayDto() { OrderId = order.Id }, cancellationToken: cancellationToken);
+
+                //    _capPublisher.PublishDelay(expiredTimeConfig.ExpiredTime - DateTime.Now.AddHours(1), EventNames.HotlineOrderAutomaticDelay,
+                //        new PublishAutomaticDelayDto() { OrderId = order.Id });
+                //}
             }
         }
         else if (dto.Workflow.FlowDirection is EFlowDirection.CenterToFile) //中心直接归档,中心意见为实际办理意见
@@ -4566,14 +4567,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;
@@ -6548,7 +6549,7 @@ public class OrderApplication : IOrderApplication, IScopeDependency
     {
         var isAdmin = _orderDomainService.IsCheckAdmin();
         var query = _orderDelayRepository.Queryable();
-        if (!isAdmin)
+        if (!isAdmin && _appOptions.Value.IsZiGong == false)
         {
             query.Where(d => SqlFunc.Subqueryable<WorkflowTrace>()
                 .Where(step => step.ExternalId == d.Id &&
@@ -6578,8 +6579,7 @@ public class OrderApplication : IOrderApplication, IScopeDependency
                 // .WhereIF(dto.DataScope is 0, d => (d.AutomaticDelayNum == 0 || d.AutomaticDelayNum == null) && d.DelayState != EDelayState.Withdraw)
                 .WhereIF(dto.DataScope is 0, d => d.DelayState != EDelayState.Withdraw)
                 .WhereIF(dto.DataScope is 0 && !_sessionContext.OrgIsCenter, d => d.CreatorOrgId.StartsWith(_sessionContext.RequiredOrgId))
-                .WhereIF(!string.IsNullOrEmpty(dto.Keyword),
-                    d => d.Order.Title.Contains(dto.Keyword!) || d.Order.No.Contains(dto.Keyword!))
+                .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Order.Title.Contains(dto.Keyword!) || d.Order.No.Contains(dto.Keyword!))
                 .WhereIF(dto.IsApply == true, d => d.DelayState != EDelayState.Examining)
                 .WhereIF(dto.IsApply == false, d => d.DelayState == EDelayState.Examining)
                 .WhereIF(dto.DelayState != null, d => d.DelayState == dto.DelayState)

+ 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)
+//    {
+
+//    }
+
+//}

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

@@ -1,6 +1,7 @@
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.Order.OrderVisit;
 
-namespace Hotline.Application.OrderApp;
+namespace Hotline.Application.OrderApp.OrderVisitApp;
 
 public interface IOrderVisitApplication
 {
@@ -17,4 +18,10 @@ public interface IOrderVisitApplication
     /// <param name="dto"></param>
     /// <returns></returns>
     Task<string> AddOrderVisitRecordAsync(OrderVisitRecordDto dto, CancellationToken cancellationToken = default);
+
+    /// <summary>
+    /// 新增批量语音回访任务
+    /// </summary>
+    /// <returns></returns>
+    Task AddTaskBatchVoiceVisitAsync(BatchVoiceVisitRequest request, CancellationToken cancellation);
 }

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

@@ -1,6 +1,10 @@
-using Hotline.Configurations;
+using Hotline.BatchTask;
+using Hotline.Configurations;
 using Hotline.Orders;
+using Hotline.Share.Dtos.BatchTask;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.Order.OrderVisit;
+using Hotline.Share.Enums.BatchTask;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Tools;
 using MapsterMapper;
@@ -9,7 +13,7 @@ using SqlSugar;
 using XF.Domain.Dependency;
 using XF.Domain.Repository;
 
-namespace Hotline.Application.OrderApp;
+namespace Hotline.Application.OrderApp.OrderVisitApp;
 
 /// <summary>
 /// 回访服务
@@ -20,17 +24,20 @@ public class OrderVisitApplication : IOrderVisitApplication, IScopeDependency
 
     private readonly IOrderVisitRepository _orderVisitRepository;
     private readonly IRepository<OrderVisitRecord> _orderVisitRecordRepository;
+    private readonly IApptaskDomainService _apptaskDomainService;
     private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
     private readonly IMapper _mapper;
 
     public OrderVisitApplication(
         IOrderVisitRepository orderVisitRepository,
         IRepository<OrderVisitRecord> orderVisitRecordRepository,
+        IApptaskDomainService apptaskDomainService,
         IOptionsSnapshot<AppConfiguration> appOptions,
         IMapper mapper)
     {
         _orderVisitRepository = orderVisitRepository;
         _orderVisitRecordRepository = orderVisitRecordRepository;
+        _apptaskDomainService = apptaskDomainService;
         _appOptions = appOptions;
         _mapper = mapper;
     }
@@ -99,4 +106,44 @@ public class OrderVisitApplication : IOrderVisitApplication, IScopeDependency
     }
 
     #endregion
+
+    /// <summary>
+    /// 新增批量语音回访任务
+    /// </summary>
+    /// <returns></returns>
+    public async Task AddTaskBatchVoiceVisitAsync(BatchVoiceVisitRequest request, CancellationToken cancellation)
+    {
+        var visits = await _orderVisitRepository.Queryable()
+            .Includes(d => d.Order)
+            .Where(d => request.VisitIds.Contains(d.Id)
+                        && !string.IsNullOrEmpty(d.Order.Contact))
+            .ToListAsync(cancellation);
+
+        var taskId = await _apptaskDomainService.AddAsync(new AddApptaskRequest
+        {
+            TaskType = ETaskType.VoiceVisit,
+            TryLimit = 10,
+            ApptaskItems = visits.Select(d => new AddApptaskItemRequest
+            {
+                BusinessId = d.Id,
+                TaskParams = new VoiceVisitRequest
+                {
+                    VisitId = d.Id,
+                    PhoneNo = d.Order.Contact!
+                }
+            }).ToList()
+        }, cancellation);
+
+        if (!string.IsNullOrEmpty(taskId))
+        {
+            var visitIds = visits.Select(d => d.Id).ToList();
+            await _orderVisitRepository.Updateable()
+                  .SetColumns(d => new OrderVisit
+                  {
+                      VisitState = EVisitState.AiVisiting
+                  })
+                  .Where(d =>  visitIds.Contains(d.Id))
+                  .ExecuteCommandAsync(cancellation);
+        }
+    }
 }

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

@@ -0,0 +1,67 @@
+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;
+using Hotline.Share.Dtos.Order.OrderVisit;
+
+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);
+            return result.IsSuccessStatusCode
+                ? new ApptaskExecuteResult
+                {
+                    IsSuccess = true,
+                    Message = "成功"
+                }
+                : new ApptaskExecuteResult
+                {
+                    IsSuccess = false,
+                    Message = "请求失败"
+                };
+        }
+    }
+}

+ 2 - 1
src/Hotline.Repository.SqlSugar/CallCenter/TrCallRecordRepository.cs

@@ -264,7 +264,7 @@ namespace Hotline.Repository.SqlSugar.CallCenter
                  .WhereIF(dto.BeginIvrTimeEnd.HasValue, x => x.BeginIvrTime <= dto.BeginIvrTimeEnd)
                  .WhereIF(dto.EndIvrTimeStart.HasValue, x => x.EndIvrTime >= dto.EndIvrTimeStart)
                  .WhereIF(dto.EndIvrTimeEnd.HasValue, x => x.EndIvrTime <= dto.EndIvrTimeEnd)
-                 .WhereIF(dto.BeginQueueTimeStart.HasValue, x => x.BeginQueueTime >= dto.BeginQueueTimeEnd)
+                 .WhereIF(dto.BeginQueueTimeStart.HasValue, x => x.BeginQueueTime >= dto.BeginQueueTimeStart)
                  .WhereIF(dto.BeginQueueTimeEnd.HasValue, x => x.BeginQueueTime <= dto.BeginQueueTimeEnd)
                  .WhereIF(dto.EndQueueTimeStart.HasValue, x => x.EndQueueTime >= dto.EndQueueTimeStart)
                  .WhereIF(dto.EndQueueTimeEnd.HasValue, x => x.EndQueueTime <= dto.EndQueueTimeEnd)
@@ -298,6 +298,7 @@ namespace Hotline.Repository.SqlSugar.CallCenter
                 .Select(x => new QueryCallDateStatisticsDetailResp()
                 {
                     Date = x.CreatedTime.ToString("yyyy-MM-dd"),
+                    //CallInTotal = SqlFunc.AggregateSum(SqlFunc.IIF(x.CallDirection == ECallDirection.In, 1, 0)),//呼入总量
                     IvrCallInTotal = SqlFunc.AggregateSum(SqlFunc.IIF(x.BeginIvrTime.HasValue && !x.BeginQueueTime.HasValue && !x.BeginRingTime.HasValue && x.OnState == EOnState.NoOn, 1, 0)), //IVR挂断
                     PersonCallInCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.IvrDtmf.Substring(x.IvrDtmf.Length - 1, 1) == "1", 1, 0)),
                     EnterpriseCallInCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.IvrDtmf.Substring(x.IvrDtmf.Length - 1, 1) == "2", 1, 0)),

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

+ 2 - 0
src/Hotline.Share/Dtos/CallCenter/BiQueryCallsDto.cs

@@ -239,10 +239,12 @@ public class QueryCallDateStatisticsDetailResp
     /// 日期
     /// </summary>
     public string Date { get; set; }
+
     /// <summary>
     /// 呼入总量(计算所得)
     /// </summary>
     public int CallInTotal => IvrCallInTotal + PersonCallInCount + EnterpriseCallInCount + GaoXiaoCallInCount + AiCallInCount;
+    //public int CallInTotal { get; set; }
 
     /// <summary>
     /// IVR呼入总量

+ 45 - 0
src/Hotline.Share/Dtos/ExamManages/ExamQuestionDto.cs

@@ -1,6 +1,8 @@
 using Exam.Infrastructure.Data.Entity;
+using Hotline.Share.Dtos.Questions;
 using Hotline.Share.Enums.Exams;
 using System.ComponentModel;
+using System.Runtime.CompilerServices;
 
 namespace Hotline.Share.Dtos.ExamManages
 {
@@ -40,4 +42,47 @@ namespace Hotline.Share.Dtos.ExamManages
         [Description("答案")]
         public string Answer { get; set; }
     }
+
+    /// <summary>
+    /// 查看考试试题
+    /// </summary>
+    [Description("查看考试试题")]
+    public class ViewExamQuestionDto : ExamQuestionDto
+    {
+        /// <summary>
+        /// 实际得分
+        /// </summary>
+        [Description("实际得分")]
+        public int? RealScore { get; set; }
+
+        /// <summary>
+        /// 试题Id
+        /// </summary>
+        [Description("试题Id")]
+        public string QuestionId { get; set; }
+
+        /// <summary>
+        /// 试题关联知识
+        /// </summary>
+        [Description("试题关联知识")]
+        public List<QuestionKnowladgeDto> QuestionKnowladgeDtos { get; set; }
+
+        /// <summary>
+        /// 试题课件
+        /// </summary>
+        [Description("试题课件")]
+        public List<QuestionSourcewareDto> QuestionSourcewareDtos { get; set; }
+
+        /// <summary>
+        /// 查看试题选项
+        /// </summary>
+        [Description("查看试题选项")]
+        public new List<ViewQuestionOptionDto> QuestionOptions { get; set; }
+
+        /// <summary>
+        /// 参考答案
+        /// </summary>
+        [Description("参考答案")]
+        public string CorrectAnswer { 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;

+ 14 - 0
src/Hotline.Share/Dtos/Order/OrderVisit/BatchVoiceVisitRequest.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Dtos.Order.OrderVisit
+{
+    public class BatchVoiceVisitRequest
+    {
+        public string? Name { get; set; }
+        public List<string> VisitIds { get; set; }
+    }
+}

+ 7 - 0
src/Hotline.Share/Dtos/Order/OrderVisit/VoiceVisitRequest.cs

@@ -0,0 +1,7 @@
+namespace Hotline.Share.Dtos.Order.OrderVisit;
+
+public class VoiceVisitRequest
+{
+    public string PhoneNo { get; set; }
+    public string VisitId { get; set; }
+}

+ 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
 {
@@ -438,137 +437,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/Dtos/Questions/QuestionAnswerDto.cs

@@ -15,6 +15,19 @@ namespace Hotline.Share.Dtos.Questions
 
     }
 
+    /// <summary>
+    /// 查看试题参考答案
+    /// </summary>
+    [Description("查看试题参考答案")]
+    public class ViewQuestionAnswerDto : QuestionAnswerDto
+    {
+        /// <summary>
+        /// 正确答案
+        /// </summary>
+        [Description("正确答案")]
+        public string CorrectAnswer { get; set; }
+    }
+
     /// <summary>
     /// 试题参考答案
     /// </summary>

+ 9 - 0
src/Hotline.Share/Dtos/Questions/QuestionOptionsDto.cs

@@ -22,6 +22,15 @@ namespace Hotline.Share.Dtos.Questions
         }
     }
 
+    /// <summary>
+    /// 查看试题选项
+    /// </summary>
+    [Description("查看试题选项")]
+    public class ViewQuestionOptionDto : QuestionOptionsDto
+    {
+        public bool IsSelected { get; set; }
+    }
+
     /// <summary>
     /// 试题选项
     /// </summary>

+ 1 - 1
src/Hotline.Share/Dtos/Questions/QuestionSourcewareDto.cs

@@ -12,7 +12,7 @@ namespace Hotline.Share.Dtos.Questions
     [Description("关联课件")]
     public class QuestionSourcewareDto:UpdateQuestionSourcewareDto
     {
-
+        public string AttachmentId { get; set; }
     }
 
     /// <summary>

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

+ 3 - 3
src/Hotline.Share/Requests/Exam/ExamQuestionRequest.cs

@@ -13,9 +13,9 @@ namespace Hotline.Share.Requests.Exam
         public string QuestionId { get; set; }
 
         /// <summary>
-        /// 考试Id
+        /// 用户考试Id
         /// </summary>
-        [Description("考试Id")]
-        public string ExamId { get; set; }
+        [Description("用户考试Id")]
+        public string UserExamId { get; set; }
     }
 }

+ 12 - 0
src/Hotline.Share/ViewResponses/Exam/UserExamResultViewResponse.cs

@@ -121,6 +121,18 @@ namespace Exam.Share.ViewResponses.Exam
         /// </summary>
         [Description("是否阅卷")]
         public bool IsCheck { get; set; }
+
+        /// <summary>
+        /// 是否重考
+        /// </summary>
+        [Description("是否重考")]
+        public bool? IsReExam { get; set; }
+
+        /// <summary>
+        /// 能否重考
+        /// </summary>
+        [Description("能否重考")]
+        public bool CanReExam { get; set; }
     }
 
     public class GradingResultViewResponse : IViewResponse

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

+ 9 - 1
src/Hotline/Orders/IOrderVisitDomainService.cs

@@ -1,9 +1,17 @@
-using Hotline.Settings;
+using Hotline.CallCenter.Calls;
+using Hotline.Settings;
 using Hotline.Share.Dtos.Push;
 
 namespace Hotline.Orders;
 public interface IOrderVisitDomainService
 {
+    /// <summary>
+    /// 语音回访处理
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    Task UpdateVoiceReplyAsync(VoiceEvaluationButton dto);
+
     /// <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; }
+
+    }
 }

+ 31 - 12
src/Hotline/Orders/OrderVisitDomainService.cs

@@ -1,26 +1,21 @@
 using DotNetCore.CAP;
 using Hotline.Caching.Interfaces;
-using Hotline.Share.Dtos;
+using Hotline.CallCenter.Calls;
+using Hotline.Configurations;
+using Hotline.EventBus;
+using Hotline.Settings;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Dtos.Push;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Tools;
 using Mapster;
+using MediatR;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System.Text.RegularExpressions;
 using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
 using XF.Domain.Repository;
-using Hotline.EventBus;
-using Hotline.Orders.Notifications;
-using Hotline.Configurations;
-using Microsoft.Extensions.Options;
-using Hotline.Settings;
-using System.Text.RegularExpressions;
-using System.Threading;
-using Hotline.Caching.Services;
-using Hotline.Push.Notifies;
-using Hotline.Share.Enums.Push;
-using MediatR;
 
 namespace Hotline.Orders;
 public class OrderVisitDomainService : IOrderVisitDomainService, IScopeDependency
@@ -67,6 +62,30 @@ public class OrderVisitDomainService : IOrderVisitDomainService, IScopeDependenc
         _mediator = mediator;
     }
 
+    /// <summary>
+    /// 语音回访处理
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    public async Task UpdateVoiceReplyAsync(VoiceEvaluationButton dto)
+    {
+        var data = await _orderVisitRepository.GetAsync(p => p.Id == dto.CustomerId);
+        if (data != null)
+        {
+            var replyTxt = "智能电话回访不作评价";
+            if (dto.Content == "1")
+                replyTxt = "智能电话回访满意";
+            else if (dto.Content == "2")
+                replyTxt = "智能电话回访不满意";
+
+            await UpdateSmsReplyAsync(data, replyTxt);
+        }
+        else
+        {
+            _logRepository.Add("语音回访-不处理", dto, "未查询到回访数据", "UpdateVoiceReplyAsync", 0);
+        }
+    }
+
     /// <summary>
     /// 用户回访短信回复更新回访状态
     /// </summary>

+ 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不能为空");
+    }
+}

+ 21 - 0
src/Hotline/Validators/Order/OrderVisit/BatchVoiceVisitRequestValidator.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FluentValidation;
+using Hotline.Share.Dtos.Order.OrderVisit;
+
+namespace Hotline.Validators.Order.OrderVisit
+{
+    public class BatchVoiceVisitRequestValidator : AbstractValidator<BatchVoiceVisitRequest>
+    {
+        public BatchVoiceVisitRequestValidator()
+        {
+            RuleFor(d => d.VisitIds)
+                .Cascade(CascadeMode.Stop)
+                .NotEmpty()
+                .WithMessage("请选择待回访数据");
+        }
+    }
+}

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

+ 3 - 0
test/Hotline.Tests/Domain/OrderVisitDomainServiceTest.cs

@@ -121,6 +121,9 @@ public class OrderVisitDomainServiceTest : TestBase
     [InlineData("都没有办理好", "SMSUnsatisfied", "", "", "都没有办理好", "Published", "YiBin")]
     [InlineData("1", "Visited", "4", "满意", "满意", "Visited", "YiBin")]
     [InlineData("2", "SMSUnsatisfied", "2", "不满意", "不满意", "Published", "YiBin")]
+    [InlineData("智能电话回访满意", "Visited", "4", "满意", "满意", "Visited")]
+    [InlineData("智能电话回访不满意", "Visited", "2", "不满意", "不满意", "Visited")]
+    [InlineData("智能电话回访不作评价", "Visited", "7", "未做评价", "未做评价", "Visited")]
     public async Task UpdateSmsReply_Test(string content, string visitState, string orgResuktKey, string orgResuktValue, string visitContent, string visited = "Visited", string appScope = "ZiGong")
     {
         if (appScope != "ZiGong")