ソースを参照

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

xf 10 ヶ月 前
コミット
3b5187b33f
42 ファイル変更892 行追加248 行削除
  1. 6 5
      src/Hotline.Ai.Jths/AiQualityService.cs
  2. 15 3
      src/Hotline.Ai.Jths/AiVisitService.cs
  3. 114 3
      src/Hotline.Api/Controllers/AiController.cs
  4. 117 2
      src/Hotline.Api/Controllers/Bi/BiOrderController.cs
  5. 6 5
      src/Hotline.Api/Controllers/IPPbxController.cs
  6. 14 5
      src/Hotline.Api/Controllers/IdentityController.cs
  7. 37 36
      src/Hotline.Api/Controllers/OrderController.cs
  8. 7 3
      src/Hotline.Api/Controllers/QualityController.cs
  9. 86 77
      src/Hotline.Api/Controllers/TestController.cs
  10. 8 8
      src/Hotline.Api/Controllers/WorkflowController.cs
  11. 2 2
      src/Hotline.Api/config/appsettings.Development.json
  12. 2 2
      src/Hotline.Api/config/appsettings.json
  13. 1 1
      src/Hotline.Application/FlowEngine/IWorkflowApplication.cs
  14. 8 9
      src/Hotline.Application/FlowEngine/WorkflowApplication.cs
  15. 4 1
      src/Hotline.Application/Mappers/WorkflowMapperConfigs.cs
  16. 27 0
      src/Hotline.Application/Orders/IOrderApplication.cs
  17. 94 10
      src/Hotline.Application/Orders/OrderApplication.cs
  18. 28 3
      src/Hotline.Application/Quality/QualityApplication.cs
  19. 2 2
      src/Hotline.Application/Subscribers/DatasharingSubscriber.cs
  20. 14 0
      src/Hotline.Application/Systems/ISystemLogApplication.cs
  21. 47 0
      src/Hotline.Application/Systems/SystemLogApplication.cs
  22. 14 6
      src/Hotline.Repository.SqlSugar/Extensions/DataPermissionExtensions.cs
  23. 31 2
      src/Hotline.Share/Dtos/Ai/AiDto.cs
  24. 7 0
      src/Hotline.Share/Dtos/FlowEngine/BasicWorkflowDto.cs
  25. 85 0
      src/Hotline.Share/Dtos/Order/OrderBiDto.cs
  26. 7 1
      src/Hotline.Share/Dtos/Order/OrderWaitedDto.cs
  27. 4 0
      src/Hotline.Share/Dtos/Settings/SystemLogDto.cs
  28. 23 27
      src/Hotline.Share/Dtos/TrCallCenter/TrTelDao.cs
  29. 3 0
      src/Hotline.Share/Enums/Ai/EAiCallOutTaskState.cs
  30. 2 0
      src/Hotline.Share/Enums/Ai/EAiOrderVisitTaskState.cs
  31. 30 2
      src/Hotline.Share/Requests/PagedKeywordRequest.cs
  32. 2 0
      src/Hotline/Ai/CallOut/CallOutTask.cs
  33. 1 0
      src/Hotline/Ai/CallOut/CallOutTemplate.cs
  34. 1 1
      src/Hotline/Ai/Quality/IAiQualityService.cs
  35. 3 0
      src/Hotline/Ai/Visit/IAiVisitService.cs
  36. 2 2
      src/Hotline/FlowEngine/FlowAssignInfo.cs
  37. 2 2
      src/Hotline/FlowEngine/Workflows/IWorkflowDomainService.cs
  38. 11 14
      src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs
  39. 0 6
      src/Hotline/FlowEngine/Workflows/WorkflowStep.cs
  40. 12 5
      src/Hotline/Quality/QualityDomainService.cs
  41. 5 0
      src/Hotline/Settings/SystemLog.cs
  42. 8 3
      src/XF.Domain/Constants/SettingConstants.cs

+ 6 - 5
src/Hotline.Ai.Jths/AiQualityService.cs

@@ -19,20 +19,20 @@ namespace Hotline.Ai.Jths
         {
             _client = new RestClient();
             _baseUrl = baseUrl;
-
         }
 
-        public async Task CreateAiOrderQualityTask(Hotline.Quality.Quality model, TrCallRecord? call, Order order, CancellationToken cancellationToken)
+        public async Task CreateAiOrderQualityTask(Hotline.Quality.Quality model, TrCallRecord? call, Order order, string viteRecordPrefix, CancellationToken cancellationToken)
         {
             if (string.IsNullOrEmpty(call?.RecordingFileName)) return;
             var fileName = call.RecordingFileName.Split(".");
             var length = fileName.Count() - 1;
             var recordForm = fileName.Any() && fileName.Length > 1 ? fileName[length] : string.Empty;
-            List<AiQualityDto> datalist = new List<AiQualityDto>();
+            var viteRecordPrefixUrl = new Uri(viteRecordPrefix);
+			List<AiQualityDto> datalist = new List<AiQualityDto>();
             AiQualityDto aiQuality = new AiQualityDto
             {
                 RecordID = model.Id,
-                RecordPath = call.RecordingFileUrl,
+                RecordPath = viteRecordPrefixUrl + call.RecordingAbsolutePath,
                 AgentID = "1001",
                 CallNumber = call.CPN,
                 CallTime = call.CreatedTime.ToString("yyyy-MM-dd HH:mm:ss"),
@@ -41,7 +41,8 @@ namespace Hotline.Ai.Jths
             };
             datalist.Add(aiQuality);
             var data = JsonConvert.SerializeObject(datalist);
-            await ExecuteAsync(_baseUrl + "routeinfo/api", Method.Post, data, cancellationToken);
+            var baseUrl = new Uri(_baseUrl);
+			await ExecuteAsync(baseUrl.ToString() + "routeinfo/api", Method.Post, data, cancellationToken);
         }
 
         public async Task<ApiResponse<TResponse>> ExecuteAsync<TRequest, TResponse>(string path, Method httpMethod,

+ 15 - 3
src/Hotline.Ai.Jths/AiVisitService.cs

@@ -32,6 +32,18 @@ namespace Hotline.Ai.Jths
             _client = new RestClient(options);
         }
 
+        public async Task<bool> ChangeStatusAsync(string batchUid, string status, CancellationToken cancellationToken)
+        {
+            string posturl = _baseUrl + "/edas/task/" + status + "?batchUid=" + batchUid;
+            var requestData = new AiVisitServiceRequest(){ };
+            var response = await ExecuteAsync<AiVisitServiceRequest, AiVisitServiceResponse>(_baseUrl + "/edas/batchTask", Method.Post, requestData, cancellationToken);
+            if (response.Code== 10000)
+            {
+                return true;
+            }
+            return false;
+        }
+
         public async Task<CallOutTask> CreateAiCallOutTask(CallOutTask callOutTask, string sceneuid, string ruleuId, string callOutContentKey, CancellationToken cancellationToken)
         {
             string content = callOutTask.CallOutTemplate.TemplateContent;
@@ -44,7 +56,7 @@ namespace Hotline.Ai.Jths
                 StartDate = callOutTask.BeginTime.ToString("yyyy/MM/dd HH:mm:ss"),
                 EndDate = callOutTask.EndTime.ToString("yyyy/MM/dd HH:mm:ss"),
                 FestivalBan = callOutTask.FestivalBan,
-                RuleType = 1,
+                RuleType = 2,
                 RuleUid = ruleuId,
             };
             var taskDataList = new List<TaskData>();
@@ -58,7 +70,7 @@ namespace Hotline.Ai.Jths
             }
             requestData.TaskDataList = taskDataList;
             var response = await ExecuteAsync<AiVisitServiceRequest, AiVisitServiceResponse>(_baseUrl + "/edas/batchTask", Method.Post, requestData, cancellationToken);
-            if (response.Result.TaskInfoList != null && !string.IsNullOrEmpty(response.Result.BatchUid))
+            if (response.Result != null && response.Result.TaskInfoList != null && !string.IsNullOrEmpty(response.Result.BatchUid))
             {
                 //拼对象
                 callOutTask.BatchUid = response.Result.BatchUid;
@@ -133,7 +145,7 @@ namespace Hotline.Ai.Jths
             }
             requestData.TaskDataList = taskDataList;
             var response = await ExecuteAsync<AiVisitServiceRequest, AiVisitServiceResponse>(_baseUrl + "/edas/batchTask",Method.Post, requestData,cancellationToken);
-            if (response.Result.TaskInfoList!=null && !string.IsNullOrEmpty(response.Result.BatchUid))
+            if (response.Result!=null && response.Result.TaskInfoList!=null && !string.IsNullOrEmpty(response.Result.BatchUid))
             {
                 //拼对象
                 aiOrderVisit.BatchUid = response.Result.BatchUid;

+ 114 - 3
src/Hotline.Api/Controllers/AiController.cs

@@ -30,6 +30,7 @@ using XF.Domain.Authentications;
 using XF.Domain.Constants;
 using XF.Domain.Exceptions;
 using XF.Domain.Repository;
+using XF.Utility.EnumExtensions;
 
 namespace Hotline.Api.Controllers
 {
@@ -130,7 +131,7 @@ namespace Hotline.Api.Controllers
                     Id = x.Id,
                     TemplateName = x.TemplateName,
                     TemplateContent = x.TemplateContent,
-                    //CallOutTaskCount = x.CallOutTasks != null ? x.CallOutTasks.Count() : 0,
+                    CallOutTaskCount = x.CallOutTasks.Count(),
                     CreationTime = x.CreationTime,
                     IsEnable = x.IsEnable,
                     CreatorName = x.CreatorName,
@@ -247,6 +248,19 @@ namespace Hotline.Api.Controllers
             return new PagedDto<AiCallOutListRep>(total, _mapper.Map<IReadOnlyList<AiCallOutListRep>>(items));
         }
 
+        /// <summary>
+        /// 外呼任务列表基础数据
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("callout/list-basedata")]
+        public async Task<object> AiCallOuttListBaseData()
+        {
+            return new {
+                AiCallOutTaskState = EnumExts.GetDescriptions<EAiCallOutTaskState>(),
+                AiCallOutState = EnumExts.GetDescriptions<EAiCallOutState>()
+            };
+        }
+
         /// <summary>
         /// 批量外呼任务明细列表
         /// </summary>
@@ -257,6 +271,9 @@ namespace Hotline.Api.Controllers
         {
             var (total, items) = await _callOutTaskDetailRepository.Queryable()
                 .Where(x => x.CallOutTaskId == dto.Id)
+                .WhereIF(!string.IsNullOrEmpty(dto.OuterNo),x=>x.OuterNo.Contains(dto.OuterNo))
+                .WhereIF(!string.IsNullOrEmpty(dto.Name),x=>x.Name.Contains(dto.Name))
+                .WhereIF(dto.AiCallOutState!=null,x=>x.AiCallOutState== dto.AiCallOutState)
                 .OrderByDescending(x => x.CreationTime)
                 .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
             return new PagedDto<AiCallOutDetailListRep>(total,_mapper.Map<IReadOnlyList<AiCallOutDetailListRep>>(items));
@@ -277,7 +294,7 @@ namespace Hotline.Api.Controllers
             }
             var model = _mapper.Map<CallOutTask>(dto);
             var detaillist = _mapper.Map<List<CallOutTaskDetail>>(dto.AddAiCallOutTaskDetailDtos);
-            model.AiCallOutTaskState = EAiCallOutTaskState.InProgress;
+            model.AiCallOutTaskState = EAiCallOutTaskState.NoStarted;
             model.HasVisitCount = dto.AddAiCallOutTaskDetailDtos.Count;
             model.VisitedCount = 0;
             model.VisitedFailCount = 0;
@@ -308,6 +325,88 @@ namespace Hotline.Api.Controllers
             await _callOutTaskDetailRepository.UpdateRangeAsync(newModel.CallOutTaskDetails, HttpContext.RequestAborted);
         }
 
