Эх сурвалжийг харах

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

tangjiang 7 сар өмнө
parent
commit
b876eb03a5
48 өөрчлөгдсөн 1377 нэмэгдсэн , 421 устгасан
  1. 13 0
      src/Hotline.Api/Controllers/CommonPController.cs
  2. 189 111
      src/Hotline.Api/Controllers/OrderController.cs
  3. 6 0
      src/Hotline.Api/Program.cs
  4. 1 1
      src/Hotline.Api/StartupExtensions.cs
  5. 10 8
      src/Hotline.Api/StartupHelper.cs
  6. 1 2
      src/Hotline.Api/config/appsettings.Development.json
  7. 43 0
      src/Hotline.Application.Tests/Application/OrderApplicationTest.cs
  8. 0 159
      src/Hotline.Application.Tests/Application/SnapshotApplicationTest.cs
  9. 44 0
      src/Hotline.Application.Tests/Mock/MediatorMock.cs
  10. 59 0
      src/Hotline.Application.Tests/Repository/OrderVisitRepositoryTest.cs
  11. 16 6
      src/Hotline.Application.Tests/Startup.cs
  12. 1 10
      src/Hotline.Application.Tests/appsettings.Development.json
  13. 4 2
      src/Hotline.Application/Handlers/FlowEngine/WorkflowNextHandler.cs
  14. 27 11
      src/Hotline.Application/Orders/IOrderApplication.cs
  15. 175 4
      src/Hotline.Application/Orders/OrderApplication.cs
  16. 5 0
      src/Hotline.Application/Subscribers/DatasharingSubscriber.cs
  17. 2 4
      src/Hotline.Application/Tels/TelApplication.cs
  18. 103 0
      src/Hotline.Repository.SqlSugar/Orders/OrderVisitRepository.cs
  19. 8 0
      src/Hotline.Share/Dtos/FlowEngine/PreviousWorkflowDto.cs
  20. 6 1
      src/Hotline.Share/Dtos/Order/OrderDto.cs
  21. 1 0
      src/Hotline.Share/Dtos/Order/OrderPreviousDto.cs
  22. 2 0
      src/Hotline.Share/Dtos/Order/OrderSpecialDto.cs
  23. 165 4
      src/Hotline.Share/Dtos/Order/OrderVisitDto.cs
  24. 30 0
      src/Hotline.Share/Dtos/Order/SendBackDto.cs
  25. 15 0
      src/Hotline.Share/Dtos/Push/FWMessage/MessageDataDto.cs
  26. 10 10
      src/Hotline.Share/Enums/Order/ESeatEvaluate.cs
  27. 7 0
      src/Hotline.Share/Enums/Order/ESpecialType.cs
  28. 14 0
      src/Hotline.Share/Enums/Order/EVisitState.cs
  29. 18 0
      src/Hotline.Share/Enums/Order/EVoiceEvaluate.cs
  30. 6 0
      src/Hotline.Share/Enums/Push/EPushBusiness.cs
  31. 2 2
      src/Hotline.Share/Tools/ObjectExtensions.cs
  32. 17 0
      src/Hotline.Share/Tools/StringExtensions.cs
  33. 21 0
      src/Hotline.Share/Tools/TaskExtensions.cs
  34. 30 1
      src/Hotline/CallCenter/Calls/CallNative.cs
  35. 9 7
      src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs
  36. 2 0
      src/Hotline/Orders/AiVisitDomainService.cs
  37. 13 0
      src/Hotline/Orders/IOrderVisitRepository.cs
  38. 189 64
      src/Hotline/Orders/Order.cs
  39. 26 0
      src/Hotline/Orders/OrderSendBackAudit.cs
  40. 1 1
      src/Hotline/Orders/OrderSpecial.cs
  41. 21 1
      src/Hotline/Permissions/EPermission.cs
  42. 1 1
      src/Hotline/Push/FWMessage/Message.cs
  43. 8 2
      src/Hotline/Push/FWMessage/PushDomainService.cs
  44. 8 2
      src/Hotline/Settings/SysDicTypeConsts.cs
  45. 16 4
      src/Hotline/Settings/SystemOrganize.cs
  46. 29 1
      src/Hotline/Settings/TimeLimitDomain/ExpireTimeLimitBase.cs
  47. 2 1
      src/Hotline/Settings/TimeLimitDomain/ICalcExpireTime.cs
  48. 1 1
      src/Hotline/dataview.md

+ 13 - 0
src/Hotline.Api/Controllers/CommonPController.cs

@@ -25,6 +25,7 @@ using Hotline.Share.Dtos.Order;
 using System.Runtime.InteropServices;
 using Lucene.Net.Util;
 using NPOI.Util;
+using XF.Domain.Entities;
 
 namespace Hotline.Api.Controllers
 {
@@ -228,6 +229,12 @@ namespace Hotline.Api.Controllers
 				//待办
 				var waitedDataList = await _orderRepository
 					.Queryable(hasHandled: false, isAdmin: isAdmin)
+					.Where(d => SqlFunc.Subqueryable<WorkflowStep>()
+						.Where(step => step.ExternalId == d.Id &&
+						               ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == _sessionContext.RequiredUserId) ||
+						                (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == _sessionContext.RequiredOrgId) ||
+						                (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && _sessionContext.Roles.Contains(step.RoleId))))
+						.Any())
 					.Includes(d => d.OrderSpecials)
 					.Where(d => d.Status != EOrderStatus.WaitForAccept && d.Status != EOrderStatus.BackToUnAccept && d.Status != EOrderStatus.SpecialToUnAccept && d.Status != EOrderStatus.HandOverToUnAccept)
 					.Where(d => d.Source < ESource.MLSQ || d.Source > ESource.WZSC)
@@ -395,6 +402,12 @@ namespace Hotline.Api.Controllers
 				//待办
 				var waitedDataList = await _orderRepository
 					.Queryable(hasHandled: false, isAdmin: isAdmin)
+					.Where(d => SqlFunc.Subqueryable<WorkflowStep>()
+						.Where(step => step.ExternalId == d.Id &&
+						               ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == _sessionContext.RequiredUserId) ||
+						                (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == _sessionContext.RequiredOrgId) ||
+						                (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && _sessionContext.Roles.Contains(step.RoleId))))
+						.Any())
 					.Includes(d => d.OrderSpecials)
 					.Where(d => d.Status != EOrderStatus.WaitForAccept && d.Status != EOrderStatus.BackToUnAccept && d.Status != EOrderStatus.SpecialToUnAccept && d.Status != EOrderStatus.HandOverToUnAccept)
 					.Where(d => d.Source < ESource.MLSQ || d.Source > ESource.WZSC)

+ 189 - 111
src/Hotline.Api/Controllers/OrderController.cs

@@ -57,6 +57,7 @@ using MiniExcelLibs;
 using MongoDB.Driver;
 using SqlSugar;
 using StackExchange.Redis;
+using System.Text;
 using XF.Domain.Authentications;
 using XF.Domain.Cache;
 using XF.Domain.Entities;
@@ -304,6 +305,14 @@ public class OrderController : BaseController
     [HttpPost("batch-publish")]
     public async Task BatchPublishOrder([FromBody] BatchPublishOrderDto dto)
     {
+        var hasHuiQian = await _orderRepository.Queryable().AnyAsync(x => dto.Ids.Contains(x.Id) && x.CounterSignType != null);
+        if (hasHuiQian)
+            throw UserFriendlyException.SameMessage("选择的工单中含有会签工单, 不能批量发布. 请排除会签工单.");
+
+        var hasProvince = await _orderRepository.Queryable().AnyAsync(x => dto.Ids.Contains(x.Id) && x.IsProvince == true);
+        if (hasProvince)
+            throw UserFriendlyException.SameMessage("选择的工单中含有省工单, 不能批量发布. 请排除省工单.");
+
         foreach (var item in dto.Ids)
         {
             var order = await _orderRepository.GetAsync(item, HttpContext.RequestAborted);
@@ -355,6 +364,11 @@ public class OrderController : BaseController
                         orderVisit.IsCanAiVisit = true;
                     }
 
+                    if (_appOptions.Value.IsZiGong)
+                    {
+                        orderVisit.EmployeeId = string.Empty;
+                    }
+
                     string visitId = await _orderVisitRepository.AddAsync(orderVisit);
 
                     //新增回访信息
@@ -488,7 +502,7 @@ public class OrderController : BaseController
             orderVisit.EmployeeId = _sessionContext.RequiredUserId;
         }
 
-        if (order is { ProcessType: EProcessType.Zhiban, CounterSignType: null })
+        if (order is { ProcessType: EProcessType.Zhiban, CounterSignType: null } && !order.IsProvince)
         {
             orderVisit.VisitState = EVisitState.Visited;
             orderVisit.VisitTime = DateTime.Now;
@@ -496,6 +510,12 @@ public class OrderController : BaseController
             orderVisit.NowEvaluate = new Kv() { Key = "4", Value = "满意" };
         }
 
+        if (_appOptions.Value.IsZiGong)
+        {
+            orderVisit.EmployeeId = string.Empty;
+        }
+
+
         if (order.CounterSignType != ECounterSignType.Center)
         {
             orderVisit.IsCanAiVisit = true;
@@ -539,7 +559,7 @@ public class OrderController : BaseController
         await _orderVisitedDetailRepository.AddRangeAsync(visitedDetail, HttpContext.RequestAborted);
 
 
-        if (orderVisit.VisitState == EVisitState.Visited)
+        if (orderVisit.VisitState == EVisitState.Visited && !order.IsProvince)
         {
             //推省上
             await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderVisited,
@@ -776,6 +796,8 @@ public class OrderController : BaseController
                 d => d.VisitState == EVisitState.WaitForVisit ||
                       d.VisitState == EVisitState.NoSatisfiedWaitForVisit)
             .WhereIF(dto.VisitState == EVisitStateQuery.Visited, d => d.VisitState == EVisitState.Visited)
+            .WhereIF(dto.VisitState == EVisitStateQuery.SMSUnsatisfied, m => m.VisitState == EVisitState.SMSUnsatisfied)
+            .WhereIF(dto.VisitState == EVisitStateQuery.SMSVisiting, m => m.VisitState == EVisitState.SMSVisiting)
             .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Order.Title.StartsWith(dto.Keyword!))
             .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.No == dto.No)
             .WhereIF(dto.VisitType != null, x => x.VisitType == dto.VisitType)
@@ -788,11 +810,27 @@ public class OrderController : BaseController
             .WhereIF(dto.IsProvince != null && dto.IsProvince == true, d => d.Order.IsProvince == true)
             .WhereIF(dto.IsProvince != null && dto.IsProvince == false, d => d.Order.IsProvince == false)
             .WhereIF(dto.IsEffectiveAiVisit != null, d => d.IsEffectiveAiVisit == dto.IsEffectiveAiVisit)
+            .WhereIF(dto.FromPhone.NotNullOrEmpty(), m => m.Order.FromPhone == dto.FromPhone)
+            .WhereIF(dto.Contact.NotNullOrEmpty(), m => m.Order.Contact == dto.Contact)
+            .WhereIF(dto.VoiceEvaluate.Any(), d => d.OrderVisitDetails.Any(m => dto.VoiceEvaluate.Contains(m.VoiceEvaluate.Value)))
+            .WhereIF(dto.SeatEvaluate.Any(), d => d.OrderVisitDetails.Any(m => dto.SeatEvaluate.Contains(m.SeatEvaluate.Value)))
+            .WhereIF(dto.OrgProcessingResults.Any(),
+                d => d.OrderVisitDetails.Any(m => dto.OrgProcessingResults.Contains(SqlFunc.JsonField(m.OrgProcessingResults, "Key"))))
+            .WhereIF(dto.OrgHandledAttitude.Any(),
+                d => d.OrderVisitDetails.Any(q => dto.OrgHandledAttitude.Contains(SqlFunc.JsonField(q.OrgHandledAttitude, "Key"))))
             .OrderByDescending(x => x.PublishTime)
             .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
         return new PagedDto<OrderVisitDto>(total, _mapper.Map<IReadOnlyList<OrderVisitDto>>(items));
     }
 
+    /// <summary>
+    /// 发送 回访短信
+    /// </summary>
+    /// <returns></returns>
+    [HttpPost("visit/sms")]
+    public async Task VisitPushSMSAsync([FromBody] VisitSmsInDto dto)
+        => await _orderApplication.VisitPushSMSAsync(dto, HttpContext.RequestAborted);
+
     /// <summary>
     /// 回访查询基础数据
     /// </summary>
@@ -803,6 +841,13 @@ public class OrderController : BaseController
         var rsp = new
         {
             VisitType = EnumExts.GetDescriptions<EVisitType>(),
+            VoiceEvaluate = EnumExts.GetDescriptions<EVoiceEvaluate>(),
+            SeatEvaluate = EnumExts.GetDescriptions<ESeatEvaluate>(),
+            // 部门办件结果
+            VisitSatisfaction = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitSatisfaction).Where(x => x.DicDataValue != "-1").Select(m => new { m.Id, m.DicDataName, m.DicDataValue }),
+            // 部门办件态度
+            VisitManner = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitManner).Where(x => x.DicDataValue != "-1").Select(m => new { m.Id, m.DicDataName, m.DicDataValue }),
+            VisitStateQuery = EnumExts.GetDescriptions<EVisitStateQuery>()
         };
         return rsp;
     }
@@ -832,6 +877,10 @@ public class OrderController : BaseController
         //    x => x.OrderId == orderVisit.OrderId && x.AgainState == EAgainState.DoAgain, HttpContext.RequestAborted);
         var voiceEvaluate = EnumExts.GetDescriptions<EVoiceEvaluate>();
         var seatEvaluate = EnumExts.GetDescriptions<ESeatEvaluate>();
+        if (_appOptions.Value.IsZiGong == false)
+        {
+            seatEvaluate = seatEvaluate.Where(m => new int[] { 1, 3 }.Contains(m.Key) == false).ToList();
+        }
         var visitSatisfaction = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitSatisfaction).Where(x => x.DicDataValue != "-1");
         var dissatisfiedReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.DissatisfiedReason);
         var visitManner = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitManner).Where(x => x.DicDataValue != "-1");
@@ -864,6 +913,20 @@ public class OrderController : BaseController
         //获取系统配置智能回访语音URL头
         aiVisitVoiceBaseUrl = _systemSettingCacheManager.GetSetting(SettingConstants.AiVisitVoiceBaseUrl)?.SettingValue[0];
 
+        var histories = await _orderVisitRepository.Queryable()
+            .Includes(m => m.OrderVisitDetails)
+            .Where(m => m.OrderId == orderVisit.OrderId)
+            .Select(m => new OrderVisitDetailHistiryDto
+            {
+                VoiceEvaluate = m.OrderVisitDetails.Where(n => n.VisitTarget == EVisitTarget.Seat).Select(s => s.VoiceEvaluate).First(),
+                SeatEvaluate = m.OrderVisitDetails.Where(n => n.VisitTarget == EVisitTarget.Seat).Select(s => s.SeatEvaluate).First(),
+                VisitOrgName = m.OrderVisitDetails.Where(n => n.VisitTarget == EVisitTarget.Org).Select(s => s.VisitOrgName).First(),
+                OrgProcessingResults = m.OrderVisitDetails.Where(n => n.VisitTarget == EVisitTarget.Org).Select(s => s.OrgProcessingResults).First(),
+                OrgHandledAttitude = m.OrderVisitDetails.Where(n => n.VisitTarget == EVisitTarget.Org).Select(s => s.OrgHandledAttitude).First(),
+                VisitContent = m.OrderVisitDetails.Where(n => n.VisitTarget == EVisitTarget.Org).Select(s => s.VisitContent).First(),
+                VisitTime = m.VisitTime
+            }).ToListAsync();
+
         return new
         {
             OrderVisitModel = _mapper.Map<OrderVisitDto>(orderVisit),
@@ -878,6 +941,7 @@ public class OrderController : BaseController
             RecordingBaseAddress = recordingBaseAddress,
             RecordingAbsolutePath = recordingAbsolutePath,
             AiVisitVoiceBaseUrl = aiVisitVoiceBaseUrl,
+            Histories = histories
         };
     }
 
@@ -911,120 +975,111 @@ public class OrderController : BaseController
     [HttpPost("visit")]
     [LogFilter("工单回访")]
     public async Task Visit([FromBody] VisitDto dto)
-    {
-        //var visit = await _orderVisitRepository.GetAsync(dto.Id, HttpContext.RequestAborted);
-        var visit = await _orderVisitRepository.Queryable()
-            .Includes(d => d.Order)
-            .Includes(d => d.OrderVisitDetails)
-            .FirstAsync(d => d.Id == dto.Id, HttpContext.RequestAborted);
-        if (visit is null)
-            throw UserFriendlyException.SameMessage("未知回访信息");
+        => await _orderApplication.SaveOrderVisit(dto, HttpContext.RequestAborted);
 
-        if (visit.VisitState == EVisitState.Visited)
-            throw UserFriendlyException.SameMessage("已回访,不能重复回访");
-
-        var first = dto.VisitDetails.FirstOrDefault(x => x.VisitTarget == EVisitTarget.Org);
-
-        visit.IsPutThrough = dto.IsPutThrough;
-        visit.AgainState = dto.IsAgain ? EAgainState.NeedAgain : EAgainState.NoAgain;
-        visit.EmployeeId = _sessionContext.UserId;
-        visit.CallId = dto.CallId;
-        if (first != null)
-        {
-            visit.NowEvaluate = first.OrgProcessingResults;
-        }
-
-        //update order
-        //if (dto.IsPutThrough)
-        //{
-        visit.VisitState = Share.Enums.Order.EVisitState.Visited;
-        visit.VisitTime = DateTime.Now;
-        if (!string.IsNullOrEmpty(visit.CallId))
-        {
-            visit.VisitType = EVisitType.CallVisit;
-        }
-
-        if (visit.VisitType is null)
-        {
-            visit.VisitType = EVisitType.ArtificialVisit;
-        }
+    /// <summary>
+    /// 批量保存回访
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [Permission(EPermission.Visit)]
+    [HttpPost("visit/batch")]
+    [LogFilter("批量工单回访")]
+    public async Task<VisitBatchOutDto> VisitBatch([FromBody] VisitBatchInDto dto)
+    {
+        var errorMessage = new StringBuilder();
+        var outDto = new VisitBatchOutDto();
+        var orderIds = dto.Visit.Select(m => m.OrderId).ToList();
+        var hasHuiQian = await _orderRepository.Queryable().AnyAsync(x => orderIds.Contains(x.Id) && x.CounterSignType != null);
+        if (hasHuiQian)
+            throw UserFriendlyException.SameMessage("选择的回访单中含有会签工单, 不能批量回访. 请排除会签工单.");
 
-        if (first != null)
-        {
-            visit.Order.Visited(first.OrgProcessingResults.Key, first.OrgProcessingResults.Value);
-        }
+        var hasProvince = await _orderRepository.Queryable().AnyAsync(x => orderIds.Contains(x.Id) && x.IsProvince == true);
+        if (hasProvince)
+            throw UserFriendlyException.SameMessage("选择的回访单中含有省工单, 不能批量回访. 请排除省工单.");
 
-        visit.OrgJudge = dto.OrgJudge;
-        visit.SeatJudge = dto.SeatJudge;
 
-        if (visit.OrgJudge == true || visit.SeatJudge == true)
+        foreach (var visit in dto.Visit)
         {
-            visit.JudgeState = EJudgeState.Judging;
-        }
 
-        //_mapper.Map(dto.VisitDetails,visit.OrderVisitDetails);
-        for (int i = 0;i < visit.OrderVisitDetails.Count;i++)
-        {
-            var detail = visit.OrderVisitDetails[i];
-            var detaildto = dto.VisitDetails.FirstOrDefault(x => x.Id == detail.Id);
-            if (detaildto != null)
+            try
             {
-                if (visit.Order.SourceChannelCode != "RGDH" && detaildto.VisitTarget == EVisitTarget.Seat)
+                var details = await _orderVisitedDetailRepository
+		    .Queryable()
+		    .Where(m => m.VisitId == visit.VisitId)
+		    .ToListAsync();
+                var seatDetail = details.First(m => m.VisitTarget == EVisitTarget.Seat);
+                var visitDto = new VisitDto
                 {
-                    detaildto.SeatEvaluate = ESeatEvaluate.DefaultSatisfied;
-                }
-
-                _mapper.Map(detaildto, visit.OrderVisitDetails[i]);
-            }
-        }
+                    Id = visit.VisitId,
+                    IsPutThrough = true,
+                    IsAgain = false,
+                    VisitDetails = new List<VisitDetailDto>
+                    {
+			    new()
+			    {
+				Id = seatDetail.Id,
+				VisitId = visit.VisitId,
+				VisitContent = dto.SeatVisitContent,
+				SeatEvaluate = dto.SeatEvaluate,
+                                VisitTarget = EVisitTarget.Seat
+			    }
+                    }
+                };
 
-        await _orderVisitRepository.UpdateAsync(visit, HttpContext.RequestAborted);
-        await _orderVisitedDetailRepository.UpdateRangeAsync(visit.OrderVisitDetails, HttpContext.RequestAborted);
-        await _orderRepository.UpdateAsync(visit.Order, HttpContext.RequestAborted);
-        var orderDto = _mapper.Map<OrderDto>(visit.Order);
-        if (first != null)
-        {
-            //推省上
-            await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderVisited,
-                new PublishVisitDto()
+                var orgDetails = details.Where(m => m.VisitTarget == EVisitTarget.Org).ToList();
+                foreach (var orgDetail in orgDetails)
                 {
-                    Order = orderDto,
-                    No = visit.No,
-                    VisitType = visit.VisitType,
-                    VisitName = visit.CreatorName,
-                    VisitTime = visit.VisitTime,
-                    VisitRemark = string.IsNullOrEmpty(first.VisitContent) ? first.OrgProcessingResults?.Value : first.VisitContent,
-                    AreaCode = visit.Order.AreaCode!,
-                    SubjectResultSatifyCode = first.OrgProcessingResults.Key,
-                    FirstSatisfactionCode = visit.Order.FirstVisitResultCode!,
-                    ClientGuid = ""
-                }, cancellationToken: HttpContext.RequestAborted);
-
-            //推门户
-            await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderVisitedWeb, new PublishVisitAllDto()
+                    visitDto.VisitDetails.Add(
+			new()
+			{
+			    Id = orgDetail.Id,
+			    VisitId = visit.VisitId,
+			    VisitContent = dto.OrgVisitContent,
+			    VisitTarget = EVisitTarget.Org,
+			    OrgNoSatisfiedReason = dto.OrgNoSatisfiedReason,
+			    OrgProcessingResults = dto.OrgProcessingResults,
+			    OrgHandledAttitude = dto.OrgHandledAttitude
+			});
+                }
+                await _orderApplication.SaveOrderVisit(visitDto, HttpContext.RequestAborted);
+                outDto.CompleteCount += 1;
+            }
+            catch (Exception e)
             {
-                Id = visit.Id,
-                Order = orderDto,
-                OrderVisitDetails = _mapper.Map<List<VisitDetailDto>>(visit.OrderVisitDetails),
-                VisitName = _sessionContext.UserName,
-                VisitTime = visit.VisitTime,
-                VisitType = visit.VisitType,
-                VisitState = visit.VisitState,
-                PublishTime = visit.PublishTime,
-            }, cancellationToken: HttpContext.RequestAborted);
+                outDto.ErrorCount += 1;
+                errorMessage.Append($"【{visit.No}】 保存失败.");
+                _logger.LogError($"orderVisitId:{visit.VisitId} 保存回访失败, err: {e.Message}");
+            }
+            finally
+            {
+                outDto.TotalCount += 1;
+            }
         }
+        outDto.ErrorMessage = errorMessage.ToString();
+        return outDto;
+    }
 
-        if (first != null)
+    /// <summary>
+    /// 批量保存回访基础数据
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("visit/batch-basedata")]
+    [AllowAnonymous]
+    public async Task<dynamic> VisitBatchBaseDat()
+    {
+        return new
         {
-            //写入质检
-            await _qualityApplication.AddQualityAsync(EQualitySource.Visit, visit.Order.Id, visit.Id,
-                HttpContext.RequestAborted);
-        }
-        //}
-        //else
-        //{
-        //    await _orderVisitRepository.UpdateAsync(visit, HttpContext.RequestAborted);
-        //}
+            // 话务员评价
+            SeatEvaluate = EnumExts.GetDescriptions<ESeatEvaluate>(),
+            // 不满意原因
+            DissatisfiedReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.DissatisfiedReason).Select(m => new { m.Id, m.DicDataName, m.DicDataValue}),
+            // 回访满意度
+            VisitSatisfaction = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitSatisfaction).Where(x => x.DicDataValue != "-1").Select(m => new { m.Id, m.DicDataName, m.DicDataValue }),
+            // 回访态度
+            VisitManner = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitManner).Where(x => x.DicDataValue != "-1").Select(m => new { m.Id, m.DicDataName, m.DicDataValue })
+        };
     }
 
     /// <summary>
@@ -3979,13 +4034,26 @@ public class OrderController : BaseController
             ApplyOrgName = currentStep!.AcceptorOrgName,
             SendBackOrgId = prevStep.HandlerOrgId, //prevStep.AcceptorOrgId,
             SendBackOrgName = prevStep.HandlerOrgName, //prevStep!.AcceptorOrgName,
-            WorkflowOrgId = _sessionContext.RequiredOrgId,
+            SendBackStepName = prevStep.Name,
+            WorkflowStepSendBackCrTime = currentStep.CreationTime,
+			WorkflowOrgId = _sessionContext.RequiredOrgId,
             WorkflowUserId = _sessionContext.RequiredUserId,
             WorkflowRoleIds = _sessionContext.Roles.ToList(),
             Status = order.Status,
             TraceId = currentStep.Id
         };