+
+        /// <summary>
+        /// 可用外呼模板
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("callout/canuse-template")]
+        public async Task<List<CanUseCallOutTemplateListRep>> CanUseCallOutTemplateList()
+        {
+            var list = await _callOutTemplateRepository.Queryable().Where(x => x.IsEnable == true).ToListAsync(HttpContext.RequestAborted);
+            return _mapper.Map<List<CanUseCallOutTemplateListRep>>(list);
+        }
+
+        /// <summary>
+        /// 终止外呼任务
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [HttpPost("callout/closecallouttask")]
+        public async Task CloseCalloutTask([FromBody] CloseCalloutTaskReq request)
+        {
+            switch (request.TypeId)
+            {
+                //待处理子表
+                case 1:
+                    var aiVisit = await _aiOrderVisitRepository.Queryable()
+                        .Includes(x => x.AiOrderVisitDetails,s=>s.OrderVisit)
+                        .FirstAsync(x => x.Id == request.Id);
+                    if (aiVisit!=null && !string.IsNullOrEmpty(aiVisit.BatchUid))
+                    {
+                        if (aiVisit.TaskState != EAiOrderVisitTaskState.NoStarted || aiVisit.TaskState != EAiOrderVisitTaskState.InProgress)
+                            throw UserFriendlyException.SameMessage("当前状态不能终止");
+
+                        bool isOk = await _aiVisitService.ChangeStatusAsync(aiVisit.BatchUid, "cancel", HttpContext.RequestAborted);
+
+                        if (!isOk)
+                            throw UserFriendlyException.SameMessage("终止失败");
+
+                        aiVisit.TaskState = EAiOrderVisitTaskState.Close;
+                        aiVisit.AiOrderVisitDetails.ForEach(async x =>
+                        {
+                            if (x.AiOrderVisitState == EAiOrderVisitState.NoStarted)
+                            {
+                                x.AiOrderVisitState = EAiOrderVisitState.LoseEfficacy;
+                                x.OrderVisit.VisitState = EVisitState.WaitForVisit;
+                            }
+                            await _orderVisitRepository.UpdateAsync(x.OrderVisit);
+                        });
+                        await _aiOrderVisitRepository.UpdateAsync(aiVisit, HttpContext.RequestAborted);
+                        await _aiOrderVisitDetailRepository.UpdateRangeAsync(aiVisit.AiOrderVisitDetails, HttpContext.RequestAborted);
+
+                    }
+                    break;
+                case 2:
+                    var callOut = await _callOutTaskRepository.Queryable()
+                       .Includes(x => x.CallOutTaskDetails)
+                       .FirstAsync(x => x.Id == request.Id);
+                    if (callOut != null && !string.IsNullOrEmpty(callOut.BatchUid))
+                    {
+                        if (callOut.AiCallOutTaskState != EAiCallOutTaskState.NoStarted || callOut.AiCallOutTaskState != EAiCallOutTaskState.InProgress)
+                            throw UserFriendlyException.SameMessage("当前状态不能终止");
+
+                        bool isOk = await _aiVisitService.ChangeStatusAsync(callOut.BatchUid, "cancel", HttpContext.RequestAborted);
+
+                        if (!isOk)
+                            throw UserFriendlyException.SameMessage("终止失败");
+                        //处理业务数据
+                        callOut.AiCallOutTaskState = EAiCallOutTaskState.Close;
+                        callOut.CallOutTaskDetails.ForEach(x =>
+                        {
+                            if (x.AiCallOutState == EAiCallOutState.NoStarted)
+                                x.AiCallOutState = EAiCallOutState.LoseEfficacy;
+                        });
+                        await _callOutTaskRepository.UpdateAsync(callOut, HttpContext.RequestAborted);
+                        await _callOutTaskDetailRepository.UpdateRangeAsync(callOut.CallOutTaskDetails, HttpContext.RequestAborted);
+                    }
+                    break;
+                default:
+                    throw UserFriendlyException.SameMessage("未知业务");
+            }
+        }
+
+
         #endregion
 
         #endregion
@@ -554,6 +653,10 @@ namespace Hotline.Api.Controllers
                             {
                                 aiOrderVisit.TaskState = Share.Enums.Ai.EAiOrderVisitTaskState.Ended;
                             }
+                            else
+                            {
+                                aiOrderVisit.TaskState = EAiOrderVisitTaskState.InProgress;
+                            }
                             await _aiOrderVisitRepository.UpdateAsync(aiOrderVisit, HttpContext.RequestAborted);
                             #region 注释
                             //处理不满意结果(如果差评没有不满意原因则不能视为回访完成)   --(不满意设置为失效,生成新的人工回访记录)
@@ -672,6 +775,14 @@ namespace Hotline.Api.Controllers
                             aicallOutDetail.CallOutTime = DateTime.Now;
                             callOut.VisitedFailCount++;
                         }
+                        if ((callOut.VisitedFailCount + callOut.VisitedCount) == callOut.HasVisitCount)
+                        {
+                            callOut.AiCallOutTaskState = EAiCallOutTaskState.Ended;
+                        }
+                        else
+                        {
+                            callOut.AiCallOutTaskState = EAiCallOutTaskState.InProgress;
+                        }
                         await _callOutTaskRepository.UpdateAsync(callOut,HttpContext.RequestAborted);
                         await _callOutTaskDetailRepository.UpdateAsync(aicallOutDetail,HttpContext.RequestAborted);
                     }
@@ -768,7 +879,7 @@ namespace Hotline.Api.Controllers
 
             var detaillist = _mapper.Map<List<AiOrderVisitDetail>>(dto.AiOrderVisitDetails);
 
-            model.TaskState = Share.Enums.Ai.EAiOrderVisitTaskState.InProgress;
+            model.TaskState = Share.Enums.Ai.EAiOrderVisitTaskState.NoStarted;
             model.RuleType = 2;
             model.HasVisitCount = dto.AiOrderVisitDetails.Count;
             model.VisitedCount = 0;

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

@@ -23,11 +23,13 @@ using Hotline.Tools;
 using MapsterMapper;
 using Microsoft.AspNetCore.Mvc;
 using NPOI.SS.Formula.Functions;
+using Org.BouncyCastle.Utilities;
 using SqlSugar;
 using XF.Domain.Authentications;
 using XF.Domain.Constants;
 using XF.Domain.Exceptions;
 using XF.Domain.Repository;
+using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
 
 namespace Hotline.Api.Controllers.Bi
 {
@@ -3218,7 +3220,7 @@ namespace Hotline.Api.Controllers.Bi
         /// </summary>
         /// <returns></returns>
         [HttpPost("unsigned_order_report/_export")]
-        public async Task<FileStreamResult> UnsignedOrdersReportReport([FromBody] ExportExcelDto<QueryUnsignedOrdersRequest> dto)
+        public async Task<FileStreamResult> UnsignedOrdersReport([FromBody] ExportExcelDto<QueryUnsignedOrdersRequest> dto)
         {
             var query = _orderApplication.QueryUnsignedOrders(dto.QueryDto).Select((x, ws) => new UnsignedOrder { Order = x, WorkflowStep = ws });
             List<UnsignedOrder> unsignedOrders;
@@ -3250,5 +3252,118 @@ namespace Hotline.Api.Controllers.Bi
 
             return ExcelStreamResult(stream, "未签收统计列表数据");
         }
-    }
+
+
+		/// <summary>
+		/// 信件来源统计
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("order_source_report")]
+		public async Task<IReadOnlyList<OrderSourceVo>> QueryOrderSourceReport([FromQuery] QueryOrderSourceRequest dto)
+		{
+			var count = await _orderApplication.QueryOrderSource(dto).CountAsync();
+			var query = _orderApplication.QueryOrderSource(dto);
+			var items = await query.GroupBy(d=>d.SourceChannel).Select(d => new OrderSourceVo { Source = d.SourceChannel, Num = SqlFunc.AggregateCount(d.Id), TotalSumCount = count }).ToListAsync(HttpContext.RequestAborted);
+            items.Add(new OrderSourceVo { Source = "合计", Num = count, TotalSumCount = count });
+            return items;
+		}
+		/// <summary>
+		/// 信件来源统计导出
+		/// </summary>
+		/// <returns></returns>
+		[HttpPost("order_source/_export")]
+		public async Task<FileStreamResult> QueryOrderSourceReport([FromBody] ExportExcelDto<QueryOrderSourceRequest> dto)
+		{
+			var count = await _orderApplication.QueryOrderSource(dto.QueryDto).CountAsync();
+			var query = _orderApplication.QueryOrderSource(dto.QueryDto).GroupBy(d => d.SourceChannel).Select(d => new OrderSourceVo { Source = d.SourceChannel, Num = SqlFunc.AggregateCount(d.Id), TotalSumCount = count });
+			List<OrderSourceVo> orderSources;
+			if (dto.IsExportAll)
+			{
+				orderSources = await query.ToListAsync(HttpContext.RequestAborted);
+			}
+			else
+			{
+				var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+				orderSources = items;
+			}
+			orderSources.Add(new OrderSourceVo { Source = "合计", Num = count, TotalSumCount = count });
+			dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+			var dtos = orderSources
+				.Select(stu => _mapper.Map(stu, typeof(OrderSourceVo), dynamicClass))
+				.Cast<object>()
+				.ToList();
+			var stream = ExcelHelper.CreateStream(dtos);
+			return ExcelStreamResult(stream, "信件来源统计数据");
+		}
+
+
+		/// <summary>
+		/// 信件来源统计列表
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("order_source_report_list")]
+		public async Task<Object> QueryOrderSourceReportList([FromQuery] QueryOrderSourceRequest dto)
+		{
+			var data = await _orderApplication.QueryOrderSourceList(dto);
+            var header = await _orderApplication.QueryOrderSourceHeaderList(dto);
+			return new {Data =data, Header =header };
+		}
+		/// <summary>
+		/// 信件来源统计导出
+		/// </summary>
+		/// <returns></returns>
+		[HttpPost("order_source_list/_export")]
+		public async Task<FileStreamResult> QueryOrderSourceReportList([FromBody] ExportExcelDto<QueryOrderSourceRequest> dto)
+		{
+			var data = await _orderApplication.QueryOrderSourceList(dto.QueryDto);
+			dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+			var dtos = data
+				.Select(stu => _mapper.Map(stu, typeof(OrderSourceTimeVo), dynamicClass))
+				.Cast<object>()
+				.ToList();
+			var stream = ExcelHelper.CreateStream(dtos);
+			return ExcelStreamResult(stream, "信件来源统计列表数据");
+		}
+
+		/// <summary>
+		/// 信件来源统计明细
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("order_source_detail_report")]
+		public async Task<PagedDto<OrderDto>> QueryOrderSourceDetailReport([FromQuery] QueryOrderSourceDetailRequest dto)
+		{
+			var (total, items) = await _orderApplication.QueryOrderSourceDetail(dto).ToPagedListAsync(dto, HttpContext.RequestAborted);
+			return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
+		}
+		/// <summary>
+		/// 信件来源统计明细导出
+		/// </summary>
+		/// <returns></returns>
+		[HttpPost("order_source_detail/_export")]
+		public async Task<FileStreamResult> QueryOrderSourceDetailReport([FromBody] ExportExcelDto<QueryOrderSourceDetailRequest> dto)
+		{
+			var query = _orderApplication.QueryOrderSourceDetail(dto.QueryDto);
+			List<Order> orders;
+			if (dto.IsExportAll)
+			{
+				orders = await query.ToListAsync(HttpContext.RequestAborted);
+			}
+			else
+			{
+				var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+				orders = items;
+			}
+			var ordersDtos = _mapper.Map<ICollection<OrderDto>>(orders);
+			dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+			var dtos = ordersDtos
+				.Select(stu => _mapper.Map(stu, typeof(OrderDto), dynamicClass))
+				.Cast<object>()
+				.ToList();
+			var stream = ExcelHelper.CreateStream(dtos);
+			return ExcelStreamResult(stream, "信件来源统计明细数据");
+		}
+	}
 }

+ 6 - 5
src/Hotline.Api/Controllers/IPPbxController.cs

@@ -420,7 +420,7 @@ namespace Hotline.Api.Controllers
 
             //获取关联 工单或是回访
             //var order = await _orderRepository.GetAsync(x => x.CallId == model.CallAccept, HttpContext.RequestAborted);//由CallAccept改为OtherAccept
-            var order = await _orderRepository.GetAsync(x => x.CallId == model.OtherAccept, HttpContext.RequestAborted);
+            var order = await _orderRepository.GetAsync(x => x.CallId == model.OtherAccept && string.IsNullOrEmpty(x.CallId) == false, HttpContext.RequestAborted);
             if (order != null)
             {
                 model.CallOrderType = ECallOrderType.Order;
@@ -434,13 +434,14 @@ namespace Hotline.Api.Controllers
                 if (teAny)
                 {
                     var quality = await _qualiteyRepository.Queryable().Where(x => x.OrderId == order.Id && x.Source == Share.Enums.Quality.EQualitySource.Accepted).FirstAsync();
-                    await _aiQualityService.CreateAiOrderQualityTask(quality, model, order, HttpContext.RequestAborted);
+                    var setting = _systemSettingCacheManager.GetSetting(SettingConstants.ViteRecordPrefix);
+					await _aiQualityService.CreateAiOrderQualityTask(quality, model, order, setting?.SettingValue[0], HttpContext.RequestAborted);
                 }
             }
             else
             {
                 //var orderVisit = await _orderVisitRepository.GetAsync(x => x.CallId == model.CallAccept, HttpContext.RequestAborted);//由CallAccept改为OtherAccept
-                var orderVisit = await _orderVisitRepository.GetAsync(x => x.CallId == model.OtherAccept, HttpContext.RequestAborted);
+                var orderVisit = await _orderVisitRepository.GetAsync(x => x.CallId == model.OtherAccept && string.IsNullOrEmpty(x.CallId) == false, HttpContext.RequestAborted);
                 if (orderVisit != null)
                 {
                     model.CallOrderType = ECallOrderType.Visit;
@@ -547,8 +548,8 @@ namespace Hotline.Api.Controllers
                 .WhereIF(!string.IsNullOrEmpty(dto.OrderNo), x => x.Order.No.Contains(dto.OrderNo))
                 .WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.Order.Title.Contains(dto.Title))
                 .WhereIF(!string.IsNullOrEmpty(dto.Gateway), x => x.Gateway.Contains(dto.Gateway))
-                // .WhereIF(dto.StartTime.HasValue, x => x.CreatedTime >= dto.StartTime)
-                //.WhereIF(dto.EndTime.HasValue, x => x.CreatedTime <= dto.EndTime)
+                
+                .WhereIF(dto.IsAiAnswered == true,x=>string.IsNullOrEmpty(x.UserId) == true)
                 .OrderByDescending(x => x.CreatedTime)
                 .ToPagedListAsync(dto.PageIndex, dto.PageSize);
             return new PagedDto<TrCallDto>(total, _mapper.Map<IReadOnlyList<TrCallDto>>(items));

+ 14 - 5
src/Hotline.Api/Controllers/IdentityController.cs

@@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using System.Security.Cryptography;
 using System.Text;
+using Hotline.Api.Filter;
+using Hotline.Application.Systems;
 using XC.RSAUtil;
 using XF.Domain.Constants;
 using XF.Domain.Exceptions;
@@ -15,7 +17,9 @@ public class IdentityController : BaseController
 {
     private readonly IIdentityAppService _identityAppService;
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
-    private const string PublicKey = @"-----BEGIN PUBLIC KEY-----
+    private readonly ISystemLogApplication _iSystemLogApplication;
+
+	private const string PublicKey = @"-----BEGIN PUBLIC KEY-----
 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgw+/x6IQPkH0A4eoF63j
 kLThsOXWyNBdcL9LATGy/G1yTHOr1RyKJB//iNug+V8DIoIHuFTlhgLHDbSqxvRW
 MONxIIF289riS6bDI4Ox/pFmOfmElFRk0lKGihaTE2Aefd6g/N+RfLLaHWztY+/v
@@ -52,11 +56,12 @@ Q9PP8NTEmKqdI3WVFYqW/OlOFC6sjiscTOOn9Tc5Mrcn8ocCjAPjkhkCCVRMiJnv
 jxrWXHbT1FB6DqkdOnBbQqS1Azqz5HxLlSyEK3F60e3SgB5iZsDZ
 -----END RSA PRIVATE KEY-----";
 
-    public IdentityController(IIdentityAppService identityAppService, ISystemSettingCacheManager systemSettingCacheManager)
+    public IdentityController(IIdentityAppService identityAppService, ISystemSettingCacheManager systemSettingCacheManager, ISystemLogApplication iSystemLogApplication)
     {
         _identityAppService = identityAppService;
         _systemSettingCacheManager = systemSettingCacheManager;
-    }
+        _iSystemLogApplication = iSystemLogApplication;
+	}
 
     /// <summary>
     /// 登录
@@ -65,10 +70,14 @@ jxrWXHbT1FB6DqkdOnBbQqS1Azqz5HxLlSyEK3F60e3SgB5iZsDZ
     /// <returns></returns>
     [AllowAnonymous]
     [HttpPost("login")]
-    public async Task<string> Login([FromBody] LoginDto dto)
+    [LogFilter("",false)]
+	public async Task<string> Login([FromBody] LoginDto dto)
     {
         dto = Decrypt(dto);
-        return await _identityAppService.LoginAsync(dto, HttpContext.RequestAborted);
+        var res = await _identityAppService.LoginAsync(dto, HttpContext.RequestAborted);
+        dto.Password = string.Empty;
+        await _iSystemLogApplication.AddLog("账号登录", res, dto, HttpContext,dto.Username);
+		return res ;
     }
 
     [AllowAnonymous]

+ 37 - 36
src/Hotline.Api/Controllers/OrderController.cs

@@ -668,7 +668,7 @@ public class OrderController : BaseController
         var dissatisfiedReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.DissatisfiedReason);
         var visitManner = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitManner).Where(x => x.DicDataValue != "-1");
         //var callRecord = await _trCallRecordRepository.GetAsync(x => x.CallAccept == orderVisit.CallId); //由CallAccept改为OtherAccept
-        var callRecord = await _trCallRecordRepository.GetAsync(x => x.OtherAccept == orderVisit.CallId);
+        var callRecord = await _trCallRecordRepository.GetAsync(x => x.OtherAccept == orderVisit.CallId && string.IsNullOrEmpty(x.OtherAccept)==false,HttpContext.RequestAborted);
         var recordingFileUrl = "";
         var recordingBaseAddress = "";
         var recordingAbsolutePath = "";
@@ -1340,14 +1340,14 @@ public class OrderController : BaseController
     public async Task<PagedDto<OrderDelayDto>> DelayList([FromQuery] DelayListDto dto)
     {
         var (total, items) = await _orderDelayRepository.Queryable(canView: true)
-            .Includes(x => x.Order)
-            .Includes(x => x.Workflow)
+            .Includes(d => d.Order)
+            .Includes(d => d.Workflow)
             .WhereIF(!string.IsNullOrEmpty(dto.Keyword),
                 d => d.Order.Title.Contains(dto.Keyword!) || d.Order.No.Contains(dto.Keyword!))
-            .WhereIF(dto.IsApply == true, x => x.DelayState != EDelayState.Examining)
-            .WhereIF(dto.IsApply == false, x => x.DelayState == EDelayState.Examining)
-            .WhereIF(dto.DelayState != null, x => x.DelayState == dto.DelayState)
-            .OrderByDescending(x => x.ApplyDelayTime)
+            .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)
+            .OrderByDescending(d => d.ApplyDelayTime)
             .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
 
         return new PagedDto<OrderDelayDto>(total, _mapper.Map<IReadOnlyList<OrderDelayDto>>(items));
@@ -1364,15 +1364,15 @@ public class OrderController : BaseController
         var isHandled = dto.IsApply.HasValue && dto.IsApply.Value;
         var (total, items) = await _orderDelayRepository
             .Queryable(hasHandled: !isHandled)
-            .Includes(x => x.Order)
-            .Includes(x => x.Workflow)
+            .Includes(d => d.Order)
+            .Includes(d => d.Workflow)
             .WhereIF(!string.IsNullOrEmpty(dto.Keyword),
                 d => d.Order.Title.Contains(dto.Keyword!) || d.No.Contains(dto.Keyword!))
-            .WhereIF(dto.IsApply == true, x => x.DelayState != EDelayState.Examining)
-            .WhereIF(dto.IsApply == false, x => x.DelayState == EDelayState.Examining)
+            .WhereIF(dto.IsApply == true, d => d.DelayState != EDelayState.Examining)
+            .WhereIF(dto.IsApply == false, d => d.DelayState == EDelayState.Examining)
             //.WhereIF(dto.DelayState != null, x => x.DelayState == dto.DelayState)
             //.Where(x=>x.DelayState == EDelayState.Examining)
-            .OrderByDescending(x => x.ApplyDelayTime)
+            .OrderByDescending(d => d.ApplyDelayTime)
             .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
 
         return new PagedDto<OrderDelayDto>(total, _mapper.Map<IReadOnlyList<OrderDelayDto>>(items));
@@ -1388,9 +1388,9 @@ public class OrderController : BaseController
     public async Task<OrderDelayDto> DelayEntity(string id)
     {
         var model = await _orderDelayRepository.Queryable()
-            .Includes(x => x.Order)
-            .Includes(x => x.Workflow)
-            .FirstAsync(x => x.Id == id);
+            .Includes(d => d.Order)
+            .Includes(d => d.Workflow)
+            .FirstAsync(d => d.Id == id);
         var rspModel = _mapper.Map<OrderDelayDto>(model);
         rspModel.IsCanHandle = model.CanHandle(_sessionContext.RequiredUserId, _sessionContext.RequiredOrgId);
         rspModel.Handle = false;
@@ -1531,27 +1531,27 @@ public class OrderController : BaseController
         var handler = dto.source == 1 && dto.Status is EScreenStatus.Apply;
 
         var query = _orderScreenRepository.Queryable(canView: view, hasHandled: handler)
-            .Includes(x => x.Order)
-            .Includes(x => x.VisitDetail)
-            .Includes(x => x.Visit, d => d.Order)
-            .Includes(x => x.Workflow)
+            .Includes(d => d.Order)
+            .Includes(d => d.VisitDetail)
+            .Includes(d => d.Visit, v => v.Order)
+            .Includes(d => d.Workflow)
             .WhereIF(!string.IsNullOrEmpty(dto.Keyword),
-                x => x.Visit.Order.Title.Contains(dto.Keyword!) || x.Visit.Order.No.Contains(dto.Keyword!));
+                d => d.Visit.Order.Title.Contains(dto.Keyword!) || d.Visit.Order.No.Contains(dto.Keyword!));
         if (dto.Status is EScreenStatus.Apply)
         {
-            query.Where(x => (x.Status == EScreenStatus.Apply || x.Status == EScreenStatus.Approval));
+            query.Where(d => (d.Status == EScreenStatus.Apply || d.Status == EScreenStatus.Approval));
         }
         if (dto.Status.HasValue && dto.Status == EScreenStatus.MyHandle)
         {
-            query.Where(x => (x.Status != EScreenStatus.Apply));
+            query.Where(d => (d.Status != EScreenStatus.Apply));
         }
         var (total, items) = await query
             //.WhereIF(dto.Status.HasValue && dto.Status == EScreenStatus.MyHandle,
             //    x => x.Status != EScreenStatus.Apply && x.CreatorId == _sessionContext.UserId)
-            .WhereIF(dto.CreationTimeStart.HasValue, x => x.CreationTime >= dto.CreationTimeStart)
-            .WhereIF(dto.CreationTimeEnd.HasValue, x => x.CreationTime <= dto.CreationTimeEnd)
-            .WhereIF(!string.IsNullOrEmpty(dto.OrderId), x => x.OrderId == dto.OrderId)
-            .OrderByDescending(x => x.CreationTime)
+            .WhereIF(dto.CreationTimeStart.HasValue, d => d.CreationTime >= dto.CreationTimeStart)
+            .WhereIF(dto.CreationTimeEnd.HasValue, d => d.CreationTime <= dto.CreationTimeEnd)
+            .WhereIF(!string.IsNullOrEmpty(dto.OrderId), d => d.OrderId == dto.OrderId)
+            .OrderByDescending(d => d.CreationTime)
             .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
         return new PagedDto<OrderScreenListDto>(total, _mapper.Map<IReadOnlyList<OrderScreenListDto>>(items));
     }
@@ -2390,7 +2390,7 @@ public class OrderController : BaseController
 
         //工单ID跟通话记录相关联
         //var callRecord = await _trCallRecordRepository.GetAsync(p => p.CallAccept == order.CallId, HttpContext.RequestAborted);//由CallAccept改为OtherAccept
-        var callRecord = await _trCallRecordRepository.GetAsync(p => p.OtherAccept== order.CallId, HttpContext.RequestAborted);
+        var callRecord = await _trCallRecordRepository.GetAsync(p => p.OtherAccept== order.CallId && string.IsNullOrEmpty(p.OtherAccept) ==false, HttpContext.RequestAborted);
         if (callRecord != null && string.IsNullOrEmpty(callRecord.ExternalId))
         {
             callRecord.ExternalId = order.Id;
@@ -2910,9 +2910,9 @@ public class OrderController : BaseController
             .WhereIF(dto.ExpiredOrAlmostOverdue.HasValue && dto.ExpiredOrAlmostOverdue == true, d => (d.ExpiredTime < DateTime.Now && d.Status < EOrderStatus.Filed) || (d.ExpiredTime < d.ActualHandleTime && d.Status >= EOrderStatus.Filed)) //超期 未办
             .WhereIF(dto.ExpiredOrAlmostOverdue.HasValue && dto.ExpiredOrAlmostOverdue == false, d => d.NearlyExpiredTime < DateTime.Now && d.ExpiredTime > DateTime.Now)//即将超期 未办
             .Where(d => d.Source < ESource.MLSQ || d.Source > ESource.WZSC)
-            .Where(d => d.Status != EOrderStatus.BackToProvince)
+            .Where(d => d.Status != EOrderStatus.BackToProvince && d.Status < EOrderStatus.Filed)
             //.Where(d => SqlFunc.Subqueryable<OrderSpecial>().Where(os => os.OrderId == d.Id).NotAny())
-            .Where(d => d.OrderSpecials.Any() == false || d.OrderSpecials.Any(s => s.State == 0) == false)
+            .Where(d => d.OrderSpecials.Any() == false || d.OrderSpecials.Any(s => s.State > 0))
             .WhereIF(dto.StartTime.HasValue, d => d.StartTime >= dto.StartTime)
             .WhereIF(dto.EndTime.HasValue, d => d.StartTime <= dto.EndTime)
             .OrderByDescending(d => d.StartTime)
@@ -2948,7 +2948,7 @@ public class OrderController : BaseController
             .WhereIF(dto.StartTime.HasValue, d => d.CreationTime >= dto.StartTime)
             .WhereIF(dto.EndTime.HasValue, d => d.CreationTime <= dto.EndTime)
             .Where(x => x.Source < ESource.MLSQ || x.Source > ESource.WZSC)
-            .Where(x => x.Status != EOrderStatus.BackToProvince)
+            .Where(x => x.Status != EOrderStatus.BackToProvince && x.Status < EOrderStatus.Filed)
             .OrderBy(d => d.Status)
             .OrderByIF(dto.IsHandled == true, d => d.StartTime, OrderByType.Desc)
             .OrderByIF(dto.IsHandled == false, d => d.CreationTime, OrderByType.Desc)
@@ -2972,7 +2972,7 @@ public class OrderController : BaseController
         var (total, items) = await _orderRepository.Queryable(canView: false)
             .Where(x => x.Workflow.Steps.Any(s => s.Status < EWorkflowStepStatus.Handled && s.HandlerOrgId == OrgSeedData.CenterId))
             .Where(x => x.Source < ESource.MLSQ || x.Source > ESource.WZSC)
-            .Where(x => x.Status != EOrderStatus.BackToProvince)
+            .Where(x => x.Status != EOrderStatus.BackToProvince && x.Status < EOrderStatus.Filed)
             .WhereIF(!string.IsNullOrEmpty(dto.No), x => x.No!.Contains(dto.No!))
             .WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.Title!.Contains(dto.Title!))
             .WhereIF(dto is { StCreationTime: not null, EndCreationTime: not null }, x => x.CreationTime >= dto.StCreationTime && x.CreationTime <= dto.EndCreationTime)