-        if (oneSendBack || twoSendBack)
+        if (_appOptions.Value.IsZiGong && prevStep.BusinessType == EBusinessType.Send)
+        {
+	        // 平均派单
+	        var averageSendOrder = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.AverageSendOrder).SettingValue[0]);
+	        if (averageSendOrder)
+	        {
+		        var handler = await _orderDomainService.AverageOrder(HttpContext.RequestAborted);
+                dto.NextHandlers = new List<FlowStepHandler> { handler };
+			}
+        }
+
+		if (oneSendBack || twoSendBack)
         {
             var sendBack = await _orderSendBackAuditRepository.Queryable()
                 .Where(x => x.OrderId == workflow.ExternalId && x.State == ESendBackAuditState.Apply).AnyAsync();
@@ -3996,7 +4064,15 @@ public class OrderController : BaseController
                 .AnyAsync();
             if (specialAny) throw UserFriendlyException.SameMessage("工单已存在待审批特提信息!");
             if (order.Workflow.IsInCountersign) throw UserFriendlyException.SameMessage("工单会签中,无法进行退回!");
-            if ((oneSendBack && isOrgToCenter) || (twoSendBack && isSecondToFirstOrgLevel))
+            if (oneSendBack && isOrgToCenter &&  _appOptions.Value.IsZiGong)
+            {
+	            if (order.SendBackAuditEndTime.HasValue && order.SendBackAuditEndTime.Value < DateTime.Now )
+		            throw UserFriendlyException.SameMessage("工单截至退回时间【" + order.SendBackAuditEndTime.Value.ToString("yyyy-MM-dd HH:mm:ss") + "】,无法进行退回!");
+				var sendBackAgain = await _orderSendBackAuditRepository.Queryable().Where(x => x.OrderId == workflow.ExternalId && x.IsReturnAgain == true).AnyAsync();
+	            if (sendBackAgain)
+		            throw UserFriendlyException.SameMessage("工单已不允许退回!");
+			}                
+			if ((oneSendBack && isOrgToCenter) || (twoSendBack && isSecondToFirstOrgLevel))
             {
 
                 await _orderRepository.Updateable().SetColumns(o => new Orders.Order() { Status = EOrderStatus.SendBackAudit })
@@ -4008,7 +4084,7 @@ public class OrderController : BaseController
                 audit.AuditUser = "默认通过";
                 audit.AuditTime = DateTime.Now;
                 dto.ExpiredTime = order.ExpiredTime;
-                var flowDirection = await _workflowApplication.PreviousAsync(dto, HttpContext.RequestAborted);
+				var flowDirection = await _workflowApplication.PreviousAsync(dto, HttpContext.RequestAborted);
                 var processType = flowDirection == EFlowDirection.OrgToCenter || flowDirection == EFlowDirection.CenterToCenter
                     ? EProcessType.Zhiban
                     : EProcessType.Jiaoban;
@@ -4618,7 +4694,7 @@ public class OrderController : BaseController
         model.StepName = step.Name;
         model.StepCode = step.Code;
         model.State = 1;
-        model.ESpecialType = ESpecialType.ReTransact;
+        model.SpecialType = dto.SpecialType;
         model.LastFileOpinion = order.FileOpinion;
         model.FirstFileOpinion = order.FileOpinion;
         var firstSpecial = await _orderSpecialRepository.Queryable().Where(x => x.OrderId == dto.OrderId).FirstAsync();
@@ -5115,7 +5191,8 @@ public class OrderController : BaseController
         {
             SpecialTimeType = EnumExts.GetDescriptions<ETimeType>(),
             SpecialReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.SpecialReason),
-            Step = step,
+            InstaShotSpecialReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.InstaShotSpecialReason),
+			Step = step,
             BaseTypeId = baseTypeId
         };
         return rsp;
@@ -5129,7 +5206,8 @@ public class OrderController : BaseController
     public async Task<object> ReTransactBaseData(string id)
     {
         var order = await _orderRepository.GetAsync(id, HttpContext.RequestAborted);
-        List<Kv> orgs = new();
+        var isInstaShot = order.SourceChannel.Contains("随手拍");
+		List<Kv> orgs = new();
         if (order == null) throw UserFriendlyException.SameMessage("无效工单信息!");
         //中心会签调取方法
         var org = await _workflowDomainService.GetLevelOneOrgsAsync(order.WorkflowId, HttpContext.RequestAborted);
@@ -5139,8 +5217,8 @@ public class OrderController : BaseController
         var rsp = new
         {
             SpecialTimeType = EnumExts.GetDescriptions<ETimeType>(),
-            SpecialReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.SpecialReason),
-            ReTransactErrorType = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.ReTransactErrorType),
+            SpecialReason = isInstaShot ? _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.InstaShotSpecialReason) : _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.SpecialReason),
+            ReTransactErrorType =  _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.ReTransactErrorType),
             Step = step,
             Orgs = orgs,
         };

+ 6 - 0
src/Hotline.Api/Program.cs

@@ -11,6 +11,12 @@ try
 
     var builder = WebApplication.CreateBuilder(args);
 
+    builder.WebHost.ConfigureKestrel(serverOptions =>
+    {
+        serverOptions.Limits.MaxConcurrentConnections = 5000;
+        serverOptions.Limits.MaxConcurrentUpgradedConnections = 5000;
+    });
+
     builder.Host
         .ConfigureAppConfiguration((hostBuilderContext, configBuilder) =>
         {

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

@@ -188,7 +188,7 @@ internal static class StartupExtensions
         services.AddScoped<IExpireTimeSupplier, HourSupplier>();
 
         //services.AddScoped<LogFilterAttribute>();
-        ServiceLocator.Instance = services.BuildServiceProvider();
+        // ServiceLocator.Instance = services.BuildServiceProvider();
         return builder.Build();
     }
 

+ 10 - 8
src/Hotline.Api/StartupHelper.cs

@@ -256,14 +256,7 @@ namespace Hotline.Api
                 d.MaxBatchSize = 3;
 
                 //load send order job
-                var autoSendOrderKey = new JobKey(nameof(SendOrderJob), "send order task");
-                d.AddJob<SendOrderJob>(autoSendOrderKey);
-                d.AddTrigger(t => t
-                    .WithIdentity("task-send-order-trigger")
-                    .ForJob(autoSendOrderKey)
-                    .StartNow()
-                    .WithCronSchedule("0 10 9 * * ?")
-                );
+                
 
                 //即将超期和超期短信
                 var autoSendOverTimeSmsKey = new JobKey(nameof(SendOverTimeSmsJob), "send overtime order task");
@@ -285,6 +278,15 @@ namespace Hotline.Api
                         .ForJob(aiVisitStatusKey)
                         .StartNow()
                         .WithCronSchedule("0 0/5 * * * ? *"));
+
+                        var autoSendOrderKey = new JobKey(nameof(SendOrderJob), "send order task");
+                        d.AddJob<SendOrderJob>(autoSendOrderKey);
+                        d.AddTrigger(t => t
+                            .WithIdentity("task-send-order-trigger")
+                            .ForJob(autoSendOrderKey)
+                            .StartNow()
+                            .WithCronSchedule("0 10 9 * * ?")
+                        );
                         break;
                     default:
                         break;

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