@@ -3016,7 +3016,6 @@ public class OrderController : BaseController
 
     public async Task<Object> QueryWaitedHome([FromQuery] QueryOrderWaitedDto dto)
     {
-
         var isHandled = dto.IsHandled.HasValue && dto.IsHandled.Value;
         if (dto.EndTime.HasValue)
             dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
@@ -3032,7 +3031,7 @@ public class OrderController : BaseController
             .WhereIF(dto.StartTime.HasValue, d => d.CreationTime >= dto.StartTime)
             .WhereIF(dto.EndTime.HasValue, d => d.CreationTime <= dto.EndTime)
             .Where(d => d.Source < ESource.MLSQ || d.Source > ESource.WZSC)
-            .Where(d => d.Status != EOrderStatus.BackToProvince)
+            .Where(d => d.Status != EOrderStatus.BackToProvince &&  d.Status < EOrderStatus.Filed)
             //.Where(d => SqlFunc.Subqueryable<OrderSpecial>().Where(os => os.OrderId == d.Id).NotAny())
             .Where(d => d.OrderSpecials.Any() == false || d.OrderSpecials.Any(s => s.State == 0) == false)
             .OrderByDescending(d => d.StartTime)
@@ -3040,7 +3039,9 @@ public class OrderController : BaseController
 
         var page1 = new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
 
-        EOrderStatus[] handleStatuses = EnumExts.GetFields<EOrderStatus>().Select(d => (EOrderStatus)d.Key).ToArray();
+        dto.IsHandled = false;
+        isHandled = dto.IsHandled.HasValue && dto.IsHandled.Value;
+		EOrderStatus[] handleStatuses = EnumExts.GetFields<EOrderStatus>().Select(d => (EOrderStatus)d.Key).ToArray();
         handleStatuses = handleStatuses.WhereIF(dto.IsHandled.HasValue,
                 d => dto.IsHandled!.Value
                     ? d is not EOrderStatus.WaitForAccept and not EOrderStatus.BackToUnAccept and not EOrderStatus.SpecialToUnAccept
@@ -3057,13 +3058,13 @@ public class OrderController : BaseController
             .WhereIF(dto.ExpiredOrAlmostOverdue.HasValue && dto.ExpiredOrAlmostOverdue == true, d => (d.ExpiredTime < DateTime.Now && d.Status < EOrderStatus.Filed) || (d.ExpiredTime < d.ActualHandleTime && d.Status >= EOrderStatus.Filed)) //超期 未办
             .WhereIF(dto.ExpiredOrAlmostOverdue.HasValue && dto.ExpiredOrAlmostOverdue == false, d => d.NearlyExpiredTime < DateTime.Now && d.ExpiredTime > DateTime.Now)//即将超期 未办
             .Where(x => x.Source < ESource.MLSQ || x.Source > ESource.WZSC)
-            .Where(x => x.Status != EOrderStatus.BackToProvince)
+            .Where(x => x.Status != EOrderStatus.BackToProvince && x.Status < EOrderStatus.Filed)
             .OrderBy(d => d.Status)
             .OrderByIF(dto.IsHandled == true, d => d.StartTime, OrderByType.Asc)
             .OrderByIF(dto.IsHandled == false, d => d.CreationTime, OrderByType.Desc)
             .ToPagedListAsync(dto, HttpContext.RequestAborted);
 
-        var page2 = new PagedDto<OrderDto>(total2, _mapper.Map<IReadOnlyList<OrderDto>>(items2));
+		var page2 = new PagedDto<OrderDto>(total2, _mapper.Map<IReadOnlyList<OrderDto>>(items2));
 
         return new { Waited = page1, Sign = page2 };
     }

+ 7 - 3
src/Hotline.Api/Controllers/QualityController.cs

@@ -25,6 +25,7 @@ using Hotline.Ai.Quality;
 using Newtonsoft.Json;
 using Polly;
 using Hotline.Api.Filter;
+using XF.Domain.Constants;
 
 namespace Hotline.Api.Controllers
 {
@@ -44,6 +45,7 @@ namespace Hotline.Api.Controllers
 		private readonly IOrderRepository _orderRepository;
 		private readonly IAiQualityService _aiQualityService;
 		private readonly ILogger<QualityController> _logger;
+		private readonly ISystemSettingCacheManager _systemSettingCacheManager;
 
 		public QualityController(
 			ISessionContext sessionContext,
@@ -59,8 +61,8 @@ namespace Hotline.Api.Controllers
 			IQualityApplication qualityApplication,
 			IOrderRepository orderRepository,
 			IAiQualityService aiQualityService,
-			ILogger<QualityController> logger
-
+			ILogger<QualityController> logger,
+			ISystemSettingCacheManager systemSettingCacheManager
 		)
 		{
 			_sessionContext = sessionContext;
@@ -77,6 +79,7 @@ namespace Hotline.Api.Controllers
 			_orderRepository = orderRepository;
 			_aiQualityService = aiQualityService;
 			_logger = logger;
+			_systemSettingCacheManager = systemSettingCacheManager;
 		}
 		#region 质检管理
 		/// <summary>
@@ -673,7 +676,8 @@ namespace Hotline.Api.Controllers
 				quality.AiQuality = true;
                 //var call = await _trCallRecordRepository.GetAsync(x => x.CallAccept == order.CallId, HttpContext.RequestAborted); //由CallAccept改为OtherAccept
                 var call = await _trCallRecordRepository.GetAsync(x => x.OtherAccept == order.CallId, HttpContext.RequestAborted);
-                await _aiQualityService.CreateAiOrderQualityTask(quality, call, order, HttpContext.RequestAborted);
+                var setting = _systemSettingCacheManager.GetSetting(SettingConstants.ViteRecordPrefix);
+				await _aiQualityService.CreateAiOrderQualityTask(quality, call, order, setting?.SettingValue[0], HttpContext.RequestAborted);
 				await _qualitey.UpdateAsync(quality, HttpContext.RequestAborted);
 			}
 		}

+ 86 - 77
src/Hotline.Api/Controllers/TestController.cs

@@ -26,6 +26,7 @@ using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Dtos.Realtime;
+using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Settings;
 using Hotline.Share.Mq;
 using Hotline.Tools;
@@ -90,6 +91,8 @@ public class TestController : BaseController
     private readonly IExportApplication _exportApplication;
     private readonly IAiVisitService _aiVisitService;
     private readonly IRepository<WorkflowTrace> _workflowTraceRepository;
+
+    private readonly IRepository<WorkflowStep> _workflowStepRepository;
     //private readonly IRepository<WorkflowStepHandler> _workflowStepHandleRepository;
 
     private readonly IRepository<SystemOrganize> _systemOrganizeRepository;
@@ -129,6 +132,7 @@ public class TestController : BaseController
         IExportApplication exportApplication,
         IAiVisitService aiVisitService,
         IRepository<WorkflowTrace> workflowTraceRepository,
+        IRepository<WorkflowStep> workflowStepRepository,
         //IRepository<WorkflowStepHandler> workflowStepHandleRepository,
         IRepository<SystemOrganize> systemOrganizeRepository,
         IOrderRepository orderRepository,
@@ -159,6 +163,7 @@ public class TestController : BaseController
         _exportApplication = exportApplication;
         _aiVisitService = aiVisitService;
         _workflowTraceRepository = workflowTraceRepository;
+        _workflowStepRepository = workflowStepRepository;
         //_workflowStepHandleRepository = workflowStepHandleRepository;
         _systemOrganizeRepository = systemOrganizeRepository;
         _orderRepository = orderRepository;
@@ -467,96 +472,100 @@ public class TestController : BaseController
         //await _workflowStepHandleRepository.AddRangeAsync(stepHandlers, HttpContext.RequestAborted);
     }
 
+    /// <summary>
+    /// 处理FlowAssignType
+    /// </summary>
+    /// <returns></returns>
     [AllowAnonymous]
     [HttpPost("t3")]
     public async Task TestExportExcel()
     {
-        // var items = await _workflowTraceRepository.Queryable()
-        //     .Where(d => !SqlFunc.Subqueryable<WorkflowStepHandler>().Where(x => x.WorkflowStepId == d.StepId).Any())
-        //     .ToListAsync(HttpContext.RequestAborted);
-
-        //var handlerStepIds = await _workflowStepHandleRepository
-        //    .Queryable()
-        //    .Select(d=>d.WorkflowStepId)
-        //    .ToListAsync(HttpContext.RequestAborted);
-        //_logger.LogInformation($"handler stepids: {handlerStepIds.Count}");
-
-        //var items = await _workflowTraceRepository
-        //    .Queryable()
-        //    .Where(d => !handlerStepIds.Distinct().Contains(d.StepId))
-        //    .ToListAsync(HttpContext.RequestAborted);
-        //_logger.LogInformation($"traces.count: {items.Count}");
-
-        ////var items = await _workflowTraceRepository.Queryable()
-        ////    .LeftJoin<WorkflowStepHandler>((t, h) => t.StepId == h.WorkflowStepId)
-        ////    .Where((t, h) => h == null)
-        ////    .Select((t, h) => new { t, h })
-        ////    .ToListAsync(HttpContext.RequestAborted);
-
-        //var handlerIds = items.Select(d => d)
-        //    .SelectMany(d => d.Handlers)
-        //    .Select(d => d.Key)
-        //    .Distinct()
-        //    .ToList();
-        //var users = await _userRepository.Queryable()
-        //    .Includes(d => d.Organization)
-        //    .Where(d => handlerIds.Contains(d.Id))
-        //    .ToListAsync(HttpContext.RequestAborted);
-        //var orgs = await _systemOrganizeRepository.Queryable()
-        //    .Where(d => handlerIds.Contains(d.Id))
-        //    .ToListAsync(HttpContext.RequestAborted);
-
-        //var handlers = new List<WorkflowStepHandler>();
-        //var updateTraces = new List<WorkflowTrace>();
-        //foreach (var item in items)
+        var steps = await _workflowStepRepository.Queryable()
+            .Includes(d => d.WorkflowTrace)
+            .Where(d => SqlFunc.Exists(d.WorkflowTrace.Id) &&
+                        d.FlowAssignType == null &&
+                        SqlFunc.JsonArrayLength(d.Handlers) == 1)
+            .ToListAsync(HttpContext.RequestAborted);
+        //foreach (var step in steps)
         //{
-        //    var trace = item;
-        //    foreach (var traceHandler in trace.Handlers)
+        //    var handlerCount = step.Handlers.Count;
+        //    if (handlerCount == 1)
+        //    {
+        //        step.FlowAssignType =
+        //            step.Handlers.First().Key.Length == 36 ? EFlowAssignType.User : EFlowAssignType.Org;
+        //        step.WorkflowTrace.FlowAssignType = step.FlowAssignType;
+        //    }
+        //    else if (handlerCount > 1)
         //    {
-        //        if (!trace.FlowAssignType.HasValue)
-        //        {
-        //            trace.FlowAssignType = traceHandler.Key.Length == 36 ? EFlowAssignType.User : EFlowAssignType.Org;
-        //            updateTraces.Add(trace);
-        //        }
 
-        //        if (trace.FlowAssignType == EFlowAssignType.User)
-        //        {
-        //            var user = users.FirstOrDefault(d => d.Id == traceHandler.Key);
-        //            if (user != null)
-        //            {
-        //                var stepHandler = WorkflowStepHandler.Create(trace.WorkflowId, trace.ExternalId,
-        //                    trace.FlowAssignType.Value, user.Id, user.Name, user.OrgId, user.Organization.Name);
-        //                stepHandler.WorkflowStepId = trace.StepId;
-        //                handlers.Add(stepHandler);
-        //            }
-        //        }
-        //        else
-        //        {
-        //            var org = orgs.FirstOrDefault(d => d.Id == traceHandler.Key);
-        //            if (org != null)
-        //            {
-        //                var stepHandler = WorkflowStepHandler.Create(trace.WorkflowId, trace.ExternalId,
-        //                    trace.FlowAssignType.Value, orgId: org.Id, orgName: org.Name);
-        //                stepHandler.WorkflowStepId = trace.StepId;
-        //                handlers.Add(stepHandler);
-        //            }
-        //        }
+        //    }
+        //    else
+        //    {
+        //        _logger.LogError($"无办理对象, {handlerCount}");
         //    }
         //}
 
-        //_logger.LogInformation($"待更新traces: {updateTraces.Count}");
-        //if (updateTraces.Any())
-        //    await _workflowTraceRepository.UpdateRangeAsync(updateTraces, HttpContext.RequestAborted);
-
-        //_logger.LogInformation($"待更新traces: {handlers.Count}");
-        //if (handlers.Any())
-        //    await _workflowStepHandleRepository.AddRangeAsync(handlers, HttpContext.RequestAborted);
+        var list = new List<WorkflowStep>();
+        foreach (var step in steps)
+        {
+            var handler = step.Handlers.First();
+            if (string.IsNullOrEmpty(handler.Key)) continue;
+            step.FlowAssignType =
+                        handler.Key.Length == 36 ? EFlowAssignType.User : EFlowAssignType.Org;
+            step.WorkflowTrace.FlowAssignType = step.FlowAssignType;
+            list.Add(step);
+        }
 
-        ///////
+        await _workflowStepRepository
+            .UpdateNav(list)
+            .Include(d => d.WorkflowTrace)
+            .ExecuteCommandAsync();
+        //.UpdateRangeAsync(steps, HttpContext.RequestAborted);
+    }
 