@@ -1,8 +1,7 @@
 {
   "AllowedHosts": "*",
   "AppConfiguration": {
-    "OldFilesUrls": "http://12345.zwfwhfgjjfzj.yibin.gov.cn:81",
-    "AppScope": "YiBin",
+    "AppScope": "ZiGong",
     "YiBin": {
       "CallCenterType": "TianRun", //XunShi、WeiErXin、TianRun、XingTang
       //智能回访

+ 43 - 0
src/Hotline.Application.Tests/Application/OrderApplicationTest.cs

@@ -0,0 +1,43 @@
+using Hotline.Application.Orders;
+using Hotline.Orders;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Order;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Tests.Application;
+public class OrderApplicationTest
+{
+    private readonly IOrderApplication _orderApplication;
+    private readonly IRepository<OrderVisit> _orderVisitRepository;
+
+    public OrderApplicationTest(IOrderApplication orderApplication, IRepository<OrderVisit> orderVisitRepository)
+    {
+        _orderApplication = orderApplication;
+        _orderVisitRepository = orderVisitRepository;
+    }
+
+    [Theory]
+    [InlineData(1)]
+    [InlineData(2)]
+    public async Task VisitPushSMS_Test(int count)
+    {
+        var orderVisit = await _orderVisitRepository.Queryable()
+            .Where(m => m.VisitState == EVisitState.WaitForVisit)
+            .OrderByDescending(m => m.CreationTime)
+            .FirstAsync();
+        var dto = new VisitSmsInDto
+        {
+            Ids = new List<string> { orderVisit.Id }
+        };
+        await _orderApplication.VisitPushSMSAsync(dto, new CancellationToken());
+        var visit = await _orderVisitRepository.Queryable().Where(m => m.Id == orderVisit.Id).FirstAsync();
+        visit.VisitState.ShouldBe(EVisitState.SMSVisiting);
+        visit.VisitType.ShouldBe(EVisitType.SmsVisit);
+    }
+}

+ 0 - 159
src/Hotline.Application.Tests/Application/SnapshotApplicationTest.cs

@@ -1,159 +0,0 @@
-//using Hotline.Application.Identity;
-//using Hotline.Application.Snapshot;
-//using Hotline.Share.Dtos.Article;
-//using Hotline.Share.Dtos.Snapshot;
-//using Hotline.Share.Enums;
-//using Hotline.Share.Enums.Snapshot;
-//using Hotline.Share.Tools;
-//using Hotline.Snapshot;
-//using Shouldly;
-//using XF.Domain.Repository;
-//using XF.Utility.EnumExtensions;
-
-//namespace Hotline.Application.Tests.Application;
-//public class SnapshotApplicationTest
-//{
-//    private readonly ISnapshotApplication _snapshotApplication;
-//    private readonly IIdentityAppService _identityAppService;
-//    private readonly IRepository<RedPackRecord> _redPackRecordRepository;
-
-//    public SnapshotApplicationTest(ISnapshotApplication snapshotApplication, IIdentityAppService identityAppService, IRepository<RedPackRecord> redPackRecordRepository)
-//    {
-//        _snapshotApplication = snapshotApplication;
-//        _identityAppService = identityAppService;
-//        _redPackRecordRepository = redPackRecordRepository;
-//    }
-
-//    [Fact]
-//    public async Task GetHomePage_Test()
-//    {
-//        var result = await _snapshotApplication.GetHomePageAsync();
-//        result.Any().ShouldBe(true, "首页数据为空");
-//        result.First().DisplayOrder.ShouldBe(1, "排序异常");
-//    }
-
-//    [Fact]
-//    public async Task GetBulletins_Test()
-//    {
-//        var homePage = await _snapshotApplication.GetHomePageAsync();
-//        var inDto = new BulletinInDto
-//        {
-//            IndustryId = homePage.First(m => m.Name == "文化旅游").Id,
-//        };
-//        var items = await _snapshotApplication.GetBulletinsAsync(inDto);
-//        items.ShouldNotBeNull();
-//        items.Any().ShouldBe(true, "公告数据为空");
-//        items.Any(m => m.Title.IsNullOrEmpty()).ShouldBe(false, "标题错误");
-//        items.Any(m => m.Content.IsNullOrEmpty()).ShouldBe(false, "内容错误");
-//        items.Any(m => m.Id.IsNullOrEmpty()).ShouldBe(false, "Id错误");
-//    }
-
-//    [Fact]
-//    public async Task GetSnapshotUserInfo_Test()
-//    {
-//        var result = await _snapshotApplication.GetSnapshotUserInfoAsync();
-//        result.ShouldNotBeNull();
-//        result.PhoneNumber.ShouldNotBeNullOrEmpty();
-//    }
-
-//    [Fact]
-//    public async Task GetThirdToken_Test()
-//    {
-//        var result = await _identityAppService.GetThredTokenAsync(new ThirdTokenInDto { Code = "0e3dql000zdwLS1ZRn000Z3SFR3dql00" });
-//        result.Code.ShouldBe(0);
-//        result.Result.Token.ShouldNotBeNull();
-//        result.Result.UserType.ShouldBe(EReadPackUserType.Citizen);
-//    }
-
-//    [Theory]
-//    [InlineData("")]
-//    [InlineData("测")]
-//    public async Task SnapshotOrder_Test(string key)
-//    {
-//        var dto = new OrderInDto();
-//        dto.KeyWords = key;
-//        var page = await _snapshotApplication.GetSnapshotOrdersAsync(dto);
-//        page.Total.ShouldNotBe(0);
-//        page.Items.FirstOrDefault()?.IndustryName.ShouldNotBeNullOrEmpty();
-//        page.Items.FirstOrDefault()?.OrderNo.ShouldNotBeNullOrEmpty();
-//        page.Items.FirstOrDefault()?.StatusText.ShouldNotBeNullOrEmpty();
-//        page.Items.FirstOrDefault()?.Area.ShouldNotBeNullOrEmpty();
-//    }
-
-//    [Theory]
-//    [InlineData(EOrderQueryStatus.All, 3)]
-//    [InlineData(EOrderQueryStatus.Reply, 2)]
-//    [InlineData(EOrderQueryStatus.NoReply, 1)]
-//    [InlineData(EOrderQueryStatus.Appraise, 1)]
-//    public async Task SnapshotOrderStatus_Test(EOrderQueryStatus status, int count)
-//    {
-//        var dto = new OrderInDto { Status = status };
-//        var page = await _snapshotApplication.GetSnapshotOrdersAsync(dto);
-//        page.Total.ShouldNotBe(0, $"状态:{status.GetDescription()} 数据为空");
-//        page.Total.ShouldBe(count, $"状态:{status.GetDescription()} 数据条数错误");
-//        page.Items.FirstOrDefault()?.IndustryName.ShouldNotBeNullOrEmpty();
-//        page.Items.FirstOrDefault()?.OrderNo.ShouldNotBeNullOrEmpty();
-//        page.Items.FirstOrDefault()?.StatusText.ShouldNotBeNullOrEmpty();
-//        page.Items.FirstOrDefault()?.Area.ShouldNotBeNullOrEmpty();
-
-//        dto.PageIndex = 2;
-//        page = await _snapshotApplication.GetSnapshotOrdersAsync(dto);
-//        page.Items.Count.ShouldBe(0);
-//    }
-
-//    [Fact]
-//    public async Task GetSnapshotOrderDetail_Test()
-//    {
-//        var page = await _snapshotApplication.GetSnapshotOrdersAsync(new OrderInDto());
-//        var id = page.Items.First().Id;
-//        var detail = await _snapshotApplication.GetSnapshotOrderDetailAsync(id);
-//        detail.Id.ShouldBe(id);
-//        detail.IndustryName.ShouldNotBeNull();
-//    }
-
-//    [Theory]
-//    [InlineData(2, 2)]
-//    [InlineData(12, 12)]
-//    public async Task GetRedPackDateAsync(int count, int exp)
-//    {
-//        var items = await _snapshotApplication.GetRedPackDateAsync(new RedPackDateInDto { Count = count});
-//        items.Count.ShouldNotBe(0, "0数据");
-//        items.Count.ShouldBe(exp, $"应该:{exp}, 实际 {items.Count}");
-//    }
-
-//    [Theory]
-//    [InlineData(ERedPackPickupStatus.Unreceived)]
-//    [InlineData(ERedPackPickupStatus.Received)]
-//    public async Task GetRedPacksAsync(ERedPackPickupStatus status)
-//    {
-//        var page = await _snapshotApplication.GetRedPacksAsync(new RedPacksInDto { Status = status});
-//        page.Total.ShouldNotBe(0, "数据不应该为空");
-//    }
-
-//    [Fact]
-//    public async Task GetBulletinsDetail_Test()
-//    {
-//        var detail = await _snapshotApplication.GetBulletinsDetailAsync("08dc788f-20f4-4bf1-83d3-b5a8a4f395b0");
-//        detail.Id.ShouldNotBeNullOrEmpty();
-//        detail.Title.ShouldNotBeNullOrEmpty();
-//        detail.Content.ShouldNotBeNullOrEmpty();
-//    }
-
-//    [Fact]
-//    public async Task InitRedPackDataAsync()
-//    {
-//        for (int i = 0; i < 12; i++)
-//        {
-//            var now = DateTime.Now;
-//            var entity = new RedPackRecord
-//            {
-//                OrderId = "111111111",
-//                Amount = 10 * 1000,
-//                CreationTime = new DateTime(2024, i + 1, 02, now.Hour, now.Minute, now.Second),
-//                WXOpenId = "测试生成的OpenId",
-//                PickupStatus = ERedPackPickupStatus.Received,
-//            };
-//            await _redPackRecordRepository.AddAsync(entity);
-//        }
-//    }
-//}

+ 44 - 0
src/Hotline.Application.Tests/Mock/MediatorMock.cs

@@ -0,0 +1,44 @@
+using MediatR;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Application.Tests.Mock;
+public class MediatorMock : IMediator
+{
+    public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamRequest<TResponse> request, CancellationToken cancellationToken = default)
+    {
+        throw new NotImplementedException();
+    }
+
+    public IAsyncEnumerable<object?> CreateStream(object request, CancellationToken cancellationToken = default)
+    {
+        throw new NotImplementedException();
+    }
+
+    public Task Publish(object notification, CancellationToken cancellationToken = default)
+    {
+        throw new NotImplementedException();
+    }
+
+    public async Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification
+    {
+    }
+
+    public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
+    {
+        throw new NotImplementedException();
+    }
+
+    public Task Send<TRequest>(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest
+    {
+        throw new NotImplementedException();
+    }
+
+    public Task<object?> Send(object request, CancellationToken cancellationToken = default)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 59 - 0
src/Hotline.Application.Tests/Repository/OrderVisitRepositoryTest.cs

@@ -0,0 +1,59 @@
+using Hotline.Orders;
+using Hotline.Push.FWMessage;
+using Hotline.Share.Dtos.Push;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Tools;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+using XF.Domain.Repository.Extensions;
+
+namespace Hotline.Application.Tests.Repository;
+public class OrderVisitRepositoryTest
+{
+    private readonly IOrderVisitRepository _orderVisitRepository;
+    private readonly IRepository<OrderVisitDetail> _orderVisitDetailRepository;
+
+    public OrderVisitRepositoryTest(IOrderVisitRepository orderVisitRepository, IRepository<OrderVisitDetail> orderVisitDetailRepository)
+    {
+        _orderVisitRepository = orderVisitRepository;
+        _orderVisitDetailRepository = orderVisitDetailRepository;
+    }
+
+    [Theory]
+    [InlineData("4", "SMSUnsatisfied", "2" , "不满意")]
+    [InlineData("1", "Visited", "5", "非常满意")]
+    public async Task UpdateSmsReply_Test(string content, string visitState, string orgResuktKey,  string orgResuktValue)
+    {
+        var visit = await _orderVisitRepository.Queryable()
+            .Where(m => m.VisitState == EVisitState.SMSVisiting)
+            .OrderByDescending(m => m.CreationTime)
+            .FirstAsync();
+
+        var dto = new PushReceiveMessageDto { ExternalId = visit.Id, IsSmsReply = true, SmsReplyContent = content };
+        var message = new Message();
+        await _orderVisitRepository.UpdateSmsReplyAsync(dto, message);
+        visit = _orderVisitRepository.Get(visit.Id);
+        visit.VisitState.ShouldBe(visitState.ToEnum<EVisitState>());
+        visit.NowEvaluate.Key.ShouldBe(content);
+        visit.NowEvaluate.Value.ShouldBe(orgResuktValue);
+
+        if (content == "4" || content == "5")
+            visit.VisitType.ShouldBeNull();
+
+        await _orderVisitDetailRepository.Queryable()
+            .Where(m => m.VisitId == visit.Id && m.VisitTarget == EVisitTarget.Org)
+            .FirstAsync()
+            .Then(async org =>
+            {
+                org.OrgProcessingResults.Key.ShouldBe(orgResuktKey);
+                org.OrgProcessingResults.Value.ShouldBe(orgResuktValue);
+                org.OrgHandledAttitude.Key.ShouldBe(orgResuktKey);
+                org.OrgHandledAttitude.Value.ShouldBe(orgResuktValue);
+            });
+    }
+}

+ 16 - 6
src/Hotline.Application.Tests/Startup.cs

@@ -38,6 +38,13 @@ using Hotline.Settings.TimeLimitDomain;
 using Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
 using Microsoft.AspNetCore.WebSockets;
 using Hotline.CallCenter.Configs;
+using MediatR;
+using Hotline.Application.Tests.Mock;
+using Hotline.Repository.SqlSugar.Ts;
+using Hotline.Application.CallCenter;
+using Hotline.Application.CallCenter.Calls;
+using Hotline.CallCenter.Configs;
+using Tr.Sdk;
 
 namespace Hotline.Application.Tests;
 public class Startup
@@ -78,12 +85,8 @@ public class Startup
             var appConfiguration = appConfigurationSection.Get<AppConfiguration>();
             if (appConfiguration is null) throw new ArgumentNullException(nameof(appConfiguration));
 
-            //var callCenterConfigurationSection = configuration.GetRequiredSection(nameof(CallCenterConfiguration));
-            //var callCenterConfiguration = callCenterConfigurationSection.Get<CallCenterConfiguration>();
-            //services.AddTrSdk(callCenterConfiguration.TianRun.Address,
-            //            callCenterConfiguration.TianRun.Username,
-            //            callCenterConfiguration.TianRun.Password);
-
+            var callCenterConfigurationSection = configuration.GetRequiredSection(nameof(CallCenterConfiguration));
+            var callCenterConfiguration = callCenterConfigurationSection.Get<CallCenterConfiguration>();
 
             services.Configure<AppConfiguration>(d => appConfigurationSection.Bind(d));
             services.Configure<IdentityConfiguration>(d => configuration.GetSection(nameof(IdentityConfiguration)).Bind(d));
@@ -111,6 +114,12 @@ public class Startup
             });
             services.AddScoped(typeof(IPasswordHasher<>), typeof(PasswordHasher<>));
             services.AddScoped(typeof(ITypedCache<>), typeof(DefaultTypedCache<>));
+            services.AddScoped(typeof(IRepositoryTextSearch<>), typeof(BaseRepositoryTextSearch<>));
+            services.AddScoped<ICallApplication, TianRunCallApplication>();
+            services.AddScoped<ITrApplication, TrApplication>();
+            services.AddTrSdk(callCenterConfiguration.TianRun.Address,
+                        callCenterConfiguration.TianRun.Username,
+                        callCenterConfiguration.TianRun.Password);
             services.RegisterMediatR(appConfiguration);
 
             // services.AddSenparcWeixinServices(configuration);
@@ -135,6 +144,7 @@ public class Startup
             services.AddScoped<YiBinExpireTimeLimit>();
             services.AddScoped<ZiGongExpireTimeLimit>();
             services.AddScoped<LuZhouExpireTimeLimit>();
+            services.AddScoped<IMediator, MediatorMock>();
 
             ServiceLocator.Instance = services.BuildServiceProvider();
         }

+ 1 - 10
src/Hotline.Application.Tests/appsettings.Development.json

@@ -1,13 +1,4 @@
 {
-    "SenparcWeixinSetting": {
-        "IsDebug": true,
-
-        //小程序
-        "WxOpenAppId": "#{WxOpenAppId}#",
-        "WxOpenAppSecret": "#{WxOpenAppSecret}#",
-        "WxOpenToken": "#{WxOpenToken}#",
-        "WxOpenEncodingAESKey": "#{WxOpenEncodingAESKey}#"
-    },
     "AllowedHosts": "*",
     "AppConfiguration": {
         "AppScope": "ZiGong",
@@ -119,7 +110,7 @@
             "UserName": "dev",
             "Password": "123456",
             "HostName": "110.188.24.182",
-            "VirtualHost": "fwt-master"
+            "VirtualHost": "fwt-unit-test"
         }
     },
     //"SmsAccountInfo": {

+ 4 - 2
src/Hotline.Application/Handlers/FlowEngine/WorkflowNextHandler.cs

@@ -53,7 +53,7 @@ public class WorkflowNextHandler : INotificationHandler<NextStepNotify>
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
     private readonly Publisher _publisher;
 
-    public WorkflowNextHandler(
+	public WorkflowNextHandler(
         IOrderDomainService orderDomainService,
         IOrderRepository orderRepository,
         ICapPublisher capPublisher,
@@ -157,7 +157,9 @@ public class WorkflowNextHandler : INotificationHandler<NextStepNotify>
 
                     //    expiredTimeChanged = true;
                     //}
-                    await _orderRepository.Updateable(order).ExecuteCommandAsync(cancellationToken);
+                    if (data.FlowDirection is EFlowDirection.CenterToOrg)
+                        order.SendBackAuditEndTime = await _expireTime.WorkDay_ZG(DateTime.Now);
+					await _orderRepository.Updateable(order).ExecuteCommandAsync(cancellationToken);
                     //await _orderRepository.UpdateAsync(order, cancellationToken);
 
                     //司法行政监督管理-推诿工单

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

@@ -17,6 +17,7 @@ using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Settings;
 using Hotline.Share.Requests;
 using MediatR;
+using Microsoft.AspNetCore.Mvc;
 using SqlSugar;
 using XF.Domain.Authentications;
 using XF.Domain.Entities;
@@ -74,6 +75,21 @@ namespace Hotline.Application.Orders
         /// <returns></returns>
         Task OrderVisitWeb(OrderVisitWebDto dto, CancellationToken cancellationToken);
 
+        /// <summary>
+        /// 回访保存
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        Task SaveOrderVisit(VisitDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 发送回访短信
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task VisitPushSMSAsync(VisitSmsInDto dto, CancellationToken cancellationToken);
+
         /// <summary>
         /// 回访来源统计
         /// </summary>
@@ -218,20 +234,20 @@ namespace Hotline.Application.Orders
         /// <returns></returns>
         ISugarQueryable<OrderBiCentreDataListVo> CentreDataList(ReportPagedRequest dto);
 
-		/// <summary>
-		/// 热点受理类型统计
+        /// <summary>
+        /// 热点受理类型统计
         /// </summary>
-		/// <param name="dto"></param>
-		/// <returns></returns>
-		Task<(List<SystemDicData> acceptTypes, object items)> HotspotAndAcceptTypeStatistics(HotspotAndAcceptTypeStatisticsReq dto);
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task<(List<SystemDicData> acceptTypes, object items)> HotspotAndAcceptTypeStatistics(HotspotAndAcceptTypeStatisticsReq dto);
 
 
-		/// <summary>
-		/// 热点受理类型统计--导出
-		/// </summary>
-		/// <param name="dto"></param>
-		/// <returns></returns>
-		Task<DataTable> HotspotAndAcceptTypeStatisticsExport(HotspotAndAcceptTypeStatisticsReq dto);
+        /// <summary>
+        /// 热点受理类型统计--导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task<DataTable> HotspotAndAcceptTypeStatisticsExport(HotspotAndAcceptTypeStatisticsReq dto);
 
         /// <summary>
         /// 热点受理类型明细

+ 175 - 4
src/Hotline.Application/Orders/OrderApplication.cs

@@ -54,12 +54,19 @@ using XF.Domain.Entities;
 using Hotline.Settings.TimeLimitDomain;
 using Hotline.FlowEngine.WorkflowModules;
 using Hotline.SeedData;
+using Hotline.Share.Enums.Push;
+using Hotline.Push.Notifies;
+using Hotline.Configurations;
+using Microsoft.Extensions.Options;
 
 namespace Hotline.Application.Orders;
 
 public class OrderApplication : IOrderApplication, IScopeDependency
 {
 
+    private readonly IMediator _mediator;
+    private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
+    private readonly IRepository<OrderVisitDetail> _orderVisitedDetailRepository;
     private readonly IOrderDomainService _orderDomainService;
     private readonly IWorkflowDomainService _workflowDomainService;
     private readonly IOrderRepository _orderRepository;
@@ -114,7 +121,10 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         IRepository<OrderPublish> orderPublishRepository,
         IRepository<OrderScreen> orderScreenRepository,
         IRepository<OrderSendBackAudit> orderSendBackAuditRepository,
-        ICalcExpireTime expireTime)
+        ICalcExpireTime expireTime,
+        IMediator mediator,
+        IRepository<OrderVisitDetail> orderVisitedDetailRepository,
+        IOptionsSnapshot<AppConfiguration> appOptions)
     {
         _orderDomainService = orderDomainService;
         _workflowDomainService = workflowDomainService;
@@ -142,6 +152,9 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         _orderPublishRepository = orderPublishRepository;
         _orderSendBackAuditRepository = orderSendBackAuditRepository;
         _expireTime = expireTime;
+        _mediator = mediator;
+        _orderVisitedDetailRepository = orderVisitedDetailRepository;
+        _appOptions = appOptions;
     }
 
     /// <summary>
@@ -269,7 +282,13 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         var IsCenter = _sessionContext.OrgIsCenter;
 
         return _orderRepository.Queryable(canView: !IsCenter).Includes(d => d.OrderDelays)
-            .WhereIF(dto.IsProvince.HasValue, d => d.IsProvince == dto.IsProvince)
+	        .Where(d => SqlFunc.Subqueryable<WorkflowStep>()
+		        .Where(step => step.ExternalId == d.Id &&
+		                       ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == _sessionContext.RequiredUserId) ||
+		                        (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == _sessionContext.RequiredOrgId) ||
+		                        (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && _sessionContext.Roles.Contains(step.RoleId))))
+		        .Any())
+			.WhereIF(dto.IsProvince.HasValue, d => d.IsProvince == dto.IsProvince)
             .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.No.Contains(dto.No!))
             .WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Title.Contains(dto.Title!))
             .WhereIF(dto.Delay.HasValue && dto.Delay == 1, d => d.OrderDelays.Any() == true)