-        var orders = await _orderRepository.Queryable(hasHandled: true)
-            //.Where(entity=>entity.CreationTime>= DateTime.Now.AddMonths(-1))
+    /// <summary>
+    /// 赋值handlerId
+    /// </summary>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [HttpPost("t4")]
+    public async Task TestExportExcel1()
+    {
+        var steps = await _workflowStepRepository.Queryable()
+            .Includes(d => d.WorkflowTrace)
+            .Where(d => SqlFunc.Exists(d.WorkflowTrace.Id) &&
+                        d.Status != EWorkflowStepStatus.Handled &&
+                        d.HandlerId == null &&
+                        d.FlowAssignType != null &&
+                        SqlFunc.JsonArrayLength(d.Handlers) == 1)
             .ToListAsync(HttpContext.RequestAborted);
+
+        var list = new List<WorkflowStep>();
+        foreach (var step in steps)
+        {
+            var handler = step.Handlers.First();
+            if (string.IsNullOrEmpty(handler.Key)) continue;
+            if (step.FlowAssignType == EFlowAssignType.User)
+            {
+                step.HandlerId = handler.Key;
+                step.HandlerName = handler.Value;
+                step.WorkflowTrace.HandlerId = step.HandlerId;
+                step.WorkflowTrace.HandlerName = step.HandlerName;
+            }
+            else
+            {
+                step.HandlerOrgId = handler.Key;
+                step.HandlerOrgName = handler.Value;
+                step.WorkflowTrace.HandlerOrgId = step.HandlerOrgId;
+                step.WorkflowTrace.HandlerOrgName = step.HandlerOrgName;
+            }
+            list.Add(step);
+        }
+
+        await _workflowStepRepository
+            .UpdateNav(list)
+            .Include(d => d.WorkflowTrace)
+            .ExecuteCommandAsync();
     }
 
     [HttpGet("rsa")]

+ 8 - 8
src/Hotline.Api/Controllers/WorkflowController.cs

@@ -386,14 +386,14 @@ public class WorkflowController : BaseController
         await _workflowDomainService.TerminateAsync(dto, HttpContext.RequestAborted);
     }
 
-    /// <summary>
-    /// 撤销流程
-    /// </summary>
-    [HttpPost("cancel")]
-    public async Task Cancel([FromBody] CancelDto dto)
-    {
-        await _workflowDomainService.CancelAsync(dto, _sessionContext, HttpContext.RequestAborted);
-    }
+    ///// <summary>
+    ///// 撤销流程
+    ///// </summary>
+    //[HttpPost("cancel")]
+    //public async Task Cancel([FromBody] CancelDto dto)
+    //{
+    //    await _workflowDomainService.CancelAsync(dto, DateTime.Now, _sessionContext, HttpContext.RequestAborted);
+    //}
 
     /// <summary>
     /// 否决

+ 2 - 2
src/Hotline.Api/config/appsettings.Development.json

@@ -16,7 +16,7 @@
     }
   },
   "ConnectionStrings": {
-    "Hotline": "PORT=5432;DATABASE=hotline;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;",
+    "Hotline": "PORT=5432;DATABASE=hotline_dev;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;",
     "Redis": "110.188.24.182:50179",
     "MongoDB": "mongodb://192.168.100.121:27017",
     "Wex": "server=222.212.82.225;Port=4509;Database=fs_kft;Uid=root;Pwd=Wex@12345;"
@@ -25,7 +25,7 @@
     "Host": "110.188.24.182",
     "Port": 50179,
     //"Password": "fengwo22@@",
-    "Database": 3
+    "Database": 5
   },
   "Swagger": true,
   "Cors": {

+ 2 - 2
src/Hotline.Api/config/appsettings.json

@@ -16,7 +16,7 @@
     }
   },
   "ConnectionStrings": {
-    "Hotline": "PORT=5432;DATABASE=hotline;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;",
+    "Hotline": "PORT=5432;DATABASE=hotline_dev;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;",
     "Redis": "110.188.24.182:50179,password=fengwo22@@",
     "MongoDB": "mongodb://192.168.100.121:27017",
     "Wex": "server=222.212.82.225;Port=4509;Database=fs_kft;Uid=root;Pwd=Wex@12345;"
@@ -25,7 +25,7 @@
     "Host": "110.188.24.182",
     "Port": 50179,
     "Password": "fengwo22@@",
-    "Database": 3
+    "Database": 5
   },
   "Swagger": true,
   "Cors": {

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

@@ -52,7 +52,7 @@ namespace Hotline.Application.FlowEngine
         /// <summary>
         /// 跳转至结束节点(无视流程模板配置以及当前办理对象)
         /// </summary>
-        Task JumpToEndAsync(ISessionContext current, string workflowId, string opinion, List<FileDto> files,
+        Task JumpToEndAsync(ISessionContext current, string workflowId, string opinion, List<FileDto> files, DateTime? expiredTime,
             EReviewResult reviewResult = EReviewResult.Unknown, CancellationToken cancellationToken = default);
 
         ////////

+ 8 - 9
src/Hotline.Application/FlowEngine/WorkflowApplication.cs

@@ -317,7 +317,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
         if (targetStepDefine.StepType is EStepType.End)
             throw UserFriendlyException.SameMessage("结束节点不支持撤回");
         //var isStartCountersign = targetStepDefine.CouldPrevStartCountersign(dto.NextHandlers.Count);
-        var flowAssignInfo = await GetNextStepFlowAssignInfoByDefineAsync(targetStepDefine, dto.IsStartCountersign,
+        var flowAssignInfo = await GetNextStepFlowAssignInfoByDefineAsync(targetStepDefine, dto.HandlerType, dto.IsStartCountersign,
             dto.NextHandlers.Select(d => new Kv(d.Key, d.Value)).ToList(), cancellationToken);
 
         //var stepHandlers = await GetNextStepHandlersAsync(workflow, targetStepDefine, dto, cancellationToken);
@@ -353,7 +353,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     /// <summary>
     /// 跳转至结束节点(无视流程模板配置以及当前办理对象,直接跳至结束节点)
     /// </summary>
-    public async Task JumpToEndAsync(ISessionContext current, string workflowId, string opinion, List<FileDto> files,
+    public async Task JumpToEndAsync(ISessionContext current, string workflowId, string opinion, List<FileDto> files, DateTime? expiredTime,
         EReviewResult reviewResult = EReviewResult.Unknown, CancellationToken cancellationToken = default)
     {
         var workflow = await _workflowDomainService.GetWorkflowAsync(workflowId, withDefine: true, withSteps: true,
@@ -398,7 +398,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
         await _workflowTraceRepository.UpdateRangeAsync(unhandleTraces, cancellationToken);
 
         await _workflowDomainService.EndAsync(workflow, dto,
-            endStepDefine, unhandleSteps.First(), current, cancellationToken);
+            endStepDefine, unhandleSteps.First(), current, expiredTime, cancellationToken);
     }
 
     /// <summary>
@@ -1108,7 +1108,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
 
         if (isStartCountersign)
         {
-            var assignType = FlowAssignInfo.GetAssignType(dto.HandlerType);
+            var assignType = FlowAssignInfo.GetAssignType(dto.HandlerType, dto.NextHandlers.Any());
             //按会签策略判断,目前所有策略为org
             return FlowAssignInfo.Create(assignType, handlers, isStartCountersign);
         }
@@ -1146,7 +1146,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 }
                 else
                 {
-                    var assignType = FlowAssignInfo.GetAssignType(dto.HandlerType);
+                    var assignType = FlowAssignInfo.GetAssignType(dto.HandlerType, dto.NextHandlers.Any());
                     //按会签策略判断,目前所有策略为org
                     return FlowAssignInfo.Create(assignType, handlers, isStartCountersign);
                 }
@@ -1156,7 +1156,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
         if (isNextDynamic)
             return FlowAssignInfo.Create(EFlowAssignType.Org, handlers, isStartCountersign);
 
-        return await GetNextStepFlowAssignInfoByDefineAsync(nextStepDefine, isStartCountersign, handlers,
+        return await GetNextStepFlowAssignInfoByDefineAsync(nextStepDefine, dto.HandlerType, isStartCountersign, handlers,
             cancellationToken);
     }
 
@@ -1204,10 +1204,9 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     /// 按流程模板配置创建下一步办理对象
     /// </summary>
     private async Task<FlowAssignInfo> GetNextStepFlowAssignInfoByDefineAsync(StepDefine nextStepDefine,
-        bool isStartCountersign,
-        List<Kv> handlers, CancellationToken cancellationToken)
+        EHandlerType handlerType, bool isStartCountersign, List<Kv> handlers, CancellationToken cancellationToken)
     {
-        switch (nextStepDefine.HandlerType)
+        switch (handlerType)
         {
             case EHandlerType.Role:
                 if (!handlers.Any())

+ 4 - 1
src/Hotline.Application/Mappers/WorkflowMapperConfigs.cs

@@ -3,6 +3,7 @@ using Hotline.FlowEngine.Workflows;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.FlowEngine.Definition;
 using Hotline.Share.Dtos.FlowEngine.Workflow;
+using Hotline.Share.Enums.FlowEngine;
 using Mapster;
 
 namespace Hotline.Application.Mappers;
@@ -86,6 +87,8 @@ public class WorkflowMapperConfigs : IRegister
             .Map(d => d.ModuleName, s => s.ModuleName)
             .IgnoreNonMapped(true)
             ;
-        
+
+        config.ForType<WorkflowTrace, WorkflowTraceDto>()
+            .IgnoreIf((s, d) => s.Status != EWorkflowStepStatus.Handled, d => d.HandlerName);
     }
 }

+ 27 - 0
src/Hotline.Application/Orders/IOrderApplication.cs

@@ -75,5 +75,32 @@ namespace Hotline.Application.Orders
         /// <returns></returns>
         ISugarQueryable<Order, WorkflowStep> QueryUnsignedOrders(QueryUnsignedOrdersRequest dto);
 
+        /// <summary>
+        /// 信件来源统计
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<Order> QueryOrderSource(QueryOrderSourceRequest dto);
+
+		/// <summary>
+		/// 信件来源统计列表
+        /// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		Task<List<OrderSourceTimeVo>> QueryOrderSourceList(QueryOrderSourceRequest dto);
+
+        /// <summary>
+        /// 信件来源统计列表表头
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task<List<OrderSourceHeaderVo>> QueryOrderSourceHeaderList(QueryOrderSourceRequest dto);
+
+        /// <summary>
+        /// 信件来源统计
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<Order> QueryOrderSourceDetail(QueryOrderSourceDetailRequest dto);
 	}
 }

+ 94 - 10
src/Hotline.Application/Orders/OrderApplication.cs

@@ -20,6 +20,7 @@ using Hotline.Share.Enums.Settings;
 using Hotline.Share.Requests;
 using Hotline.Tools;
 using MapsterMapper;
+using Novacode;
 using SqlSugar;
 using XF.Domain.Authentications;
 using XF.Domain.Constants;
@@ -173,11 +174,11 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         stTime = _timeLimitDomainService.WorkDay(DateTime.Now);
         DateTime stTime2 = _timeLimitDomainService.WorkDay(DateTime.Now);
         var (total, items) = await _orderRepository.Queryable(canView: true)
-            .WhereIF(dto.IsProvince.HasValue, x => x.IsProvince == dto.IsProvince)
-            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), x => x.Title.Contains(dto.Keyword!) || x.No.Contains(dto.Keyword!))
-            .Where(x => x.ExpiredTime != null &&
-            x.Status != EOrderStatus.Filed && x.Status != EOrderStatus.Published && x.Status != EOrderStatus.Visited && stTime >= x.ExpiredTime.Value && stTime2 <= x.ExpiredTime.Value)
-            .OrderByDescending(x => x.CreationTime)
+            .WhereIF(dto.IsProvince.HasValue, d => d.IsProvince == dto.IsProvince)
+            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!) || d.No.Contains(dto.Keyword!))
+            .Where(d => d.ExpiredTime != null &&
+            d.Status != EOrderStatus.Filed && d.Status != EOrderStatus.Published && d.Status != EOrderStatus.Visited && stTime >= d.ExpiredTime.Value && stTime2 <= d.ExpiredTime.Value)
+            .OrderByDescending(d => d.CreationTime)
             .ToPagedListAsync(dto.PageIndex, dto.PageSize, cancellationToken);
         return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
     }
@@ -219,13 +220,13 @@ public class OrderApplication : IOrderApplication, IScopeDependency
     {
         DateTime stTime = _timeLimitDomainService.WorkDay(DateTime.Now);
         var (total, items) = await _orderRepository.Queryable(canView: true)
-            .WhereIF(dto.IsProvince.HasValue, x => x.IsProvince == dto.IsProvince)
-            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), x => x.Title.Contains(dto.Keyword!) || x.No.Contains(dto.Keyword!))
+            .WhereIF(dto.IsProvince.HasValue, d => d.IsProvince == dto.IsProvince)
+            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!) || d.No.Contains(dto.Keyword!))
             //.WhereIF(!string.IsNullOrEmpty(dto.No), x => x.No == dto.No)
             //.WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.Title.Contains(dto.Title!))
-            .Where(x => x.ExpiredTime != null &&
-            (((x.Status == EOrderStatus.Filed || x.Status == EOrderStatus.Published || x.Status == EOrderStatus.Visited) && x.FiledTime >= x.ExpiredTime) ||
-            ((x.Status != EOrderStatus.Filed && x.Status != EOrderStatus.Published && x.Status != EOrderStatus.Visited) && stTime >= x.ExpiredTime.Value)))
+            .Where(d => d.ExpiredTime != null &&
+            (((d.Status == EOrderStatus.Filed || d.Status == EOrderStatus.Published || d.Status == EOrderStatus.Visited) && d.FiledTime >= d.ExpiredTime) ||
+            ((d.Status != EOrderStatus.Filed && d.Status != EOrderStatus.Published && d.Status != EOrderStatus.Visited) && stTime >= d.ExpiredTime.Value)))
             .OrderByDescending(x => x.CreationTime)
             .ToPagedListAsync(dto.PageIndex, dto.PageSize, cancellationToken);
         return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