@@ -635,6 +654,158 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         }
     }
 
+    /// <summary>
+    /// 回访保存
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    public async Task SaveOrderVisit(VisitDto dto, CancellationToken cancellationToken)
+    {
+        var visit = await _orderVisitRepository.Queryable()
+            .Includes(d => d.Order)
+            .Includes(d => d.OrderVisitDetails)
+            .FirstAsync(d => d.Id == dto.Id, cancellationToken);
+        if (visit is null)
+            throw UserFriendlyException.SameMessage("未知回访信息");
+
+        if (_appOptions.Value.IsYiBin && visit.VisitState == EVisitState.Visited)
+            throw UserFriendlyException.SameMessage("已回访,不能重复回访");
+
+        var first = dto.VisitDetails.FirstOrDefault(x => x.VisitTarget == EVisitTarget.Org);
+
+        visit.IsPutThrough = dto.IsPutThrough;
+        visit.AgainState = dto.IsAgain ? EAgainState.NeedAgain : EAgainState.NoAgain;
+        visit.EmployeeId = _sessionContext.UserId;
+        visit.CallId = dto.CallId;
+        if (first != null)
+        {
+            visit.NowEvaluate = first.OrgProcessingResults;
+        }
+
+        visit.VisitState = Share.Enums.Order.EVisitState.Visited;
+        visit.VisitTime = DateTime.Now;
+        if (!string.IsNullOrEmpty(visit.CallId))
+        {
+            visit.VisitType = EVisitType.CallVisit;
+        }
+
+        if (visit.VisitType is null)
+        {
+            visit.VisitType = EVisitType.ArtificialVisit;
+        }
+
+        if (first != null)
+        {
+            visit.Order.Visited(first.OrgProcessingResults.Key, first.OrgProcessingResults.Value);
+        }
+
+        visit.OrgJudge = dto.OrgJudge;
+        visit.SeatJudge = dto.SeatJudge;
+
+        if (visit.OrgJudge == true || visit.SeatJudge == true)
+        {
+            visit.JudgeState = EJudgeState.Judging;
+        }
+
+        for (int i = 0;i < visit.OrderVisitDetails.Count;i++)
+        {
+            var detail = visit.OrderVisitDetails[i];
+            var detaildto = dto.VisitDetails.FirstOrDefault(x => x.Id == detail.Id);
+            if (detaildto != null)
+            {
+                if (visit.Order.SourceChannelCode != "RGDH" && detaildto.VisitTarget == EVisitTarget.Seat)
+                {
+                    detaildto.SeatEvaluate = ESeatEvaluate.DefaultSatisfied;
+                }
+
+                _mapper.Map(detaildto, visit.OrderVisitDetails[i]);
+            }
+        }
+
+        await _orderVisitRepository.UpdateAsync(visit, cancellationToken);
+        await _orderVisitedDetailRepository.UpdateRangeAsync(visit.OrderVisitDetails, cancellationToken);
+        await _orderRepository.UpdateAsync(visit.Order, cancellationToken);
+        var orderDto = _mapper.Map<OrderDto>(visit.Order);
+        if (first != null)
+        {
+            //推省上
+            await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderVisited,
+                new PublishVisitDto()
+                {
+                    Order = orderDto,
+                    No = visit.No,
+                    VisitType = visit.VisitType,
+                    VisitName = visit.CreatorName,
+                    VisitTime = visit.VisitTime,
+                    VisitRemark = string.IsNullOrEmpty(first.VisitContent) ? first.OrgProcessingResults?.Value : first.VisitContent,
+                    AreaCode = visit.Order.AreaCode!,
+                    SubjectResultSatifyCode = first.OrgProcessingResults.Key,
+                    FirstSatisfactionCode = visit.Order.FirstVisitResultCode!,
+                    ClientGuid = ""
+                }, cancellationToken: cancellationToken);
+
+            //推门户
+            await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderVisitedWeb, new PublishVisitAllDto()
+            {
+                Id = visit.Id,
+                Order = orderDto,
+                OrderVisitDetails = _mapper.Map<List<VisitDetailDto>>(visit.OrderVisitDetails),
+                VisitName = _sessionContext.UserName,
+                VisitTime = visit.VisitTime,
+                VisitType = visit.VisitType,
+                VisitState = visit.VisitState,
+                PublishTime = visit.PublishTime,
+            }, cancellationToken: cancellationToken);
+        }
+
+        if (first != null)
+        {
+            //写入质检
+            await _qualityApplication.AddQualityAsync(EQualitySource.Visit, visit.Order.Id, visit.Id,
+                cancellationToken);
+        }
+    }
+
+    /// <summary>
+    /// 发送回访短信
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    /// <exception cref="NotImplementedException"></exception>
+    public async Task VisitPushSMSAsync(VisitSmsInDto dto, CancellationToken cancellationToken)
+    {
+        var orderVisitList = await _orderVisitRepository.Queryable()
+            .Includes(d => d.Order)
+            .Where(d => dto.Ids.Contains(d.Id) && d.VisitState == EVisitState.WaitForVisit)
+            .Select(d => new { d.Id, d.Order.SourceChannelCode,  d.Order.Contact, d.Order.Password, d.No, d.OrderId, d.Order.Title, d.Order.FromName })
+            .ToListAsync(cancellationToken);
+
+        foreach (var item in orderVisitList)
+        {
+            var code = "1013";
+            if (item.SourceChannelCode == "ZGSSP") code = "1012";
+            var messageDto = new Share.Dtos.Push.MessageDto
+            {
+                PushBusiness = EPushBusiness.VisitSms,
+                ExternalId = item.Id,
+                OrderId = item.OrderId,
+                PushPlatform = EPushPlatform.Sms,
+                Remark = item.Title,
+                Name = item.FromName,
+                TemplateCode = code,
+                Params = new List<string>() { item.No, item.Password },
+                TelNumber = item.Contact,
+            };
+            await _mediator.Publish(new PushMessageNotify(messageDto), cancellationToken);
+            await _orderVisitRepository.Updateable()
+                .Where(m => m.Id == item.Id)
+                .SetColumns(m => m.VisitState == EVisitState.SMSVisiting)
+                .SetColumns(m => m.VisitType == EVisitType.SmsVisit)
+                .ExecuteCommandAsync();
+        }
+    }
+
     public ISugarQueryable<Order> QueryOrders(QueryOrderDto dto)
     {
         var isCenter = _sessionContext.OrgIsCenter;
@@ -929,7 +1100,7 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         return _orderSpecialDetailRepository.Queryable()
             .Includes(x => x.OrderSpecial)
             .WhereIF(!string.IsNullOrEmpty(dto.OrgName), x => x.OrgName.Contains(dto.OrgName!))
-            .Where(x => x.OrderSpecial.ESpecialType == ESpecialType.ReTransact)
+            .Where(x => x.OrderSpecial.SpecialType == ESpecialType.ReTransact)
             .Where(x => x.OrderSpecial.CreationTime >= dto.StartTime)
             .Where(x => x.OrderSpecial.CreationTime <= dto.EndTime)
             .GroupBy(x => new { Time = x.OrderSpecial.CreationTime.ToString("yyyy-MM-dd"), x.OrgId, x.OrgName })
@@ -958,7 +1129,7 @@ public class OrderApplication : IOrderApplication, IScopeDependency
             .WhereIF(!string.IsNullOrEmpty(dto.OrgName), x => x.OrgName.Contains(dto.OrgName!))
             .WhereIF(!string.IsNullOrEmpty(dto.ErrorName), x => x.ErrorName.Contains(dto.ErrorName!))
             .WhereIF(!string.IsNullOrEmpty(dto.No), x => x.OrderSpecial!.Order!.No!.Contains(dto.No!))
-            .Where(x => x.OrderSpecial.ESpecialType == ESpecialType.ReTransact)
+            .Where(x => x.OrderSpecial.SpecialType == ESpecialType.ReTransact)
             .Where(x => x.OrderSpecial.CreationTime >= dto.StartTime)
             .Where(x => x.OrderSpecial.CreationTime <= dto.EndTime);
     }

+ 5 - 0
src/Hotline.Application/Subscribers/DatasharingSubscriber.cs

@@ -534,6 +534,11 @@ namespace Hotline.Application.Subscribers
                     }
                 }
             }
+            else
+            {
+                //处理省下行回访
+
+            }
         }
 
         /// <summary>

+ 2 - 4
src/Hotline.Application/Tels/TelApplication.cs

@@ -17,7 +17,6 @@ namespace Hotline.Application.Tels;
 public class TelApplication : ITelApplication, IScopeDependency
 {
     private readonly IUserCacheManager _userCacheManager;
-    private readonly ITrClient _trClient;
     private readonly ITelRestRepository _telRestRepository;
     private readonly ITypedCache<Work> _cacheWork;
     private readonly IWorkRepository _workRepository;
@@ -28,15 +27,14 @@ public class TelApplication : ITelApplication, IScopeDependency
         ITelRestRepository telRestRepository,
         ITypedCache<Work> cacheWork,
         IWorkRepository workRepository,
-        IRepository<TelActionRecord> telActionRecordRepository,
-        ITrClient trClient)
+        IRepository<TelActionRecord> telActionRecordRepository
+        )
     {
         _userCacheManager = userCacheManager;
         _telRestRepository = telRestRepository;
         _cacheWork = cacheWork;
         _workRepository = workRepository;
         _telActionRecordRepository = telActionRecordRepository;
-        _trClient = trClient;
     }
 
     /// <summary>

+ 103 - 0
src/Hotline.Repository.SqlSugar/Orders/OrderVisitRepository.cs