@@ -466,9 +467,92 @@ public class OrderApplication : IOrderApplication, IScopeDependency
             .WhereIF(dto.Level == 2, (x, ws) => ws.AcceptorOrgId.StartsWith(_sessionContext.OrgId))
             .WhereIF(dto.Signed == 0 ,(x,ws)=>ws.Status == Share.Enums.FlowEngine.EWorkflowStepStatus.WaitForAccept)
             .WhereIF(dto.Signed == 1, (x, ws) => ws.Status == Share.Enums.FlowEngine.EWorkflowStepStatus.WaitForHandle)
+            .Where((x,ws)=>ws.CountersignPosition ==  Share.Enums.FlowEngine.ECountersignPosition.None && x.Status > EOrderStatus.WaitForAccept)
 			.OrderByDescending((x,ws) => ws.CreationTime);
 	}
 
+    /// <summary>
+    /// 信件来源统计
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    public ISugarQueryable<Order> QueryOrderSource(QueryOrderSourceRequest dto)
+    {
+	    if (dto.EndTime.HasValue)
+		    dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+	    return _orderRepository.Queryable()
+		    .WhereIF(dto.StartTime.HasValue, d => d.CreationTime >= dto.StartTime)
+		    .WhereIF(dto.EndTime.HasValue, d => d.CreationTime <= dto.EndTime)
+            .WhereIF(dto.IdentityType.HasValue , d=>d.IdentityType == dto.IdentityType)
+            .Where(d=> d.SourceChannel != null &&  d.SourceChannel !="");
+	}
+
+	/// <summary>
+	/// 信件来源统计列表
+	/// </summary>
+	/// <param name="dto"></param>
+	/// <returns></returns>
+	public async Task<List<OrderSourceTimeVo>> QueryOrderSourceList(QueryOrderSourceRequest dto)
+    {
+	    if (dto.EndTime.HasValue)
+		    dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+        var Time = await _orderRepository.Queryable()
+            .WhereIF(dto.StartTime.HasValue, d => d.CreationTime >= dto.StartTime)
+            .WhereIF(dto.EndTime.HasValue, d => d.CreationTime <= dto.EndTime)
+            .WhereIF(dto.IdentityType.HasValue, d => d.IdentityType == dto.IdentityType)
+            .Where(d => d.SourceChannel != null && d.SourceChannel != "")
+            .GroupBy(d=>  d.CreationTime.ToString("yyyy-MM-dd"))
+            .Select(d => new OrderSourceTimeVo { Time = d.CreationTime.ToString("yyyy-MM-dd") }).ToListAsync();
+        var data = await _orderRepository.Queryable()
+            .WhereIF(dto.StartTime.HasValue, d => d.CreationTime >= dto.StartTime)
+            .WhereIF(dto.EndTime.HasValue, d => d.CreationTime <= dto.EndTime)
+            .WhereIF(dto.IdentityType.HasValue, d => d.IdentityType == dto.IdentityType)
+            .Where(d => d.SourceChannel != null && d.SourceChannel != "")
+            .GroupBy(d => new { Time = d.CreationTime.ToString("yyyy-MM-dd"), d.SourceChannel })
+            .Select(d => new OrderSourceTimeListVo
+            {
+                Time = d.CreationTime.ToString("yyyy-MM-dd"),
+                Source = d.SourceChannel,
+                Num = SqlFunc.AggregateCount(d.Id)
+            }).ToListAsync() ;
+        foreach (var item in Time)
+        {
+			List<OrderSourceTimeListVo> list = data.Where(x=>x.Time == item.Time).ToList();
+			item.Lists = new List<OrderSourceTimeListVo>();
+			item.Lists.AddRange(list);
+		}
+        return Time;
+	}
+
+	/// <summary>
+	/// 信件来源统计列表表头
+	/// </summary>
+	/// <param name="dto"></param>
+	/// <returns></returns>
+	public async Task<List<OrderSourceHeaderVo>> QueryOrderSourceHeaderList(QueryOrderSourceRequest dto) 
+    {
+		if (dto.EndTime.HasValue)
+			dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+		var list =  await _orderRepository.Queryable()
+			.WhereIF(dto.StartTime.HasValue, d => d.CreationTime >= dto.StartTime)
+			.WhereIF(dto.EndTime.HasValue, d => d.CreationTime <= dto.EndTime)
+			.WhereIF(dto.IdentityType.HasValue, d => d.IdentityType == dto.IdentityType)
+			.Where(d => d.SourceChannel != null && d.SourceChannel != "")
+			.GroupBy(d => d.SourceChannel)
+			.Select(d => new OrderSourceHeaderVo { Header = d.SourceChannel,Code = d.SourceChannel }).ToListAsync();
+		list.Insert(0, new OrderSourceHeaderVo { Header = "小计", Code = "Subtotal" });
+		list.Insert(0, new OrderSourceHeaderVo { Header = "日期", Code = "Time" });
+		return list;
+	}
+
+    public ISugarQueryable<Order> QueryOrderSourceDetail(QueryOrderSourceDetailRequest dto) {
+		
+		return _orderRepository.Queryable()
+            .WhereIF(string.IsNullOrEmpty(dto.SourceChannel),d=>d.SourceChannel == dto.SourceChannel)
+            .WhereIF(dto.Time.HasValue,d=>d.CreationTime.ToString("yyyy-MM-dd") == dto.Time.Value.ToString("yyyy-MM-dd"))
+			.WhereIF(dto.IdentityType.HasValue, d => d.IdentityType == dto.IdentityType)
+			.Where(d => d.SourceChannel != null && d.SourceChannel != "");
+	}
 
 	#region private
 

+ 28 - 3
src/Hotline.Application/Quality/QualityApplication.cs

@@ -1,4 +1,5 @@
 using Hotline.Ai.Quality;
+using Hotline.Caching.Interfaces;
 using Hotline.CallCenter.Calls;
 using Hotline.Orders;
 using Hotline.Quality;
@@ -6,6 +7,7 @@ using Hotline.Share.Dtos.Quality;
 using Hotline.Share.Enums.Quality;
 using MapsterMapper;
 using XF.Domain.Authentications;
+using XF.Domain.Constants;
 using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
 using XF.Domain.Repository;
@@ -22,6 +24,7 @@ namespace Hotline.Application.Quality
         private readonly IOrderRepository _orderRepository;
         private readonly IRepository<TrCallRecord> _trCallRecordRepository;
         private readonly IRepository<QualityTemplate> _qualityTemplate;
+        private readonly ISystemSettingCacheManager _systemSettingCacheManager;
 
 		public QualityApplication(
             ISessionContext sessionContext,
@@ -31,7 +34,8 @@ namespace Hotline.Application.Quality
             IAiQualityService aiQualityService,
             IRepository<TrCallRecord> trCallRecordRepository,
             IRepository<QualityTemplate> qualityTemplate,
-			IOrderRepository orderRepository
+			IOrderRepository orderRepository,
+            ISystemSettingCacheManager systemSettingCacheManager
 		)
         {
             _sessionContext = sessionContext;
@@ -42,7 +46,7 @@ namespace Hotline.Application.Quality
             _trCallRecordRepository = trCallRecordRepository;
             _qualityTemplate = qualityTemplate;
 			_orderRepository = orderRepository;
-
+            _systemSettingCacheManager = systemSettingCacheManager;
 		}
 
         public async Task AddQualityAsync(EQualitySource Source, string OrderId, string VisitId, CancellationToken cancellationToken)
@@ -88,7 +92,28 @@ namespace Hotline.Application.Quality
                 List<QualityDetail> details = _mapper.Map<List<QualityDetail>>(model.QualityDetails);
                 await _qualiteyDetail.AddRangeAsync(details, cancellationToken);
             }
-            await _qualityRepository.AddAsync(quality, cancellationToken);
+            //受理智能质检
+            if (model.Source == EQualitySource.Accepted)
+            {
+	            var teAny = await _qualityTemplate.Queryable()
+		            .LeftJoin<QualityTemplateDetail>((x, d) => x.Id == d.TemplateId)
+		            .LeftJoin<QualityItem>((x, d, i) => d.ItemId == i.Id)
+		            .Where((x, d, i) => i.IsIntelligent == 1).AnyAsync();
+	            if (teAny)
+	            {
+		            var order = await _orderRepository.GetAsync(model.OrderId);
+		            if (order != null && !string.IsNullOrEmpty(order.CallId))
+		            {
+			            quality.AiQuality = true;
+			            quality.Mode = "智能质检";
+			            //var call = await _trCallRecordRepository.GetAsync(x => x.CallAccept == order.CallId); //由CallAccept改成OtherAccept
+			            var call = await _trCallRecordRepository.GetAsync(x => x.OtherAccept == order.CallId);
+			            var setting = _systemSettingCacheManager.GetSetting(SettingConstants.ViteRecordPrefix);
+						await _aiQualityService.CreateAiOrderQualityTask(quality, call, order, setting?.SettingValue[0], cancellationToken);
+		            }
+	            }
+            }
+			await _qualityRepository.AddAsync(quality, cancellationToken);
 		}
 
         public async Task UpdateQualityAsync(UpdateQualityDto model, CancellationToken cancellationToken)

+ 2 - 2
src/Hotline.Application/Subscribers/DatasharingSubscriber.cs

@@ -170,7 +170,7 @@ namespace Hotline.Application.Subscribers
                         //await _workflowApplication.HandleToEndAsync(current, order.WorkflowId, "省工单同意退回", null,
                         //    EReviewResult.Approval, cancellationToken);
                         await _workflowApplication.JumpToEndAsync(current, order.WorkflowId, dto.Reason ?? "省工单同意退回",
-                            null, cancellationToken: cancellationToken);
+                            null, order.ExpiredTime, cancellationToken: cancellationToken);
                     }
                 }
                 else
@@ -207,7 +207,7 @@ namespace Hotline.Application.Subscribers
             {
                 //await _workflowApplication.HandleToEndAsync(current, order.WorkflowId, dto.Opinion, null,
                 //    cancellationToken: cancellationToken);
-                await _workflowApplication.JumpToEndAsync(current, order.WorkflowId, dto.Opinion, null,
+                await _workflowApplication.JumpToEndAsync(current, order.WorkflowId, dto.Opinion, null, order.ExpiredTime,
                     cancellationToken: cancellationToken);
             }
         }

+ 14 - 0
src/Hotline.Application/Systems/ISystemLogApplication.cs

@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Application.Systems
+{
+	public interface ISystemLogApplication
+	{
+		Task AddLog<T>(string name, string res, T entity, HttpContext context,string crName ="");
+	}
+}

+ 47 - 0
src/Hotline.Application/Systems/SystemLogApplication.cs

@@ -0,0 +1,47 @@
+using Hotline.Settings;
+using Hotline.Tools;
+using MapsterMapper;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+using Org.BouncyCastle.Asn1.Ocsp;
+using System.Text;
+using XF.Domain.Dependency;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Systems
+{
+	public class SystemLogApplication: ISystemLogApplication, IScopeDependency
+	{
+		private readonly IMapper _mapper;
+		private readonly IRepository<SystemLog> _systemLogRepository;
+		public SystemLogApplication(IMapper mapper, IRepository<SystemLog> systemLogRepository) { _mapper = mapper; _systemLogRepository = systemLogRepository; }
+
+		public async Task AddLog<T>(string name, string res, T entity, HttpContext context, string crName ="") {
+			SystemLog log = new SystemLog();
+			log.Name = name;
+			var Method = "";
+			switch (context.Request.Method.ToUpper())
+			{
+				case "GET":
+					Method = "GET";
+					log.ExecuteParam = context.Request.QueryString.Value;
+					break;
+
+				case "POST":
+					Method = "POST";
+					log.ExecuteUrl += context.Request.QueryString.Value;
+					log.ExecuteParam = JsonConvert.SerializeObject(entity);
+					break;
+			}
+			log.ExecuteUrl = Method + "|" + context.Request.Path;
+			log.Status = 1;
+			log.ExecuteResult = JsonConvert.SerializeObject(res);
+			log.IpUrl = context.Connection.RemoteIpAddress.ToString();
+			log.InitId();
+			await  _systemLogRepository.AddAsync(log);
+			if (!string.IsNullOrEmpty(crName))
+				await _systemLogRepository.Updateable().SetColumns(l => new SystemLog() { CreatorName = crName }).Where(l => l.Id == log.Id).ExecuteCommandAsync();
+		}
+	}
+}

+ 14 - 6
src/Hotline.Repository.SqlSugar/Extensions/DataPermissionExtensions.cs

@@ -20,12 +20,20 @@ namespace Hotline.Repository.SqlSugar.Extensions
             where TEntity : class, IEntity<string>, IDataPermission, IWorkflow, new()
         {
             var session = dataPermissionFilterBuilder.SessionContext;
-            return queryable.LeftJoin<WorkflowTrace>((d, step) => d.Id == step.ExternalId)
-                .Where((d, step) => (step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == session.RequiredUserId) ||
-                                       (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == session.RequiredOrgId) ||
-                                       (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && session.Roles.Contains(step.RoleId)))
-                .Select((d, step) => d)
-                ;
+
+            return queryable.Where(d => SqlFunc.Subqueryable<WorkflowTrace>()
+                    .Where(step => step.ExternalId == d.Id &&
+                            ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == session.RequiredUserId) ||
+                               (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == session.RequiredOrgId) ||
+                               (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && session.Roles.Contains(step.RoleId))))
+                    .Any());
+
+            //return queryable.LeftJoin<WorkflowTrace>((d, step) => d.Id == step.ExternalId)
+            //    .Where((d, step) => (step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == session.RequiredUserId) ||
+            //                           (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == session.RequiredOrgId) ||
+            //                           (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && session.Roles.Contains(step.RoleId)))
+            //    .Select((d, step) => d)
+            //    ;
 
             //return queryable.Where(dataPermissionFilterBuilder.BuildWithFlowViewFilter<TEntity>());
         }

+ 31 - 2
src/Hotline.Share/Dtos/Ai/AiDto.cs

@@ -13,6 +13,12 @@ namespace Hotline.Share.Dtos.Ai
     public record AiCallOutDetailListRequest:PagedRequest
     {
         public string Id { get; set; }
+
+        public string? OuterNo { get; set; }
+
+        public string? Name { get; set; }
+
+        public EAiCallOutState? AiCallOutState { get; set; }
     }
 
 
@@ -30,6 +36,8 @@ namespace Hotline.Share.Dtos.Ai
         public bool? IsSuccess { get; set; }
 
         public EAiCallOutState AiCallOutState { get; set; }
+
+        public string AiCallOutStateText => AiCallOutState.GetDescription();
     }
 
 
@@ -109,6 +117,13 @@ namespace Hotline.Share.Dtos.Ai
         public string TemplateContent { get; set; }
     }
 
+    public class CanUseCallOutTemplateListRep
+    {
+        public string Id { get; set; }
+
+        public string TemplateName { get; set; }
+    }
+
     public class UpdateCallOutTemplateDto:CallOutTemplateDto
     {
         public string Id { get; set; }
@@ -366,14 +381,28 @@ namespace Hotline.Share.Dtos.Ai
         public List<AddAiOrderVisitDetailDto> AiOrderVisitDetails { get; set; }
     }
 
+    public class CloseCalloutTaskReq
+    {
+        public string Id { get; set; }
+
+        /// <summary>
+        /// 1 :智能回访  2:批量外呼
+        /// </summary>
+        public int TypeId { get; set; }
+    }
+
+
     public class AddAiCallOutRequest
     {
-        public string Name { get; set; }
+        public string TaskName { get; set; }
 
         public DateTime BeginTime { get; set; }
 
         public DateTime EndTime { get; set; }
-
+        /// <summary>
+        /// 模板ID
+        /// </summary>
+        public string CallOutTemplateId { get; set; }
         /// <summary>
         /// 节日禁呼 0:否 1:是
         /// </summary>

+ 7 - 0
src/Hotline.Share/Dtos/FlowEngine/BasicWorkflowDto.cs

@@ -26,6 +26,11 @@ public class BasicWorkflowDto : EndWorkflowDto
     /// </summary>
     public EHandlerType HandlerType { get; set; }
 
+    /// <summary>
+    /// 节点类型
+    /// </summary>
+    public EStepType StepType { get; set; }
+
     /// <summary>
     /// 根据办理者类型不同,此字段为不同内容
     /// <example>
@@ -68,6 +73,8 @@ public class BasicWorkflowDto : EndWorkflowDto
     /// 审核结果(非审批流程无意义)
     /// </summary>
     public EReviewResult ReviewResult { get; set; }
+
+    
 }
 
 public class External

+ 85 - 0
src/Hotline.Share/Dtos/Order/OrderBiDto.cs

@@ -720,4 +720,89 @@ namespace Hotline.Share.Dtos.Order
 		public string NoPutThroughRateText => NoPutThroughRate + "%";
 
 	}
+
+	public class OrderSourceVo
+	{
+
+		/// <summary>
+		/// 信件来源
+		/// </summary>
+		public string Source { get; set; }
+
+		/// <summary>
+		/// 数量
+		/// </summary>
+		public int Num { get; set; }
+
+		/// <summary>
+		/// 总数
+		/// </summary>
+		public int TotalSumCount { get; set; }
+
+		/// <summary>
+		/// 占比
+		/// </summary>
+		public double Rate => GetRate();
+
+		public double GetRate()
+		{
+			if (Num == 0 || TotalSumCount == 0)
+			{
+				return 0.000;
+			}
+			return Math.Round((Num / (double)TotalSumCount) * 100, 2) ;
+		}
+
+		public string RateText => GetRate() + "%";
+	}
+
+	public class OrderSourceHeaderVo {
+		/// <summary>
+		/// 来源渠道(电话、网站、APP等)
+		/// </summary>
+		public string? Header { get; set; }
+
+		public string? Code { get; set; }
+	}
+
+	public class OrderSourceTimeVo
+	{
+
+		/// <summary>
+		/// 日期
+		/// </summary>
+		public string Time { get; set; }
+
+		/// <summary>
+		/// 信件来源
+		/// </summary>
+		public string Source { get; set; }
+
+		/// <summary>
+		/// 小计
+		/// </summary>
+		public int Subtotal { get; set; }
+
+		public List<OrderSourceTimeListVo> Lists { get; set; }
+
+	}
+
+	public class OrderSourceTimeListVo
+	{
+		/// <summary>
+		/// 日期
+		/// </summary>
+		public string Time { get; set; }
+
+		/// <summary>
+		/// 信件来源
+		/// </summary>
+		public string Source { get; set; }
+
+		/// <summary>
+		/// 小计
+		/// </summary>
+		public int Num { get; set; }
+
+	}
 }

+ 7 - 1
src/Hotline.Share/Dtos/Order/OrderWaitedDto.cs

@@ -24,7 +24,13 @@ namespace Hotline.Share.Dtos.Order
         public DateTime? StepExpiredTime { get; set; }
     }
 
-    public record QueryOrderWaitedDto(bool? IsProvince, bool? IsHandled,bool? IsCounterSign,bool? ExpiredOrAlmostOverdue) : PagedKeywordRequest;
+    public record QueryOrderWaitedDto : PagedKeywordRequest 
+    {
+	    public bool? IsProvince { get; set; }
+	    public bool? IsHandled { get; set; }
+	    public bool? IsCounterSign { get; set; }
+	    public bool? ExpiredOrAlmostOverdue { get; set; }
+	}
 
     /// <summary>
     /// 中心待办

+ 4 - 0
src/Hotline.Share/Dtos/Settings/SystemLogDto.cs

@@ -60,6 +60,10 @@ namespace Hotline.Share.Dtos.Settings
 		///</summary>
 		public string? ExecuteUrl { get; set; }
 		/// <summary>
+		/// ip地址 
+		///</summary>
+		public string? IpUrl { get; set; }
+		/// <summary>
 		/// 请求参数 
 		///</summary>
 		public string? ExecuteParam { get; set; }

+ 23 - 27
src/Hotline.Share/Dtos/TrCallCenter/TrTelDao.cs

@@ -423,92 +423,92 @@ namespace Hotline.Share.Dtos.TrCallCenter
         public EEndBy? EndBy { get; set; }
 
         /// <summary>
-        /// IVR开始开始时间(修改)
+        /// IVR开始开始时间
         /// </summary>
         public DateTime? BeginIvrTimeStart { get; set; }
 
         /// <summary>
-        /// IVR开始结束时间(新增)
+        /// IVR开始结束时间
         /// </summary>
         public DateTime? BeginIvrTimeEnd { get; set; }
 
         /// <summary>
-        /// IVR结束开始时间(修改)
+        /// IVR结束开始时间
         /// </summary>
         public DateTime? EndIvrTimeStart { get; set; }
 
         /// <summary>
-        /// IVR结束结束时间(新增)
+        /// IVR结束结束时间
         /// </summary>
         public DateTime? EndIvrTimeEnd { get; set; }
 
         /// <summary>
-        /// 开始等待开始时间(队列开始时间)(修改)
+        /// 开始等待开始时间(队列开始时间)
         /// </summary>
         public DateTime? BeginQueueTimeStart { get; set; }
 
         /// <summary>
-        /// 开始等待结束时间(队列开始时间)(新增)
+        /// 开始等待结束时间(队列开始时间)
         /// </summary>
         public DateTime? BeginQueueTimeEnd { get; set; }
 
         /// <summary>
-        /// 结束等待开始时间(队列结束时间)(修改)
+        /// 结束等待开始时间(队列结束时间)
         /// </summary>
         public DateTime? EndQueueTimeStart { get; set; }
 
         /// <summary>
-        /// 结束等待结束时间(队列结束时间)(新增)
+        /// 结束等待结束时间(队列结束时间)
         /// </summary>
         public DateTime? EndQueueTimeEnd { get; set; }
 
         /// <summary>
-        /// 应答开始时间(修改)
+        /// 应答开始时间
         /// </summary>
         public DateTime? AnsweredTimeStart { get; set; }
 
         /// <summary>
-        /// 应答结束时间(新增)
+        /// 应答结束时间
         /// </summary>
         public DateTime? AnsweredTimeEnd { get; set; }
 
         /// <summary>
-        /// 通话结束开始时间(修改)
+        /// 通话结束开始时间
         /// </summary>
         public DateTime? OverTimeStart { get; set; }
 
         /// <summary>
-        /// 通话结束结束时间(新增)
+        /// 通话结束结束时间
         /// </summary>
         public DateTime? OverTimeEnd { get; set; }
 
         /// <summary>
-        /// 振铃开始开始时间(新增)
+        /// 振铃开始开始时间
         /// </summary>
         public DateTime? BeginRingTimeStart { get; set; }
 
         /// <summary>
-        /// 振铃开始结束时间(新增)
+        /// 振铃开始结束时间
         /// </summary>
         public DateTime? BeginRingTimeEnd { get; set; }
 
         /// <summary>
-        /// 振铃结束开始时间(新增)
+        /// 振铃结束开始时间
         /// </summary>
         public DateTime? EndRingTimeStart { get; set; }
 
         /// <summary>
-        /// 振铃结束结束时间(新增)
+        /// 振铃结束结束时间
         /// </summary>
         public DateTime? EndRingTimeEnd { get; set; }
 
         /// <summary>
-        /// 通话开始时间开始(新增)
+        /// 通话开始时间开始
         /// </summary>
         public DateTime? CallTimeStart { get; set; }
 
         /// <summary>
-        /// 通话开始时间结束(新增)
+        /// 通话开始时间结束
         /// </summary>
         public DateTime? CallTimeEnd { get; set; }
 
@@ -526,15 +526,11 @@ namespace Hotline.Share.Dtos.TrCallCenter
         /// 中继号
         /// </summary>
         public string? Gateway { get; set; }
-        ///// <summary>
-        ///// 开始时间
-        ///// </summary>
-        //public DateTime? StartTime { get; set; }
-
-        ///// <summary>
-        ///// 结束时间
-        ///// </summary>
-        //public DateTime? EndTime { get; set; }
+
+        /// <summary>
+        /// 是否智能应答(需要OnState条件传 接通)
+        /// </summary>
+        public bool IsAiAnswered { get; set; }
 
     }
 

+ 3 - 0
src/Hotline.Share/Enums/Ai/EAiCallOutTaskState.cs

@@ -17,5 +17,8 @@ namespace Hotline.Share.Enums.Ai
 
         [Description("已结束")]
         Ended = 3,
+
+        [Description("已终止")]
+        Close = 4,
     }
 }

+ 2 - 0
src/Hotline.Share/Enums/Ai/EAiOrderVisitTaskState.cs

@@ -13,6 +13,8 @@ namespace Hotline.Share.Enums.Ai
 
         [Description("已结束")]
         Ended = 3,
+        [Description("已终止")]
+        Close = 4,
     }
 
 }

+ 30 - 2
src/Hotline.Share/Requests/PagedKeywordRequest.cs

@@ -1,4 +1,6 @@
-namespace Hotline.Share.Requests;
+using Hotline.Share.Enums.Order;
+
+namespace Hotline.Share.Requests;
 
 public record PagedKeywordRequest : PagedRequest
 {
@@ -215,4 +217,30 @@ public record QueryUnsignedOrdersRequest : ReportPagedRequest
 	///  0 未签收  1 签收
 	/// </summary>
 	public int Signed { get; set; }
-}
+}
+
+public record QueryOrderSourceRequest : ReportPagedRequest 
+{
+	/// <summary>
+	/// 来电/信人身份
+	/// </summary>
+	public EIdentityType? IdentityType { get; set; }
+}
+
+public record QueryOrderSourceDetailRequest : QueryOrderSourceRequest
+{
+	/// <summary>
+	/// 来电/信人身份
+	/// </summary>
+	public EIdentityType? IdentityType { get; set; }
+
+	/// <summary>
+	/// 点击时间
+	/// </summary>
+	public DateTime? Time { get; set; }
+
+	/// <summary>
+	/// 来源渠道(电话、网站、APP等)
+	/// </summary>
+	public string? SourceChannel { get; set; }
+}

+ 2 - 0
src/Hotline/Ai/CallOut/CallOutTask.cs

@@ -1,4 +1,5 @@
 
+using Hotline.Orders;
 using Hotline.Share.Dtos.Ai;
 using Hotline.Share.Enums.Ai;
 using SqlSugar;
@@ -52,6 +53,7 @@ namespace Hotline.Ai.CallOut
 
         public DateTime EndTime { get; set; }
 
+        [Navigate(NavigateType.OneToMany, nameof(CallOutTaskDetail.CallOutTaskId))]
         public List<CallOutTaskDetail> CallOutTaskDetails{ get; set; }
     }
 }

+ 1 - 0
src/Hotline/Ai/CallOut/CallOutTemplate.cs