@@ -0,0 +1,103 @@
+using Hotline.Caching.Interfaces;
+using Hotline.Caching.Services;
+using Hotline.Orders;
+using Hotline.Push.FWMessage;
+using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Settings;
+using Hotline.Share.Dtos.Push;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Tools;
+using Microsoft.Extensions.Logging;
+using SqlSugar;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+using XF.Domain.Repository.Extensions;
+
+namespace Hotline.Repository.SqlSugar.Orders;
+public class OrderVisitRepository : BaseRepository<OrderVisit>, IOrderVisitRepository, IScopeDependency
+{
+    private readonly IRepository<OrderVisitDetail> _orderVisitDetailRepository;
+    private readonly ILogger<OrderVisitRepository> _logger;
+    private readonly IRepository<Order> _orderRepository;
+    private readonly ISystemDicDataCacheManager _systemDicDataCacheManager;
+
+    public OrderVisitRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder, IRepository<OrderVisitDetail> orderVisitDetailRepository, ILogger<OrderVisitRepository> logger, IRepository<Order> orderRepository, ISystemDicDataCacheManager systemDicDataCacheManager) : base(uow, dataPermissionFilterBuilder)
+    {
+        _orderVisitDetailRepository = orderVisitDetailRepository;
+        _logger = logger;
+        _orderRepository = orderRepository;
+        _systemDicDataCacheManager = systemDicDataCacheManager;
+    }
+
+    /// <summary>
+    /// 用户回访短信回复更新回访状态
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <param name="data"></param>
+    /// <returns></returns>
+    public async Task UpdateSmsReplyAsync(PushReceiveMessageDto dto, Message data)
+    {
+        if (dto.IsSmsReply == false || dto.SmsReplyContent.IsNullOrEmpty() || dto.ExternalId.IsNullOrEmpty()) return;
+
+        var orderVisit = await GetAsync(dto.ExternalId)
+             ?? throw new UserFriendlyException($"回访单不存在, visitId: {dto.ExternalId} message: {data.ToJson()}");
+
+        Dictionary<string, string> dics = new()
+        {
+            { "1", $"非常满意|{EVisitState.Visited}|{ESeatEvaluate.VerySatisfied}|{EVoiceEvaluate.VerySatisfied}|5" },
+            { "2", $"满意|{EVisitState.Visited}|{ESeatEvaluate.Satisfied}|{EVoiceEvaluate.Satisfied}|4"},
+            { "3", $"一般|{EVisitState.Visited}|{ESeatEvaluate.Satisfied}|{EVoiceEvaluate.Normal}|4"},
+            { "4", $"不满意|{EVisitState.SMSUnsatisfied}|{ESeatEvaluate.NoSatisfied}|{EVoiceEvaluate.NoSatisfied}|2"},
+            { "5", $"非常不满意|{EVisitState.SMSUnsatisfied}|{ESeatEvaluate.NoSatisfied}|{EVoiceEvaluate.VeryNoSatisfied}|2"},
+        };
+
+        var replyTxt = dto.SmsReplyContent.Trim();
+        var result = dics[replyTxt];
+        if (result.IsNullOrEmpty()) throw new UserFriendlyException($"用户回复短信内容异常; reply: {replyTxt}");
+        var replySplit = result.Split("|");
+        if (new string[] { "4", "5" }.Contains(replyTxt))
+        {
+            // “短信不满意待回访”状态下,由其他方式再次进行回访,回访方式需更新为最新的回访方式
+            // 故在此置为空
+            orderVisit.VisitType = null;
+        }
+        var visitSatisfaction = _systemDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitSatisfaction)
+            .First(m => m.DicDataValue == replySplit[4]);
+        orderVisit.NowEvaluate = new Share.Dtos.Kv(visitSatisfaction.DicDataValue, visitSatisfaction.DicDataName);
+
+        orderVisit.VisitTime = DateTime.Now;
+        orderVisit.VisitState = replySplit[1].ToEnum<EVisitState>();
+        await UpdateAsync(orderVisit, ignoreNullColumns: false);
+
+        if (orderVisit.VisitState == EVisitState.Visited)
+        {
+            await _orderRepository.GetAsync(orderVisit.OrderId)
+                .Then(async order =>
+                {
+                    order.Visited(replyTxt, replySplit[0]);
+                    await _orderRepository.UpdateAsync(order);
+                });
+
+        }
+        var detailOrg = await _orderVisitDetailRepository.Queryable()
+            .Where(m => m.VisitId == orderVisit.Id && m.VisitTarget == EVisitTarget.Org)
+            .ToListAsync();
+        foreach (var item in detailOrg)
+        {
+            item.OrgProcessingResults = new Share.Dtos.Kv(visitSatisfaction.DicDataValue, visitSatisfaction.DicDataName);
+            item.OrgHandledAttitude = new Share.Dtos.Kv(visitSatisfaction.DicDataValue, visitSatisfaction.DicDataName);
+            await _orderVisitDetailRepository.UpdateAsync(item);
+        }
+
+        await _orderVisitDetailRepository.Queryable()
+            .Where(m => m.VisitId == orderVisit.Id && m.VisitTarget == EVisitTarget.Seat)
+            .FirstAsync()
+            .Then(async detailSeat =>
+            {
+                detailSeat.SeatEvaluate ??= replySplit[2].ToEnum<ESeatEvaluate>();
+                detailSeat.VoiceEvaluate ??= replySplit[3].ToEnum<EVoiceEvaluate>();
+                await _orderVisitDetailRepository.UpdateAsync(detailSeat);
+            });
+    }
+}

+ 8 - 0
src/Hotline.Share/Dtos/FlowEngine/PreviousWorkflowDto.cs

@@ -12,4 +12,12 @@ public class PreviousWorkflowDto : EndWorkflowIdDto
     /// </summary>
     public DateTime? ExpiredTime { get; set; }
 
+    /// <summary>
+    /// 根据办理者类型不同,此字段为不同内容
+    /// <example>
+    /// 部门等级/分类为:depCodes, 角色为:userIds
+    /// </example>
+    /// </summary>
+    public List<FlowStepHandler> NextHandlers { get; set; } = new();
+
 }

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

@@ -680,7 +680,12 @@ namespace Hotline.Share.Dtos.Order
         /// 是否退回审批中
         /// </summary>
         public bool IsReturnUnderApproval {  get; set; }
-    }
+
+        /// <summary>
+        /// 退回截至时间
+        /// </summary>
+        public DateTime? SendBackAuditEndTime { get; set; }
+	}
 
     public class UpdateOrderDto : AddOrderDto
     {

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

@@ -5,4 +5,5 @@ namespace Hotline.Share.Dtos.Order;
 public class OrderPreviousDto : PreviousWorkflowDto
 {
     public string OrderId { get; set; }
+
 }

+ 2 - 0
src/Hotline.Share/Dtos/Order/OrderSpecialDto.cs

@@ -456,6 +456,8 @@ namespace Hotline.Share.Dtos.Order
 
 	public class OrderReTransactDto : OrderSpecialAddDto
 	{
+		public ESpecialType? SpecialType { get; set; }
+
 		/// <summary>
 		/// 错件类型
 		/// </summary>

+ 165 - 4
src/Hotline.Share/Dtos/Order/OrderVisitDto.cs

@@ -59,10 +59,40 @@ namespace Hotline.Share.Dtos.Order
         /// 是否有效智能回访
         /// </summary>
         public bool? IsEffectiveAiVisit { get; set; }
+
+        /// <summary>
+        /// 来电号码
+        /// </summary>
+        public string? FromPhone { get; set; }
+
+        /// <summary>
+        /// 联系电话
+        /// </summary>
+        public string? Contact { get; set; }
+
+        /// <summary>
+        /// 语音评价(话务评价)
+        /// </summary>
+        public List<EVoiceEvaluate> VoiceEvaluate { get; set; } = new();
+
+        /// <summary>
+        /// 话务员评价(话务评价)
+        /// </summary>
+        public List<ESeatEvaluate> SeatEvaluate { get; set; } = new();
+
+        /// <summary>
+        /// 部门办件结果
+        /// </summary>
+        public List<string> OrgProcessingResults { get; set; } = new();
+
+        /// <summary>
+        /// 部门办件态度
+        /// </summary>
+        public List<string> OrgHandledAttitude { get; set; } = new();
     }
 
     public record QueryOrderPublishStatisticsAllDto : PagedRequest
-    { 
+    {
         /// <summary>
         /// 开始时间
         /// </summary>
@@ -77,7 +107,7 @@ namespace Hotline.Share.Dtos.Order
     }
 
     public record QueryOrderPublishStatisticsDto : PagedRequest
-    { 
+    {
         /// <summary>
         /// 开始时间
         /// </summary>
@@ -98,8 +128,8 @@ namespace Hotline.Share.Dtos.Order
     }
 
 
-    public record QueryOrderVisitSourceChannelDto 
-    { 
+    public record QueryOrderVisitSourceChannelDto
+    {
         /// <summary>
         /// 开始时间
         /// </summary>
@@ -232,6 +262,105 @@ namespace Hotline.Share.Dtos.Order
         public string UserId { get; set; }
     }
 
+    public class VisitSmsInDto
+    {
+        [Required]
+        public List<string> Ids { get; set; }
+    }
+
+    /// <summary>
+    /// 批量保存回访入参
+    /// </summary>
+    public class VisitBatchInDto
+    {
+        /// <summary>
+        /// 批量保存回访入参
+        /// </summary>
+        public class VisitIdsBatchInDto
+        {
+            /// <summary>
+            /// 工单Id
+            /// </summary>
+            [Required(ErrorMessage = "工单Id不能为空")]
+            public string OrderId { get; set; }
+
+            /// <summary>
+            /// 回访Id
+            /// </summary>
+            [Required(ErrorMessage = "回访Id不能为空")]
+            public string VisitId { get; set; }
+
+            /// <summary>
+            /// 工单标题
+            /// </summary>
+            [Required(ErrorMessage = "工单标题不能为空")]
+            public string No { get; set; }
+        }
+
+        /// <summary>
+        /// 回访Id集合
+        /// </summary>
+        [Required]
+        public List<VisitIdsBatchInDto> Visit { get; set; }
+
+        /// <summary>
+        /// 话务员评价(话务评价)
+        /// </summary>
+        public ESeatEvaluate? SeatEvaluate { get; set; }
+
+        /// <summary>
+        /// 部门办件结果
+        /// </summary>
+        [Required(ErrorMessage = "部门办结结果不能为空")]
+        public Kv OrgProcessingResults { get; set; }
+
+        /// <summary>
+        /// 部门办件态度
+        /// </summary>
+        [Required(ErrorMessage = "部门办件态度不能为空")]
+        public Kv OrgHandledAttitude { get; set; }
+
+        /// <summary>
+        /// 不满意原因
+        /// </summary>
+        public List<Kv>? OrgNoSatisfiedReason { get; set; }
+
+        /// <summary>
+        /// 话务员回访内容
+        /// </summary>
+        [Required(ErrorMessage = "话务员回访内容不能为空")]
+        public string SeatVisitContent { get; set; }
+
+        /// <summary>
+        /// 部门回访内容
+        /// </summary>
+        [Required(ErrorMessage = "部门回访内容不能为空")]
+        public string OrgVisitContent { get; set; }
+    }
+
+    public class VisitBatchOutDto
+    {
+        /// <summary>
+        /// 总个数
+        /// </summary>
+        public int TotalCount { get; set; }
+
+        /// <summary>
+        /// 成功个数
+        /// </summary>
+        public int CompleteCount { get; set; }
+
+        /// <summary>
+        /// 异常个数
+        /// </summary>
+        public int ErrorCount { get; set; }
+
+        /// <summary>
+        /// 异常信息
+        /// </summary>
+        public string ErrorMessage { get; set; }
+    }
+
     public record VisitDto
     {
         /// <summary>
@@ -477,6 +606,20 @@ namespace Hotline.Share.Dtos.Order
         /// </summary>
         [Description("未回访")]
         NoVisit = 2,
+
+        /// <summary>
+        /// 短信回访中
+        /// 逻辑需求(Task_317) : 操作了短信回访的工单,在没有短信结果回复前,回访状态需从“待回访”更新为“短信回访中”
+        /// </summary>
+        [Description("短信回访中")]
+        SMSVisiting = 21,
+
+        /// <summary>
+        /// 短信不满意待回访
+        /// 逻辑需求(Task_317) : 若短信回访结果为不满意,回访状态需从“短信回访中”更新为“短信不满意待回访”
+        /// </summary>
+        [Description("短信不满意待回访")]
+        SMSUnsatisfied = 41,
     }
 
     public class OrderVisitSourceChannelDto
@@ -712,6 +855,24 @@ namespace Hotline.Share.Dtos.Order
 
     }
 
+    /// <summary>
+    /// 回访详情的历史记录
+    /// </summary>
+    public class OrderVisitDetailHistiryDto
+    {
+        public EVoiceEvaluate? VoiceEvaluate { get; set; }
+        public string? VoiceEvaluateTxt => this.VoiceEvaluate?.GetDescription();
+        public ESeatEvaluate? SeatEvaluate { get; set; }
+        public string? SeatEvaluateTxt => this.SeatEvaluate?.GetDescription();
+        public string VisitOrgName { get; set; }
+        public Kv? OrgProcessingResults { get; set; }
+        public string? orgProcessingResultsValue => this.OrgProcessingResults?.Value;
+        public Kv? OrgHandledAttitude { get; set; }
+        public string? OrgHandledAttitudeValue => this.OrgHandledAttitude?.Value;
+        public string? VisitContent { get; set; }
+        public DateTime? VisitTime { get; set; }
+    }
+
     public class DistributionVisitRspDto
     {
         public int SuccessCount { get; set; }

+ 30 - 0
src/Hotline.Share/Dtos/Order/SendBackDto.cs

@@ -25,6 +25,12 @@ namespace Hotline.Share.Dtos.Order
 		/// </summary>
 		public string? AuditContent { get; set; }
 
+
+		/// <summary>
+		/// 是否允许再次退回
+		/// </summary>
+		public bool? IsReturnAgain { get; set; }
+
 	}
 
 	public class BatchAuditSendBackDto : AuditSendBackDto {
@@ -91,6 +97,30 @@ namespace Hotline.Share.Dtos.Order
 		public ESendBackAuditState State { get; set; }
 
 		public string StateText => State.GetDescription();
+		/// <summary>
+		/// 退回节点名称
+		/// </summary>
+		public string? SendBackStepName { get; set; }
+
+		/// <summary>
+		/// 退回节点创建时间
+		/// </summary>
+		public DateTime? WorkflowStepSendBackCrTime { get; set; }
+
+		/// <summary>
+		///  退回时差
+		/// </summary>
+		public double SendBackTimeDifference => GetSendBackTimeDifference();
+
+		public double GetSendBackTimeDifference() {
+			if (WorkflowStepSendBackCrTime.HasValue)
+			{
+				TimeSpan? timeDifference = CreationTime - WorkflowStepSendBackCrTime;
+				return Math.Round(timeDifference.Value.TotalMinutes / 60, 1);
+			}
+			return 0;
+		}
+
 
 	}
 	public class SendBackBaseDto

+ 15 - 0
src/Hotline.Share/Dtos/Push/FWMessage/MessageDataDto.cs

@@ -95,5 +95,20 @@ namespace Hotline.Share.Dtos.Push.FWMessage
         /// 从发次数
         /// </summary>
         public int ResendCount { get; set; }
+
+        /// <summary>
+        /// 短信回复是否回复
+        /// </summary>   
+        public bool IsSmsReply { get; set; }
+
+        /// <summary>
+        /// 短信回复内容
+        /// </summary>   
+        public string? SmsReplyContent { get; set; }
+
+        /// <summary>
+        /// 短信回复时间
+        /// </summary>
+        public DateTime? SmsReplyTime { get; set; }
     }
 }

+ 10 - 10
src/Hotline.Share/Enums/Order/ESeatEvaluate.cs

@@ -14,11 +14,11 @@ namespace Hotline.Share.Enums.Order
         /// </summary>
         [Description("默认满意")]
         DefaultSatisfied = 0,
-        ///// <summary>
-        ///// 非常不满意
-        ///// </summary>
-        //[Description("非常不满意")]
-        //VeryNoSatisfied = 1,
+        /// <summary>
+        /// 非常不满意
+        /// </summary>
+        [Description("非常不满意")]
+        VeryNoSatisfied = 1,
 
         /// <summary>
         /// 不满意
@@ -26,11 +26,11 @@ namespace Hotline.Share.Enums.Order
         [Description("不满意")]
         NoSatisfied = 2,
 
-        ///// <summary>
-        ///// 一般
-        ///// </summary>
-        //[Description("一般")]
-        //Normal = 3,
+        /// <summary>
+        /// 一般
+        /// </summary>
+        [Description("一般")]
+        Normal = 3,
 
         /// <summary>
         /// 满意

+ 7 - 0
src/Hotline.Share/Enums/Order/ESpecialType.cs

@@ -17,5 +17,12 @@ namespace Hotline.Share.Enums.Order
 		/// 重办
 		/// </summary>
 		ReTransact = 2,
+
+
+
+		/// <summary>
+		/// 退回
+		/// </summary>
+		SendBack = 3,
 	}
 }