@@ -10,6 +10,7 @@ namespace Hotline.Ai.CallOut
 
         public bool IsEnable { get; set; }
 
+        [SugarColumn(ColumnDataType = "varchar(2000)")]
         public string TemplateContent { get; set; }
 
         [Navigate(NavigateType.OneToMany, nameof(CallOutTask.CallOutTemplateId))]

+ 1 - 1
src/Hotline/Ai/Quality/IAiQualityService.cs

@@ -13,6 +13,6 @@ namespace Hotline.Ai.Quality
 {
     public interface IAiQualityService
 	{
-        Task CreateAiOrderQualityTask(Hotline.Quality.Quality model, TrCallRecord? call,Order order, CancellationToken cancellationToken);
+        Task CreateAiOrderQualityTask(Hotline.Quality.Quality model, TrCallRecord? call,Order order, string viteRecordPrefix, CancellationToken cancellationToken);
 	}
 }

+ 3 - 0
src/Hotline/Ai/Visit/IAiVisitService.cs

@@ -2,6 +2,7 @@
 using Hotline.Ai.CallOut;
 using Hotline.Orders;
 using Hotline.Share.Dtos.Order;
+using Microsoft.AspNetCore.Http;
 
 namespace Hotline.Ai.Visit
 {
@@ -13,5 +14,7 @@ namespace Hotline.Ai.Visit
 
 
         Task<AiVisitQueryData> QueryAiVisitTask(string batchId,string taskId, CancellationToken cancellationToken);
+
+        Task<bool> ChangeStatusAsync(string batchUid, string status, CancellationToken cancellationToken);
     }
 }

+ 2 - 2
src/Hotline/FlowEngine/FlowAssignInfo.cs

@@ -65,10 +65,10 @@ public class FlowAssignInfo
             .Distinct()
             .ToList();
 
-    public static EFlowAssignType GetAssignType(EHandlerType handlerType) =>
+    public static EFlowAssignType GetAssignType(EHandlerType handlerType, bool hasSelectedHandlers) =>
         handlerType switch
         {
-            EHandlerType.Role => EFlowAssignType.User,
+            EHandlerType.Role => hasSelectedHandlers ? EFlowAssignType.User : EFlowAssignType.Role,
             EHandlerType.OrgLevel => EFlowAssignType.Org,
             EHandlerType.OrgType => EFlowAssignType.Org,
             EHandlerType.AssignedUser => EFlowAssignType.User,

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

@@ -104,7 +104,7 @@ namespace Hotline.FlowEngine.Workflows
         /// 结束流程(流程直接流转至结束节点)
         /// </summary>
         Task<WorkflowTrace> EndAsync(Workflow workflow, BasicWorkflowDto dto, StepDefine endStepDefine,
-            WorkflowStep currentStep, ISessionContext current, CancellationToken cancellationToken);
+            WorkflowStep currentStep, ISessionContext current, DateTime? expiredTime, CancellationToken cancellationToken);
 
         StepDefine GetStepDefine(WorkflowDefinition workflowDefinition, string stepCode);
 
@@ -131,7 +131,7 @@ namespace Hotline.FlowEngine.Workflows
         /// <summary>
         /// 撤销流程
         /// </summary>
-        Task CancelAsync(CancelDto dto, ISessionContext current, CancellationToken cancellationToken);
+        Task CancelAsync(CancelDto dto, DateTime? expiredTime, ISessionContext current, CancellationToken cancellationToken);
 
         ///// <summary>
         ///// 更新期满时间

+ 11 - 14
src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs

@@ -109,7 +109,7 @@ namespace Hotline.FlowEngine.Workflows
                     current.OrgAreaCode, current.OrgAreaName,
                     current.OrgLevel);
 
-                var endTrace = await EndAsync(workflow, dto, firstStepDefine, startStep, current, cancellationToken);
+                var endTrace = await EndAsync(workflow, dto, firstStepDefine, startStep, current, expiredTime, cancellationToken);
                 return;
             }
 
@@ -454,7 +454,7 @@ namespace Hotline.FlowEngine.Workflows
             //检查是否流转到流程终点
             if (nextStepDefine.StepType is EStepType.End)
             {
-                var endTrace = await EndAsync(workflow, dto, nextStepDefine, currentStep, current, cancellationToken);
+                var endTrace = await EndAsync(workflow, dto, nextStepDefine, currentStep, current, expiredTime, cancellationToken);
                 return;
             }
 
@@ -776,7 +776,7 @@ namespace Hotline.FlowEngine.Workflows
         public async Task<WorkflowStep> FindLastHandleStepAsync(string workflowId, string orgId, CancellationToken cancellation)
         {
             return await _workflowStepRepository.Queryable()
-                .Where(d => d.WorkflowId == workflowId && d.HandlerOrgId == orgId)
+                .Where(d => d.WorkflowId == workflowId && d.HandlerOrgId == orgId && d.StepType != EStepType.End)
                 //.Where(d => d.StepHandlers.Any(sh => sh.OrgId == orgId) && d.WorkflowId == workflowId)
                 .OrderByDescending(d => d.HandleTime)
                 .FirstAsync(cancellation);
@@ -844,7 +844,7 @@ namespace Hotline.FlowEngine.Workflows
             startStep.AcceptorOrgName = orgName;
 
             startStep.FlowAssignType = EFlowAssignType.User;
-            startStep.Assign(userId,username,orgId,orgName);
+            startStep.Assign(userId, username, orgId, orgName);
 
             //var stepHandler = startStep.StepHandlers.First();
             //startStep.StepHandlers.RemoveAll(d => d.Id != stepHandler.Id);
@@ -1196,7 +1196,7 @@ namespace Hotline.FlowEngine.Workflows
         /// <summary>
         /// 撤销流程
         /// </summary>
-        public async Task CancelAsync(CancelDto dto, ISessionContext current, CancellationToken cancellationToken)
+        public async Task CancelAsync(CancelDto dto, DateTime? expiredTime, ISessionContext current, CancellationToken cancellationToken)
         {
             var workflow = await GetWorkflowAsync(dto.WorkflowId, withDefine: true, withSteps: true,
                 cancellationToken: cancellationToken);
@@ -1209,7 +1209,7 @@ namespace Hotline.FlowEngine.Workflows
 
             var basicDto = _mapper.Map<BasicWorkflowDto>(dto);
             var endTrace = await EndAsync(workflow, basicDto, endStepDefine, currentStep, current,
-                cancellationToken: cancellationToken);
+               expiredTime, cancellationToken: cancellationToken);
 
             await _mediator.Publish(new CancelWorkflowNotify(workflow), cancellationToken);
         }
@@ -1250,7 +1250,7 @@ namespace Hotline.FlowEngine.Workflows
             startStep.NextSteps = nextSteps;
             startStep.IsMain = true;
             startStep.IsOrigin = true;
-            startStep.Status = EWorkflowStepStatus.WaitForHandle;
+            startStep.Status = EWorkflowStepStatus.WaitForAccept;
             startStep.PrevChosenStepCode = null;
             startStep.StepExpiredTime = expiredTime;
 
@@ -1273,7 +1273,7 @@ namespace Hotline.FlowEngine.Workflows
         /// </summary>
         public async Task<WorkflowTrace> EndAsync(Workflow workflow, BasicWorkflowDto dto,
             StepDefine endStepDefine, WorkflowStep currentStep, ISessionContext current,
-            CancellationToken cancellationToken)
+            DateTime? expiredTime, CancellationToken cancellationToken)
         {
             //var endStepHandles = new List<WorkflowStepHandler>
             //{
@@ -1282,7 +1282,7 @@ namespace Hotline.FlowEngine.Workflows
             //};
 
             //create endStep
-            var endStep = await CreateEndStepAsync(current, workflow, endStepDefine, currentStep, cancellationToken);
+            var endStep = await CreateEndStepAsync(current, workflow, endStepDefine, currentStep, expiredTime, cancellationToken);
             workflow.Steps.Add(endStep);
 
             //update endTrace
@@ -2014,6 +2014,7 @@ namespace Hotline.FlowEngine.Workflows
             Workflow workflow,
             StepDefine endStepDefine,
             WorkflowStep prevStep,
+            DateTime? expiredTime,
             CancellationToken cancellationToken)
         {
             if (workflow.Steps.Any(d => d.StepType == EStepType.End))
@@ -2031,7 +2032,7 @@ namespace Hotline.FlowEngine.Workflows
 
             var step = CreateStep(workflow, endStepDefine, prevStep, null, handler,
                  null, null, EWorkflowStepStatus.WaitForAccept,
-                ECountersignPosition.None, DateTime.Now, endStepDefine.Name, true);
+                ECountersignPosition.None, expiredTime, endStepDefine.Name, true);
 
             //step.Accept(_sessionContext.RequiredUserId, _sessionContext.UserName,
             //    _sessionContext.RequiredOrgId, _sessionContext.OrgName,
@@ -2404,10 +2405,6 @@ namespace Hotline.FlowEngine.Workflows
             //    throw new UserFriendlyException($"非法参数, handlers为空, method: {nameof(CreateStep)}");
             var step = _mapper.Map<WorkflowStep>(stepDefine);
             _mapper.Map(workflow, step);
-            //todo mainhandler 未赋值
-            //var handlerIds = handlers.Select(d => d.Key).ToList();
-            //var isMain = handlers.Count == 1 || (handlers.Count > 1 || handlerIds.First() == nextMainHandler);
-
             step.FlowAssignType = flowAssignType;
             step.Handlers = new List<Kv> { new(handler.Key, handler.Value) };
             //step.StepHandlers = stepHandlers;

+ 0 - 6
src/Hotline/FlowEngine/Workflows/WorkflowStep.cs

@@ -117,12 +117,6 @@ public class WorkflowStep : StepBasicEntity
     /// </summary>
     public void Reset()
     {
-        HandlerId = null;
-        HandlerName = null;
-        HandlerOrgId = null;
-        HandlerOrgName = null;
-        HandlerOrgAreaCode = null;
-        HandlerOrgAreaName = null;
         HandleTime = null;
 
         AcceptorId = null;

+ 12 - 5
src/Hotline/Quality/QualityDomainService.cs

@@ -1,10 +1,12 @@
 using Hotline.Ai.Quality;
+using Hotline.Caching.Interfaces;
 using Hotline.CallCenter.Calls;
 using Hotline.Orders;
 using Hotline.Share.Dtos.Quality;
 using Hotline.Share.Enums.Quality;
 using MapsterMapper;
 using XF.Domain.Authentications;
+using XF.Domain.Constants;
 using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
 using XF.Domain.Repository;
@@ -21,8 +23,9 @@ namespace Hotline.Quality
         private readonly IOrderRepository _orderRepository;
         private readonly IRepository<TrCallRecord> _trCallRecordRepository;
         private readonly IRepository<QualityTemplate> _qualityTemplate;
+        private readonly ISystemSettingCacheManager _systemSettingCacheManager;
 
-        public QualityDomainService(
+		public QualityDomainService(
             ISessionContext sessionContext,
             IMapper mapper,
             IRepository<QualityDetail> qualiteyDetail,
@@ -30,8 +33,9 @@ namespace Hotline.Quality
             IAiQualityService aiQualityService,
             IRepository<TrCallRecord> trCallRecordRepository,
             IRepository<QualityTemplate> qualityTemplate,
-            IOrderRepository orderRepository
-            )
+            IOrderRepository orderRepository,
+            ISystemSettingCacheManager systemSettingCacheManager
+			)
         {
             _sessionContext = sessionContext;
             _mapper = mapper;
@@ -41,7 +45,9 @@ namespace Hotline.Quality
             _trCallRecordRepository = trCallRecordRepository;
             _qualityTemplate = qualityTemplate;
             _orderRepository = orderRepository;
-        }
+            _systemSettingCacheManager = systemSettingCacheManager;
+
+		}
 
         public async Task AddQualityAsync(EQualitySource Source, string OrderId, string VisitId, CancellationToken cancellationToken)
         {
@@ -98,7 +104,8 @@ namespace Hotline.Quality
                         quality.Mode = "智能质检";
                         //var call = await _trCallRecordRepository.GetAsync(x => x.CallAccept == order.CallId); //由CallAccept改成OtherAccept
                         var call = await _trCallRecordRepository.GetAsync(x => x.OtherAccept == order.CallId);
-                        await _aiQualityService.CreateAiOrderQualityTask(quality, call, order, cancellationToken);
+                        var setting = _systemSettingCacheManager.GetSetting(SettingConstants.ViteRecordPrefix);
+						await _aiQualityService.CreateAiOrderQualityTask(quality, call, order, setting?.SettingValue[0],cancellationToken);
                     }
                 }
             }

+ 5 - 0
src/Hotline/Settings/SystemLog.cs

@@ -29,6 +29,11 @@ namespace Hotline.Settings
 		/// 接口地址 
 		///</summary>
 		public string? ExecuteUrl { get; set; }
+
+		/// <summary>
+		/// ip地址 
+		///</summary>
+		public string? IpUrl { get; set; }
 		/// <summary>
 		/// 请求参数 
 		///</summary>

+ 8 - 3
src/XF.Domain/Constants/SettingConstants.cs

@@ -322,7 +322,12 @@ namespace XF.Domain.Constants
         /// </summary>
         public const string CallOutContentKey = "CallOutContentKey";
 
-        #endregion
-        #endregion
-    }
+		#endregion
+		#endregion
+
+        /// <summary>
+        /// 录音文件前缀地址
+        /// </summary>
+		public const string ViteRecordPrefix = "ViteRecordPrefix";
+	}
 }