+ 14 - 0
src/Hotline.Share/Enums/Order/EVisitState.cs

@@ -21,6 +21,13 @@ namespace Hotline.Share.Enums.Order
         [Description("回访中")]
         Visiting = 20,
 
+        /// <summary>
+        /// 短信回访中
+        /// 逻辑需求(Task_317) : 操作了短信回访的工单,在没有短信结果回复前,回访状态需从“待回访”更新为“短信回访中”
+        /// </summary>
+        [Description("短信回访中")]
+        SMSVisiting = 21,
+
         /// <summary>
         /// 已回访
         /// </summary>
@@ -33,6 +40,13 @@ namespace Hotline.Share.Enums.Order
         [Description("不满意待回访")]
         NoSatisfiedWaitForVisit =40,
 
+        /// <summary>
+        /// 短信不满意待回访
+        /// 逻辑需求(Task_317) : 若短信回访结果为不满意,回访状态需从“短信回访中”更新为“短信不满意待回访”
+        /// </summary>
+        [Description("短信不满意待回访")]
+        SMSUnsatisfied = 41,
+
         /// <summary>
         /// 失效
         /// </summary>

+ 18 - 0
src/Hotline.Share/Enums/Order/EVoiceEvaluate.cs

@@ -9,6 +9,12 @@ namespace Hotline.Share.Enums.Order
 {
     public enum EVoiceEvaluate
     {
+        /// <summary>
+        /// 甄别为满意
+        /// </summary>
+        [Description("甄别为满意")]
+        ScreenSatisfied = -1,
+
         /// <summary>
         /// 非常不满意
         /// </summary>
@@ -44,5 +50,17 @@ namespace Hotline.Share.Enums.Order
         /// </summary>
         [Description("未做评价")]
         NoEvaluate = 7,
+
+        /// <summary>
+        /// 未接通
+        /// </summary>
+        [Description("未接通")]
+        BlockCall = 8,
+
+        /// <summary>
+        /// 默认满意
+        /// </summary>
+        [Description("默认满意")]
+        DefaultSatisfied = 9,
     }
 }

+ 6 - 0
src/Hotline.Share/Enums/Push/EPushBusiness.cs

@@ -54,4 +54,10 @@ public enum EPushBusiness
     /// </summary>
     [Description("批量短信")]
     BatchSms = 7,
+
+    /// <summary>
+    /// 回访短信
+    /// </summary>
+    [Description("回访短信")]
+    VisitSms = 8,
 }

+ 2 - 2
src/Hotline.Share/Tools/ObjectExtensions.cs

@@ -1,5 +1,5 @@
-using System;
-using Newtonsoft.Json;
+using Newtonsoft.Json;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;

+ 17 - 0
src/Hotline.Share/Tools/StringExtensions.cs

@@ -10,4 +10,21 @@ public static class StringExtensions
     {
         return !string.IsNullOrEmpty(str);
     }
+
+    /// <summary>
+    /// 将字符串转换为指定的枚举类型。
+    /// </summary>
+    /// <typeparam name="TEnum">枚举类型</typeparam>
+    /// <param name="value">要转换的字符串</param>
+    /// <returns>枚举值</returns>
+    public static TEnum ToEnum<TEnum>(this string value) where TEnum : struct
+    {
+        if (!typeof(TEnum).IsEnum)
+        {
+            throw new ArgumentException("TEnum must be an enumerated type");
+        }
+
+        // 尝试解析字符串为枚举
+        return (TEnum)Enum.Parse(typeof(TEnum), value);
+    }
 }

+ 21 - 0
src/Hotline.Share/Tools/TaskExtensions.cs

@@ -0,0 +1,21 @@
+namespace XF.Domain.Repository.Extensions;
+public static class TaskExtensions
+{
+    /// <summary>
+    /// 为 Task 类型的扩展方法,如果实体不为 null,则执行指定的操作。
+    /// </summary>
+    /// <typeparam name="T">实体的类型</typeparam>
+    /// <param name="task">Task 实例</param>
+    /// <param name="action">要执行的操作</param>
+    /// <returns>Task 返回自身</returns>
+    public static async Task Then<T>(this Task<T> task, Func<T, Task> action)
+    {
+        var result = await task;
+
+        if (result != null)
+        {
+            await action(result);
+        }
+    }
+}
+  

+ 30 - 1
src/Hotline/CallCenter/Calls/CallNative.cs

@@ -19,106 +19,136 @@ namespace Hotline.CallCenter.Calls
         /// <summary>
         /// 通话记录编号
         /// </summary>
+        [SugarColumn(ColumnDescription = "通话记录编号")]
         public string CallNo { get; set; }
 
+        [SugarColumn(ColumnDescription = "通话方向")]
         public ECallDirection Direction { get; set; }
 
         /// <summary>
         /// 主叫
         /// </summary>
+        [SugarColumn(ColumnDescription = "主叫号码")]
         public string FromNo { get; set; }
 
         /// <summary>
         /// 被叫
         /// </summary>
+        [SugarColumn(ColumnDescription = "被叫号码")]
         public string ToNo { get; set; }
 
         /// <summary>
         /// 响应分机号
         /// </summary>
+        [SugarColumn(ColumnDescription = "响应分机号")]
         public string TelNo { get; set; }
 
         /// <summary>
         /// 挂断方
         /// </summary>
+        [SugarColumn(ColumnDescription = "挂断方")]
         public EEndBy? EndBy { get; set; }
 
         /// <summary>
         /// IVR开始时间
         /// </summary>
+        [SugarColumn(ColumnDescription = "IVR开始时间")]
         public DateTime? BeginIvrTime { get; set; }
+
         /// <summary>
         /// IVR结束时间
         /// </summary>
+        [SugarColumn(ColumnDescription = "IVR结束时间")]
         public DateTime? EndIvrTime { get; set; }
+
         /// <summary>
         /// 开始等待时间
         /// </summary>
+        [SugarColumn(ColumnDescription = "开始等待时间")]
         public DateTime? BeginQueueTime { get; set; }
+
         /// <summary>
         /// 结束等待时间
         /// </summary>
+        [SugarColumn(ColumnDescription = "结束等待时间")]
         public DateTime? EndQueueTime { get; set; }
+
         /// <summary>
         /// 开始振铃时间
         /// </summary>
+        [SugarColumn(ColumnDescription = "开始振铃时间")]
         public DateTime? BeginRingTime { get; set; }
+
         /// <summary>
         /// 结束振铃时间
         /// </summary>
+        [SugarColumn(ColumnDescription = "结束振铃时间")]
         public DateTime? EndRingTime { get; set; }
+
         /// <summary>
         /// 接听时间
         /// </summary>
+        [SugarColumn(ColumnDescription = "接听时间")]
         public DateTime? AnsweredTime { get; set; }
+
         /// <summary>
         /// 挂机时间
         /// </summary>
+        [SugarColumn(ColumnDescription = "挂机时间")]
         public DateTime EndTime { get; set; }
 
         /// <summary>
         /// 分机组id(技能组Id)
         /// </summary>
+        [SugarColumn(ColumnDescription = "分机组ID")]
         public string? GroupId { get; set; }
 
         /// <summary>
         /// 工号
         /// </summary>
+        [SugarColumn(ColumnDescription = "工号")]
         public string? StaffNo { get; set; }
 
         /// <summary>
         /// 话务员id
         /// </summary>
+        [SugarColumn(ColumnDescription = "话务员ID")]
         public string? UserId { get; set; }
 
         /// <summary>
         /// 话务员姓名
         /// </summary>
+        [SugarColumn(ColumnDescription = "话务员姓名")]
         public string? UserName { get; set; }
 
         /// <summary>
         /// 评分
         /// </summary>
+        [SugarColumn(ColumnDescription = "评分")]
         public int Score { get; set; }
 
         /// <summary>
         /// 通话时长(秒)
         /// </summary>
+        [SugarColumn(ColumnDescription = "通话时长(秒)")]
         public int Duration { get; set; }
 
         /// <summary>
         /// 响铃时长(秒)
         /// </summary>
+        [SugarColumn(ColumnDescription = "响铃时长(秒)")]
         public int RingDuration { get; set; }
 
         /// <summary>
         /// 等待时长
         /// </summary>
+        [SugarColumn(ColumnDescription = "等待时长")]
         public int WaitDuration { get; set; }
 
         /// <summary>
         /// 通话录音
         /// </summary>
+        [SugarColumn(ColumnDescription = "通话录音")]
         public string AudioFile { get; set; }
 
         //public string? ExternalId { get; set; }
@@ -126,6 +156,5 @@ namespace Hotline.CallCenter.Calls
         //public string? OrderNo { get; set; }
 
         //public string? Title { get; set; }
-
     }
 }

+ 9 - 7
src/Hotline/FlowEngine/Workflows/WorkflowDomainService.cs

@@ -637,8 +637,14 @@ namespace Hotline.FlowEngine.Workflows
 			//如果有传入期满时间 新节点为传入的期满时间
 			if (dto.ExpiredTime.HasValue)
                 prevStep.StepExpiredTime = dto.ExpiredTime;
-			//复制上一个节点为待接办
-			var newPrevStep =
+			//退给派单组节点,需按照平均分配原则派给一个派单员 禅道299 TODO
+			if (dto.NextHandlers.Any())
+            {
+                var handle = dto.NextHandlers.FirstOrDefault();
+                prevStep.Assign(handle.UserId, handle.Username, handle.OrgId, handle.OrgName, handle.RoleId, handle.RoleName);
+			}
+            //复制上一个节点为待接办
+            var newPrevStep =
                 await DuplicateStepWithTraceAsync(workflow, prevStep, EWorkflowTraceType.Previous, cancellationToken);
 
             //remove workflow.steps
@@ -2007,11 +2013,7 @@ namespace Hotline.FlowEngine.Workflows
             {
                 //newStep.FlowAssignType = EFlowAssignType.User;
                 // 是否中心  临时紧急修改 后续在流程模版定义是否原办理人退回类型 来实现流程 禅道200
-                newStep.FlowAssignType = step.HandlerOrgIsCenter!.Value
-                    ? step.BusinessType is EBusinessType.Send
-                        ? EFlowAssignType.User
-                        : EFlowAssignType.Role
-                    : EFlowAssignType.Org;
+                newStep.FlowAssignType = step.HandlerOrgIsCenter!.Value ? step.BusinessType is EBusinessType.Send ? EFlowAssignType.User : EFlowAssignType.Role : EFlowAssignType.Org;
                 //if (newStep is { FlowAssignType: EFlowAssignType.Role, BusinessType: EBusinessType.Send })
                 //    newStep.FlowAssignType = EFlowAssignType.User;
 

+ 2 - 0
src/Hotline/Orders/AiVisitDomainService.cs

@@ -1,4 +1,5 @@
 using Hotline.Ai.Visit;
+using Hotline.DI;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -9,6 +10,7 @@ using XF.Domain.Repository;
 
 namespace Hotline.Orders
 {
+    [Injection(AppScopes = EAppScope.YiBin)]
     public class AiVisitDomainService : IAiVisitDomainService, IScopeDependency
     {
         private readonly IAiVisitService _aiVisitService;

+ 13 - 0
src/Hotline/Orders/IOrderVisitRepository.cs

@@ -0,0 +1,13 @@
+using XF.Domain.Repository;
+
+namespace Hotline.Orders;
+public interface IOrderVisitRepository : IRepository<OrderVisit>
+{
+    /// <summary>
+    /// 用户回访短信回复更新回访状态
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <param name="data"></param>
+    /// <returns></returns>
+    Task UpdateSmsReplyAsync(Share.Dtos.Push.PushReceiveMessageDto dto, Push.FWMessage.Message data);
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 189 - 64
src/Hotline/Orders/Order.cs


+ 26 - 0
src/Hotline/Orders/OrderSendBackAudit.cs

@@ -20,6 +20,15 @@ namespace Hotline.Orders
 		[SugarColumn(ColumnDescription = "工单ID")]
 		public string OrderId { get; set; }
 
+
+		/// <summary>
+		/// 根据办理者类型不同,此字段为不同内容
+		/// <example>
+		/// 部门等级/分类为:depCodes, 角色为:userIds
+		/// </example>
+		/// </summary>
+		public List<FlowStepHandler>? NextHandlers { get; set; } = new();
+
 		/// <summary>
 		/// 流程快照ID
 		/// </summary>
@@ -99,6 +108,18 @@ namespace Hotline.Orders
 		[SugarColumn(ColumnDescription = "退回部门名称")]
 		public string SendBackOrgName { get; set; }
 
+		/// <summary>
+		/// 退回节点名称
+		/// </summary>
+		[SugarColumn(ColumnDescription = "退回节点名称")]
+		public string? SendBackStepName { get; set; }
+
+		/// <summary>
+		/// 退回节点创建时间
+		/// </summary>
+		[SugarColumn(ColumnDescription = "退回节点创建时间")]
+		public DateTime? WorkflowStepSendBackCrTime { get; set; }
+
 		/// <summary>
 		/// 流程退回发起部门ID
 		/// </summary>
@@ -111,6 +132,11 @@ namespace Hotline.Orders
 		[SugarColumn(ColumnDescription = "流程退回发起用户ID")]
 		public string WorkflowUserId { get; set; }
 
+		/// <summary>
+		/// 是否允许再次退回
+		/// </summary>
+		[SugarColumn(ColumnDescription = "是否允许再次退回",DefaultValue = "f")]
+		public bool? IsReturnAgain { get; set; }
 
 		/// <summary>
 		/// 流程退回发起用户角色ID

+ 1 - 1
src/Hotline/Orders/OrderSpecial.cs

@@ -114,7 +114,7 @@ namespace Hotline.Orders
         /// <summary>
         /// 特提类型
         /// </summary>
-        public ESpecialType? ESpecialType { get; set; }
+        public ESpecialType? SpecialType { get; set; }
 
 		/// <summary>
 		/// 错件类型

+ 21 - 1
src/Hotline/Permissions/EPermission.cs

@@ -539,6 +539,11 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName ="发布待办",Name ="数据范围",Description ="数据范围")]
         PublishDataRange = 200808,
+        /// <summary>
+        /// 工单退回
+        /// </summary>
+        [Display(GroupName ="发布待办",Name ="工单退回",Description ="工单退回")]
+        PublishOrderReturn = 200809,
         #endregion
 
         #region 发布列表
@@ -576,6 +581,7 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName = "回访待办", Name = "回访", Description = "回访")]
         Visit = 200902,
+
         /// <summary>
         /// 分配回访人
         /// </summary>
@@ -596,6 +602,16 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName ="回访待办",Name ="工单类型",Description ="工单类型")]
         OrderTypeForVisit = 200915,
+        /// <summary>
+        /// 短信回访
+        /// </summary>
+        [Display(GroupName = "回访待办", Name ="短信回访",Description ="短信回访")]
+        SmsOrderVisit = 200916,
+        /// <summary>
+        /// 批量回访
+        /// </summary>
+        [Display(GroupName ="回访待办",Name ="批量回访",Description ="批量回访")]
+        BatchOrderVisit = 200917,
         #endregion
 
         #region 回访列表
@@ -604,7 +620,11 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName = "业务管理", Name = "回访列表", Description = "回访列表")]
         OrderVisited = 200904,
-
+        /// <summary>
+        /// 修改回访结果
+        /// </summary>
+        [Display(GroupName ="业务管理",Name ="修改回访结果",Description ="修改回访结果")]
+        ModifyOrderVisit = 200918,
         #endregion
 
         #region 智能回访任务

+ 1 - 1
src/Hotline/Push/FWMessage/Message.cs

@@ -49,7 +49,7 @@ namespace Hotline.Push.FWMessage
         /// <summary>
         /// 短信内容
         /// </summary>
-        [SugarColumn(ColumnDescription = "短信内容", ColumnDataType = "varchar(200)")]
+        [SugarColumn(ColumnDescription = "短信内容", ColumnDataType = "varchar(255)")]
         public string Content { get; set; }
 
         /// <summary>

+ 8 - 2
src/Hotline/Push/FWMessage/PushDomainService.cs

@@ -1,4 +1,5 @@
 using DotNetCore.CAP;
+using Hotline.Orders;
 using Hotline.Share.Dtos.Push;
 using Hotline.Share.Dtos.SendSms;
 using Hotline.Share.Enums.Push;
@@ -28,7 +29,8 @@ public class PushDomainService : IPushDomainService, IScopeDependency
     private readonly IRepository<MessageTemplate> _messageTemplateRepository;
     private readonly ICapPublisher _capPublisher;
     private readonly ILogger<PushDomainService> _logger;
-    
+    private readonly IOrderVisitRepository _orderVisitRepository;
+
     /// <summary>
     /// 
     /// </summary>
@@ -42,13 +44,15 @@ public class PushDomainService : IPushDomainService, IScopeDependency
         IRepository<MessageTemplate> messageTemplateRepository,
         ICapPublisher capPublisher,
         ILogger<PushDomainService> logger
-        )
+,
+        IOrderVisitRepository orderVisitRepository)
     {
         _messageRepository = messageRepository;
         _mapper = mapper;
         _messageTemplateRepository = messageTemplateRepository;
         _capPublisher = capPublisher;
         _logger = logger;
+        _orderVisitRepository = orderVisitRepository;
     }
     #endregion
 
@@ -146,6 +150,8 @@ public class PushDomainService : IPushDomainService, IScopeDependency
                 data.SmsReplyTime = Convert.ToDateTime(dto.SmsReplyTime);
                 data.SmsReplyContent = dto.SmsReplyContent;
 
+                if (data.PushBusiness == EPushBusiness.VisitSms)
+                    await _orderVisitRepository.UpdateSmsReplyAsync(dto, data);
             }
             data.Reason = dto.Reason;
             await _messageRepository.UpdateAsync(data, cancellation);

+ 8 - 2
src/Hotline/Settings/SysDicTypeConsts.cs

@@ -200,9 +200,15 @@ public class SysDicTypeConsts
     public const string SpecialReason = "SpecialReason";
 
     /// <summary>
-    /// 网民评价类型
+    /// 随手拍特提原因
     /// </summary>
-    public const string NetizenEvaluateType = "NetizenEvaluateType";
+    public const string InstaShotSpecialReason = "InstaShotSpecialReason";
+	
+
+	/// <summary>
+	/// 网民评价类型
+	/// </summary>
+	public const string NetizenEvaluateType = "NetizenEvaluateType";
 
 
 	/// <summary>

+ 16 - 4
src/Hotline/Settings/SystemOrganize.cs

@@ -18,62 +18,73 @@ public class SystemOrganize : CreationSoftDeleteEntity
     // /// 部门编码结构中,中心为顶级
     // /// </remarks>
     // /// </summary>
+    // [SugarColumn(ColumnDescription = "编码")]
     // public string Code { get; set; }
 
     /// <summary>
     /// 组织架构名称
     /// </summary>
+    [SugarColumn(ColumnDescription = "组织架构名称")]
     public string Name { get; set; }
 
     /// <summary>
     /// 组织架构简称
     /// </summary>
+    [SugarColumn(ColumnDescription = "组织架构简称")]
     public string ShortName { get; set; }
 
     /// <summary>
     /// 区域Code(行政区域代码)
     /// </summary>
+    [SugarColumn(ColumnDescription = "区域Code(行政区域代码)")]
     public string? AreaCode { get; set; }
 
     /// <summary>
     /// 区域名称(行政区域名称)
     /// </summary>
+    [SugarColumn(ColumnDescription = "区域名称(行政区域名称)")]
     public string? AreaName { get; set; }
 
     /// <summary>
     /// 部门级别
     /// </summary>
+    [SugarColumn(ColumnDescription = "部门级别")]
     public int Level { get; set; }
 
     /// <summary>
     /// 部门类型
     /// </summary>
+    [SugarColumn(ColumnDescription = "部门类型")]
     public EOrgType OrgType { get; set; }
 
     /// <summary>
     /// 上级ID
     /// </summary>
+    [SugarColumn(ColumnDescription = "上级ID")]
     public string? ParentId { get; set; }
 
     /// <summary>
     /// 上级名称
     /// </summary>
+    [SugarColumn(ColumnDescription = "上级名称")]
     public string? ParentName { get; set; }
 
     /// <summary>
     /// 是否启用
     /// </summary>
+    [SugarColumn(ColumnDescription = "是否启用")]
     public bool IsEnable { get; set; }
 
     /// <summary>
     /// 是否为中心
     /// </summary>
+    [SugarColumn(ColumnDescription = "是否为中心")]
     public bool IsCenter { get; set; }
 
-    [SugarColumn(IsIgnore = true)] 
+    [SugarColumn(IsIgnore = true)]
     public List<SystemOrganize> Children { get; set; }
 
-    [SugarColumn(IsIgnore = true)]
+    [SugarColumn(IsIgnore = true , ColumnDescription = "部门类型描述")]
     public string OrgTypeText => OrgType.GetDescription();
 
     public void InitOrgLevel() => Level = Id.CalcOrgLevel();
@@ -81,10 +92,11 @@ public class SystemOrganize : CreationSoftDeleteEntity
     /// <summary>
     /// 旧系统id
     /// </summary>
+    [SugarColumn(ColumnDescription = "旧系统id")]
     public int? oldBmid { get; set; }
-
 }
 
+// ... (Rest of the OrgExtensions class unchanged)
 public static class OrgExtensions
 {
     public static int CalcOrgLevel(this string orgCode)
@@ -132,4 +144,4 @@ public static class OrgExtensions
             throw UserFriendlyException.SameMessage("无效部门编码");
         return orgId == OrgSeedData.CenterId;
     }
-}
+}

+ 29 - 1
src/Hotline/Settings/TimeLimitDomain/ExpireTimeLimitBase.cs

@@ -76,7 +76,35 @@ public abstract class ExpireTimeLimitBase
         return date;
     }
 
-    public async Task<DateTime?> CalcWorkTimeReduce(DateTime now, int timeValue)
+    public virtual async Task<DateTime> WorkDay_ZG(DateTime date)
+    {
+	    //一级部门退回中心的可退回时间为1个工作日(从派单组交办给部门就开始倒计时);节假日派单组派给部门的工单,一级部门退回截止时间该为第2个工作日18: 00
+		var workTime = GetWorkTimes(SettingConstants.WorkTime);
+	    var (WorkBeginTime, WorkEndTime) = GetWorkTime(DateTime.Now, workTime);
+	    if (await IsWorkDay(date))
+	    {
+		    if (date < WorkBeginTime || date > WorkEndTime)
+		    {
+			    date = date.AddDays(1);
+		    }
+	    }
+	    else
+	    {
+		    while (await NotWorkDay(date))
+		    {
+			    date = date.AddDays(1);
+		    }
+		    while (await NotWorkDay(date))
+		    {
+			    date = date.AddDays(1);
+		    }
+		    date = DateTime.Parse(date.ToShortDateString() + "18:00");
+		}
+	    return date;
+    }
+
+
+	public async Task<DateTime?> CalcWorkTimeReduce(DateTime now, int timeValue)
     {
         return await _expireTimeHandler.CalcWorkTimeReduceAsync(now, timeValue);
     }

+ 2 - 1
src/Hotline/Settings/TimeLimitDomain/ICalcExpireTime.cs

@@ -27,8 +27,9 @@ public interface ICalcExpireTime
     Task<int> CalcWorkTime(DateTime beginTime, DateTime endTime, bool isCenter);
     Task<int> CalcWorkTimeEx(DateTime beginTime, DateTime endTime, bool isCenter);
     Task<DateTime> WorkDay(DateTime now);
+    Task<DateTime> WorkDay_ZG(DateTime now);
 
-    Task<TimeResult> CalcEndTime(DateTime beginTime, ETimeType timeType, int timeValue, int Percentage, int PercentageOne);
+	Task<TimeResult> CalcEndTime(DateTime beginTime, ETimeType timeType, int timeValue, int Percentage, int PercentageOne);
 
     /// <summary>
     /// 倒推工作日

+ 1 - 1
src/Hotline/dataview.md

@@ -54,7 +54,7 @@ CASE
 	ELSE
 		'-'
 END AS "OrgLevelTwoName" , 
-"CurrentHandleOrgName" AS "CurrentHandleOrgName" , 
+"ActualHandleOrgName" AS "ActualHandleOrgName" , 
 to_char("FiledTime", 'YYYY-MM-DD HH24:MI:SS') AS "FiledTime" , 
 "AcceptType" AS "AcceptType" , 
 "HotspotName" AS "HotspotName",

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно