소스 검색

Merge branch 'dev' into release

# Conflicts:
#	src/Hotline.Api/Controllers/OrderController.cs
xfe 6 달 전
부모
커밋
a14c1b22d6
100개의 변경된 파일5002개의 추가작업 그리고 620개의 파일을 삭제
  1. 90 19
      src/Hotline.Api/Controllers/Bi/BiCallController.cs
  2. 305 54
      src/Hotline.Api/Controllers/Bi/BiOrderController.cs
  3. 2 0
      src/Hotline.Api/Controllers/CallController.cs
  4. 5 3
      src/Hotline.Api/Controllers/HomeController.cs
  5. 356 101
      src/Hotline.Api/Controllers/KnowledgeController.cs
  6. 399 0
      src/Hotline.Api/Controllers/OldHotlineController.cs
  7. 82 55
      src/Hotline.Api/Controllers/OrderController.cs
  8. 2 2
      src/Hotline.Api/Controllers/OrderRevocationController.cs
  9. 309 0
      src/Hotline.Api/Controllers/OrderTerminateController.cs
  10. 16 16
      src/Hotline.Api/Controllers/TestController.cs
  11. 76 32
      src/Hotline.Api/Controllers/UserController.cs
  12. 34 0
      src/Hotline.Api/Middleware/HeaderMiddleware.cs
  13. 5 2
      src/Hotline.Api/StartupExtensions.cs
  14. 20 2
      src/Hotline.Application.Contracts/Validators/CallCenter/StartEndTimeDtoValidator.cs
  15. 21 0
      src/Hotline.Application.Contracts/Validators/Knowledge/KnowledageCollectGroupValidator.cs
  16. 109 0
      src/Hotline.Application.Tests/Application/KnowApplicationTest.cs
  17. 44 0
      src/Hotline.Application.Tests/Application/OrderVisitApplicationTest.cs
  18. 52 2
      src/Hotline.Application.Tests/Application/ZiGongCallReportApplicationTest.cs
  19. 3 1
      src/Hotline.Application.Tests/DefaultHttpContextAccessor.cs
  20. 1 0
      src/Hotline.Application.Tests/Domain/YiBinExpireTimeTest.cs
  21. 4 1
      src/Hotline.Application.Tests/Startup.cs
  22. 64 6
      src/Hotline.Application/ExportExcel/ExportApplication.cs
  23. 20 3
      src/Hotline.Application/ExportExcel/IExportApplication.cs
  24. 41 1
      src/Hotline.Application/ExportWord/WordHelper.cs
  25. 19 32
      src/Hotline.Application/FlowEngine/WorkflowApplication.cs
  26. 15 4
      src/Hotline.Application/Handlers/FlowEngine/WorkflowEndHandler.cs
  27. 20 4
      src/Hotline.Application/Handlers/FlowEngine/WorkflowPreviousHandler.cs
  28. 18 5
      src/Hotline.Application/Handlers/FlowEngine/WorkflowStartHandler.cs
  29. 2 0
      src/Hotline.Application/Hotline.Application.csproj
  30. 6 0
      src/Hotline.Application/Identity/IIdentityAppService.cs
  31. 106 0
      src/Hotline.Application/Identity/IdentityAppService.cs
  32. 43 1
      src/Hotline.Application/Knowledge/IKnowApplication.cs
  33. 245 153
      src/Hotline.Application/Knowledge/KnowApplication.cs
  34. 6 0
      src/Hotline.Application/Orders/IOrderApplication.cs
  35. 13 0
      src/Hotline.Application/Orders/IOrderVisitApplication.cs
  36. 32 12
      src/Hotline.Application/Orders/OrderApplication.cs
  37. 42 0
      src/Hotline.Application/Orders/OrderVisitApplication.cs
  38. 79 8
      src/Hotline.Application/StatisticalReport/CallReport/CallReportApplicationBase.cs
  39. 9 0
      src/Hotline.Application/StatisticalReport/ICallReportApplication.cs
  40. 12 2
      src/Hotline.Application/Subscribers/DatasharingSubscriber.cs
  41. 80 3
      src/Hotline.Application/Systems/BaseDataApplication.cs
  42. 144 0
      src/Hotline.Application/Tools/RSA.cs
  43. 71 0
      src/Hotline.Application/Tools/RSA_Create.cs
  44. 318 0
      src/Hotline.Application/Tools/RSA_PEM.cs
  45. 74 0
      src/Hotline.Application/Tools/RSA_Unit.cs
  46. 33 0
      src/Hotline.Application/Tools/StreamExtensions.cs
  47. 23 0
      src/Hotline.Application/Tools/StringExtensions.cs
  48. 16 0
      src/Hotline.Application/Users/IUserApplication.cs
  49. 49 0
      src/Hotline.Application/Users/UserApplication.cs
  50. 6 0
      src/Hotline.Repository.SqlSugar/File/FileRepository.cs
  51. 11 3
      src/Hotline.Repository.SqlSugar/Orders/OrderRepository.cs
  52. 10 0
      src/Hotline.Share/Attributes/ContentTypeAttribute.cs
  53. 10 0
      src/Hotline.Share/Attributes/FileExtensionAttribute.cs
  54. 121 1
      src/Hotline.Share/Dtos/CallCenter/BiQueryCallsDto.cs
  55. 140 0
      src/Hotline.Share/Dtos/CallCenter/CenterReportStatisticsDto.cs
  56. 64 0
      src/Hotline.Share/Dtos/CallCenter/QueryCallsDetailDto.cs
  57. 12 2
      src/Hotline.Share/Dtos/File/FileDto.cs
  58. 5 0
      src/Hotline.Share/Dtos/FlowEngine/NextStepOption.cs
  59. 5 0
      src/Hotline.Share/Dtos/FlowEngine/NextStepsDto.cs
  60. 5 0
      src/Hotline.Share/Dtos/Identity/LoginDto.cs
  61. 9 0
      src/Hotline.Share/Dtos/Knowledge/KnowledgeCollectDto.cs
  62. 191 1
      src/Hotline.Share/Dtos/Knowledge/KnowledgeDto.cs
  63. 6 4
      src/Hotline.Share/Dtos/Order/OrderBiDto.cs
  64. 11 2
      src/Hotline.Share/Dtos/Order/OrderDto.cs
  65. 13 1
      src/Hotline.Share/Dtos/Order/OrderStartFlowDto.cs
  66. 222 0
      src/Hotline.Share/Dtos/Order/OrderTerminateDto.cs
  67. 41 1
      src/Hotline.Share/Dtos/Order/OrderVisitDto.cs
  68. 5 0
      src/Hotline.Share/Dtos/Order/OrderWaitedDto.cs
  69. 6 0
      src/Hotline.Share/Dtos/Order/SendBackDto.cs
  70. 10 0
      src/Hotline.Share/Dtos/Settings/TimeConfig.cs
  71. 10 0
      src/Hotline.Share/Dtos/Users/UserDto.cs
  72. 1 1
      src/Hotline.Share/Dtos/Users/UserPagedDto.cs
  73. 34 0
      src/Hotline.Share/Enums/Article/EFileType.cs
  74. 25 0
      src/Hotline.Share/Enums/KnowledgeBase/EKnowledgeHotWordType.cs
  75. 7 1
      src/Hotline.Share/Enums/KnowledgeBase/EKnowledgeRetrievalType.cs
  76. 18 0
      src/Hotline.Share/Enums/Order/EAttitudeType.cs
  77. 9 2
      src/Hotline.Share/Requests/PagedKeywordRequest.cs
  78. 45 0
      src/Hotline.Share/Tools/EnumExtensions.cs
  79. 13 1
      src/Hotline.Share/Tools/StringExtensions.cs
  80. 15 0
      src/Hotline.Share/Tools/TupleExtensions.cs
  81. 13 0
      src/Hotline.Share/Tools/TypeExtensions.cs
  82. 9 2
      src/Hotline/Authentications/Police110SessionContext.cs
  83. 13 6
      src/Hotline/Authentications/ProvinceSessionContext.cs
  84. 2 10
      src/Hotline/Authentications/SessionContextCreator.cs
  85. 9 2
      src/Hotline/Authentications/YbEnterpriseSessionContext.cs
  86. 9 2
      src/Hotline/Authentications/ZzptSessionContext.cs
  87. 1 0
      src/Hotline/Caching/Interfaces/ISystemSettingCacheManager.cs
  88. 2 0
      src/Hotline/Caching/Services/SystemSettingCacheManager.cs
  89. 12 0
      src/Hotline/File/File.cs
  90. 13 1
      src/Hotline/FlowEngine/WorkflowModules/WorkflowModuleConsts.cs
  91. 5 2
      src/Hotline/KnowledgeBase/Knowledge.cs
  92. 6 1
      src/Hotline/KnowledgeBase/KnowledgeCollect.cs
  93. 17 0
      src/Hotline/KnowledgeBase/KnowledgeCollectGroup.cs
  94. 42 0
      src/Hotline/KnowledgeBase/KnowledgeHotWord.cs
  95. 6 1
      src/Hotline/Orders/IOrderRepository.cs
  96. 7 1
      src/Hotline/Orders/Order.cs
  97. 2 0
      src/Hotline/Orders/OrderDomainService.cs
  98. 67 0
      src/Hotline/Orders/OrderTerminate.cs
  99. 5 0
      src/Hotline/Orders/OrderVisit.cs
  100. 147 48
      src/Hotline/Permissions/EPermission.cs

+ 90 - 19
src/Hotline.Api/Controllers/Bi/BiCallController.cs

@@ -1,4 +1,5 @@
 using Hotline.Application.ExportExcel;
+using Hotline.Share.Tools;
 using Hotline.Application.StatisticalReport;
 using Hotline.Application.Systems;
 using Hotline.Caching.Interfaces;
@@ -16,6 +17,7 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using SqlSugar;
 using System.Data;
+using System.Linq.Dynamic.Core;
 using XF.Domain.Repository;
 
 namespace Hotline.Api.Controllers.Bi;
@@ -81,22 +83,94 @@ public class BiCallController : BaseController
     [AllowAnonymous]
     public async Task<FileStreamResult> ExportQueryCallsAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
         => ExcelStreamResult(
-            await _exportApplication.FittingAsync(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.QueryCallsAsync(dto.QueryDto, HttpContext.RequestAborted),
                 list =>
                 {
-                    list.Add(new BiCallDto()
+                    return new BiCallDto()
                     {
                         HourRange = "合计",
                         Hour = 13,
                         Total = list.Sum(p => p.Total),
                         Answered = list.Sum(p => p.Answered),
                         Hanguped = list.Sum(p => p.Hanguped)
-                    });
+                    };
                 }),
             "话务统计分析");
 
+    /// <summary>
+    /// 话务日期统计
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("query_calls_statistics")]
+    public async Task<TotalList<QueryCallsDetailStatistics>> QueryCallsStatisticsAsync([FromQuery] StartEndTimeDto dto)
+    {
+        var items = await _callReportApplication.QueryCallsDetailStatisticsAsync(dto, HttpContext.RequestAborted);
+
+        var total = new QueryCallsDetailStatistics
+        {
+            Date = "合计",
+            InTotal = items.Sum(m => m.InTotal),
+            NotAcceptedHang = items.Sum(m => m.NotAcceptedHang),
+            InConnectionQuantity = items.Sum(m => m.InConnectionQuantity),
+            InNotAnswered = items.Sum(m => m.InNotAnswered),
+            IvrByeCount = items.Sum(m => m.IvrByeCount),
+            OutConnectionQuantity = items.Sum(m => m.OutConnectionQuantity),
+            OutNotAnswered = items.Sum(m => m.OutNotAnswered)
+        };
+
+        return new TotalList<QueryCallsDetailStatistics>(items, total);
+    }
+
+    /// <summary>
+    /// 话务日期统计-导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("query_calls_statistics_export")]
+    public async Task<FileStreamResult> QueryCallsStatisticsExportAsync([FromBody] ExportExcelDto<StartEndTimeDto> dto)
+    {
+        var items = (await _callReportApplication.QueryCallsDetailStatisticsAsync(dto.QueryDto, HttpContext.RequestAborted));
+        return _exportApplication.GetExcelFile(dto, items, "话务日期统计", "Date");
+    }
+
+    /// <summary>
+    /// 话务日期统计详情
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("query_calls_statistics_detail")]
+    public async Task<PagedDto<QueryCallsStatisticsDetailOutDto>> QueryCallsStatisticsDetailAsync([FromQuery] QueryCallsStatisticsDetailInDto dto)
+        => (await _callReportApplication.QueryCallsStatisticsDetailAsync(dto, HttpContext.RequestAborted))
+            .ToPaged();
+
+    /// <summary>
+    /// 话务日期统计详情--导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("query_calls_statistics_detail/export")]
+    public async Task<FileStreamResult> QueryCallsStatisticsDetailAsync([FromBody] ExportExcelDto<QueryCallsStatisticsDetailInDto> dto)
+    {
+        var items = (await _callReportApplication.QueryCallsStatisticsDetailAsync(dto.QueryDto, HttpContext.RequestAborted)).Item2;
+        return _exportApplication.GetExcelFile(dto, items, "话务日期统计详情", "OrderNo");
+    }
+
+    /// <summary>
+    /// 话务日期统计详情页面基础数据
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("query_calls_statistics_detail/base-data")]
+    public async Task<Dictionary<string, dynamic>> QueryCallsStatisticsDetailBaseData()
+    {
+        return _baseDataApplication
+            .EndBy()
+            .Build();
+    }
+
     /// <summary>
     /// 话务日期明细
     /// </summary>
@@ -138,12 +212,12 @@ public class BiCallController : BaseController
     public async Task<FileStreamResult> QueryCallsDetailExportAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
     {
         return ExcelStreamResult(
-            await _exportApplication.FittingAsync(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.QueryCallsDetailAsync(dto.QueryDto),
                 items =>
                 {
-                    var total = new QueryCallsDetailDto
+                    return new QueryCallsDetailDto
                     {
                         Date = "合计",
                         Hour = "",
@@ -160,9 +234,7 @@ public class BiCallController : BaseController
                         OutTotal = items.Sum(p => p.OutTotal),
                         OutConnectionQuantity = items.Sum(p => p.OutConnectionQuantity)
                     };
-                    items.Add(total);
-                }
-                )
+                })
             , "话务日期明细数据");
 
     }
@@ -200,7 +272,7 @@ public class BiCallController : BaseController
 
 
         return ExcelStreamResult(
-            await _exportApplication.FittingAsync(
+            _exportApplication.GetExcelStream(
             dto,
             (await _callReportApplication.QueryCallsDetailInTotalAsync(dto.QueryDto, dto.IsExportAll)).Item2
             ),
@@ -247,12 +319,12 @@ public class BiCallController : BaseController
     [HttpPost("query_calls_hour_detail_list_export")]
     public async Task<FileStreamResult> QueryCallsHourDetailListExportAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
         => ExcelStreamResult(
-            await _exportApplication.FittingAsync(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.QueryCallsHourDetailAsync(dto.QueryDto, HttpContext.RequestAborted),
                 items =>
                 {
-                    var total = new QueryCallsDetailDto
+                    return new QueryCallsDetailDto
                     {
                         Date = "",
                         Hour = "合计",
@@ -269,7 +341,6 @@ public class BiCallController : BaseController
                         OutTotal = items.Sum(p => p.OutTotal),
                         OutConnectionQuantity = items.Sum(p => p.OutConnectionQuantity)
                     };
-                    items.Add(total);
                 })
             , "话务日期明细-时间段");
 
@@ -283,12 +354,12 @@ public class BiCallController : BaseController
     public async Task<FileStreamResult> ExportSeatss([FromBody] ExportExcelDto<ReportRequiredPagedRequest> dto)
     {
         return ExcelStreamResult(
-            await _exportApplication.FittingAsync(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.QuerySeatCallAsync(dto.QueryDto, HttpContext.RequestAborted),
                 list =>
                 {
-                    list.Add(new BiSeatCallsDto()
+                    return new BiSeatCallsDto()
                     {
                         Name = "合计",
                         InTotal = list.Sum(p => p.InTotal),
@@ -303,7 +374,7 @@ public class BiCallController : BaseController
                         LoginDuration = list.Sum(m => m.LoginDuration),
                         RestDuration = list.Sum(m => m.RestDuration),
                         InDurationAvg = list.Sum(m => m.InDurationAvg)
-                    });
+                    };
                 })
             , "坐席话务统计分析");
     }
@@ -378,12 +449,12 @@ public class BiCallController : BaseController
     [AllowAnonymous]
     public async Task<FileStreamResult> ExportQueryHourCall([FromBody] ExportExcelDto<BiQueryHourCallDto> dto)
         => ExcelStreamResult(
-            await _exportApplication.FittingAsync(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.GetCallHourListAsync(dto.QueryDto, HttpContext.RequestAborted),
                 list =>
                 {
-                    list.Add(new TrCallHourDto()
+                    return new TrCallHourDto()
                     {
                         HourTo = "合计",
                         EffectiveCount = list.Sum(p => p.EffectiveCount),
@@ -391,7 +462,7 @@ public class BiCallController : BaseController
                         NoConnectByeCount = list.Sum(p => p.NoConnectByeCount),
                         QueueByeCount = list.Sum(m => m.QueueByeCount),
                         IvrByeCount = list.Sum(m => m.IvrByeCount)
-                    });
+                    };
                 })
             , "通话时段分析");
 
@@ -435,7 +506,7 @@ public class BiCallController : BaseController
     [HttpPost("gateway-query/export")]
     public async Task<FileStreamResult> ExportQueryGatetWay(ExportExcelDto<BiQueryGateWayDto> dto)
         => ExcelStreamResult(
-            await _exportApplication.FittingAsync(
+            _exportApplication.GetExcelStream(
                 dto,
                 await _callReportApplication.GetCallHotLineListAsync(dto.QueryDto, HttpContext.RequestAborted)
                 )

+ 305 - 54
src/Hotline.Api/Controllers/Bi/BiOrderController.cs

@@ -1,6 +1,9 @@
-using Hotline.Application.FlowEngine;
+using AngleSharp.Text;
+using Hotline.Application.ExportExcel;
+using Hotline.Application.FlowEngine;
 using Hotline.Application.Orders;
 using Hotline.Application.StatisticalReport;
+using Hotline.Application.Systems;
 using Hotline.Caching.Interfaces;
 using Hotline.CallCenter.Calls;
 using Hotline.FlowEngine.WorkflowModules;
@@ -24,11 +27,14 @@ using Hotline.Share.Enums.Order;
 using Hotline.Share.Requests;
 using Hotline.Share.Tools;
 using Hotline.Tools;
+using Mapster;
 using MapsterMapper;
 using MediatR;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
+using NPOI.SS.Formula.Functions;
 using SqlSugar;
+using System.ComponentModel;
 using System.Data;
 using Hotline.SeedData;
 using XF.Domain.Authentications;
@@ -40,6 +46,8 @@ namespace Hotline.Api.Controllers.Bi
 {
     public class BiOrderController : BaseController
     {
+        private readonly IExportApplication _exportApplication;
+        private readonly BaseDataApplication _baseDataApplication;
         private readonly IOrderRepository _orderRepository;
         private readonly IRepository<Hotspot> _hotspotTypeRepository;
         private readonly ISystemDicDataCacheManager _sysDicDataCacheManager;
@@ -61,6 +69,7 @@ namespace Hotline.Api.Controllers.Bi
         private readonly IRepository<OrderScreen> _orderScreenRepository;
         private readonly IOrderSecondaryHandlingApplication _orderSecondaryHandlingApplication;
         private readonly IOrderApplication _orderApplication;
+        private readonly IOrderVisitApplication _orderVisitApplication;
         //private readonly ITimeLimitDomainService _timeLimitDomainService;
         private readonly IOrderReportApplication _orderReportApplication;
         private readonly IRepository<SystemArea> _systemAreaRepository;
@@ -70,6 +79,8 @@ namespace Hotline.Api.Controllers.Bi
         private readonly ICalcExpireTime _expireTime;
         private readonly IWorkflowApplication _workflowApplication;
         private readonly ISystemOrganizeRepository _organizeRepository;
+        private readonly IRepository<CallNative> _callNativeRepository;
+
         public BiOrderController(
             IOrderRepository orderRepository,
             IRepository<Hotspot> hotspotTypeRepository,
@@ -100,7 +111,11 @@ namespace Hotline.Api.Controllers.Bi
             IRepository<WorkflowStep> workflowStepRepository,
             IWorkflowApplication workflowApplication,
             ICalcExpireTime expireTime,
-            ISystemOrganizeRepository organizeRepository)
+            ISystemOrganizeRepository organizeRepository,
+            BaseDataApplication baseDataApplication,
+            IExportApplication exportApplication,
+            IOrderVisitApplication orderVisitApplication,
+            IRepository<CallNative> callNativeRepository)
         {
             _orderRepository = orderRepository;
             _hotspotTypeRepository = hotspotTypeRepository;
@@ -132,6 +147,10 @@ namespace Hotline.Api.Controllers.Bi
             _workflowApplication = workflowApplication;
             _expireTime = expireTime;
             _organizeRepository = organizeRepository;
+            _baseDataApplication = baseDataApplication;
+            _exportApplication = exportApplication;
+            _orderVisitApplication = orderVisitApplication;
+            _callNativeRepository = callNativeRepository;
         }
 
         /// <summary>
@@ -265,7 +284,6 @@ namespace Hotline.Api.Controllers.Bi
         /// </summary>
         /// <returns></returns>
         [HttpGet("visit/source")]
-        [AllowAnonymous]
         public async Task<IList<OrderVisitSourceChannelDto>> QueryOrderVisitSourceChannelAsync([FromQuery] QueryOrderVisitSourceChannelDto dto)
             => await _orderApplication.QueryOrderVisitSourceChannelAsync(dto);
 
@@ -274,29 +292,29 @@ namespace Hotline.Api.Controllers.Bi
         /// </summary>
         /// <returns></returns>
         [HttpPost("visit/source/export")]
-        [AllowAnonymous]
         public async Task<FileStreamResult> QueryOrderVisitSourceChannelExportAsync([FromBody] ExportExcelDto<QueryOrderVisitSourceChannelDto> dto)
         {
             var list = await _orderApplication.QueryOrderVisitSourceChannelAsync(dto.QueryDto);
-            if (list != null && list.Count > 0)
-            {
-                list.Add(new OrderVisitSourceChannelDto()
-                {
-                    SourceChannel = "合计",
-                    Count = list.Sum(p => p.Count)
-                });
-            }
-
-            dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
-
-            var dtos = list
-                .Select(stu => _mapper.Map(stu, typeof(OrderVisitSourceChannelDto), dynamicClass))
-                .Cast<object>()
-                .ToList();
+            return _exportApplication.GetExcelFile(dto, list, "回访来源统计", "SourceChannel");
+        }
 
-            var stream = ExcelHelper.CreateStream(dtos);
+        /// <summary>
+        /// 回访量统计
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("visit/quantity")]
+        public async Task<IList<OrderVisitQuantityOutDto>> QueryOrderVisitQuantityAsync([FromQuery] QueryOrderVisitQuantity dto)
+            => await _orderVisitApplication.QueryOrderVisitQuantityAsync(dto);
 
-            return ExcelStreamResult(stream, "回访来源统计");
+        /// <summary>
+        /// 回访量统计--导出
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("visit/quantity/export")]
+        public async Task<FileStreamResult> QueryOrderVisitQuantityExportAsync([FromBody] ExportExcelDto<QueryOrderVisitQuantity> dto)
+        {
+            var items = await _orderVisitApplication.QueryOrderVisitQuantityAsync(dto.QueryDto);
+            return _exportApplication.GetExcelFile(dto, items, "回访量统计");
         }
 
         /// <summary>
@@ -1333,7 +1351,7 @@ namespace Hotline.Api.Controllers.Bi
         }
 
         /// <summary>
-        /// 中心报表统计
+        /// 中心报表统计--宜宾
         /// </summary>
         /// <param name="StartTime"></param>
         /// <param name="EndTime"></param>
@@ -1371,12 +1389,11 @@ namespace Hotline.Api.Controllers.Bi
                 {
                     EffectiveCount = SqlFunc.AggregateSum(SqlFunc.IIF(true, 1, 0)),
                     InvalidCount = 0,
-                    CompletedCount = SqlFunc.AggregateSum(SqlFunc.IIF((int)x.Status >= 300, 1, 0)),
-                    InProgressCount = SqlFunc.AggregateSum(SqlFunc.IIF((int)x.Status < 300, 1, 0))
+                    CompletedCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status >= EOrderStatus.Filed, 1, 0)),
+                    InProgressCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status < EOrderStatus.Filed, 1, 0))
                 })
-                .ToListAsync();
-            if (orderData != null && orderData.Count > 0)
-                centerReportStatisticsDto.CenterReportOrder = orderData[0];
+                .FirstAsync();
+            centerReportStatisticsDto.CenterReportOrder = orderData;
             #endregion
 
             #region 信件来源
@@ -1528,22 +1545,237 @@ namespace Hotline.Api.Controllers.Bi
                     OrgName = s.Name
                 }).ToListAsync();
 
+            centerReportStatisticsDto.OrgStatisticsCityAll = new OrgStatisticsAll
+            {
+                OrgStatistics = listOrgStatisticsCityAll
+            };
+
+            //区县部门
+            var listOrgStatisticsAreaAll = await _orderRepository.Queryable()
+                .Where(o => o.CreationTime >= StartTime && o.CreationTime <= EndTime)
+                .Select(o => new
+                {
+                    OrgCode = o.ActualHandleOrgCode == null || o.ActualHandleOrgCode == "" ? "001" : o.ActualHandleOrgCode.Substring(SqlFunc.MappingColumn<int>("0"), SqlFunc.MappingColumn<int>("6")),
+                })
+                .MergeTable()
+                .LeftJoin<SystemOrganize>((o, s) => o.OrgCode == s.Id)
+                 .Where((o, s) => s.OrgType == EOrgType.County)
+                .GroupBy((o, s) => new
+                {
+                    o.OrgCode,
+                    s.Name
+                })
+                .Select((o, s) => new OrgStatistics
+                {
+                    CountNum = SqlFunc.AggregateCount(o.OrgCode),
+
+                    OrgName = s.Name
+                }).ToListAsync();
+
+            centerReportStatisticsDto.OrgStatisticsAreaAll = new OrgStatisticsAll
+            {
+                OrgStatistics = listOrgStatisticsAreaAll
+            };
+            #endregion
 
+            return centerReportStatisticsDto;
+        }
 
-            //var listOrgStatisticsCityAll = await _orderRepository.Queryable()
-            //  .LeftJoin<SystemOrganize>((it, o) => it.OrgLevelOneCode == o.Id)
-            //  .Where((it, o) => (o.OrgType == EOrgType.City || o.OrgType == EOrgType.Province) && it.CreationTime >= StartTime && it.CreationTime <= EndTime)
-            // .GroupBy((it, o) => new
-            // {
-            //     it.OrgLevelOneCode,
-            //     o.Name
-            // })
-            //  .Select((it, o) => new OrgStatistics
-            //  {
-            //      CountNum = SqlFunc.AggregateCount(it.OrgLevelOneCode),
+        /// <summary>
+        /// 中心报表统计--自贡
+        /// </summary>
+        /// <param name="StartTime"></param>
+        /// <param name="EndTime"></param>
+        /// <returns></returns>
+        [HttpGet("center_report_forms_statistics_zg")]
+        public async Task<CenterReportStatisticsDto> ZgCenterReportFormsStatistics(DateTime StartTime, DateTime EndTime)
+        {
+            CenterReportStatisticsDto centerReportStatisticsDto = new();
 
-            //      OrgName = it.OrgLevelOneCode == "001" ? "市民热线服务中心" : o.Name
-            //  }).ToListAsync();
+            //信件总量
+            int sourceChannelCount = await _orderRepository.Queryable().Where(p => p.CreationTime >= StartTime && p.CreationTime <= EndTime).CountAsync();
+
+            #region 1、通话记录
+            //通话记录
+            var callData = await _callNativeRepository.Queryable()
+               .Where(p => p.CreationTime >= StartTime && p.CreationTime <= EndTime)
+               // .Where(p => p.Gateway != "82826886" && SqlFunc.Length(p.Gateway) != 4)
+               .Select(p => new CenterReportCallInfoDto
+               {
+                   AllCallCount = SqlFunc.AggregateSum(1),//话务总量
+                   InTotal = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.In, 1, 0)),//呼入总量
+                   InConnectionQuantity = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.In && p.AnsweredTime != null, 1, 0)),//呼入接通量
+                   InHanguped = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.In && p.AnsweredTime == null, 1, 0)),//呼入未接通
+                   QueueByeCount = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.In && p.WaitDuration > 0 && p.RingDuration == 0 && p.AnsweredTime == null, 1, 0)), //队列挂断
+                   IvrByeCount = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.In && p.BeginIvrTime.HasValue && !p.BeginQueueTime.HasValue && !p.BeginRingTime.HasValue && p.AnsweredTime == null, 1, 0)), //IVR挂断
+                   OutTotal = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.Out, 1, 0)),//呼出总量
+                   OutConnectionQuantity = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.Out && p.AnsweredTime != null, 1, 0)),//呼出接通量
+                   OutHanguped = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.Out && p.AnsweredTime == null, 1, 0)),//呼出未接通
+
+               })
+               .FirstAsync();
+            centerReportStatisticsDto.CenterReportCallInfoDto = callData;
+
+            #endregion
+
+            #region 2、信件回访量
+            //信件回访量
+            var centerReportVisitd = await _orderVisitRepository.Queryable()
+              .Where(x => x.VisitTime >= StartTime && x.VisitTime <= EndTime)
+              .Select(x => new CenterReportVisitdDto
+              {
+                  Visitd = SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState == EVisitState.Visited, 1, 0)),//已回访
+                  CallVisitd = SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState == EVisitState.Visited && x.VisitType == EVisitType.CallVisit, 1, 0)),// 已回访--电话
+                  SmsVisitd = SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState == EVisitState.Visited && x.VisitType == EVisitType.SmsVisit, 1, 0)),//已回访--短信
+                  OtherVisitd = SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState == EVisitState.Visited && x.VisitType != EVisitType.CallVisit && x.VisitType != EVisitType.SmsVisit, 1, 0)),//其他回访
+                  WaitVisitd = SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState != EVisitState.None && x.VisitState != EVisitState.Visited, 1, 0)),//待回访
+              }).FirstAsync();
+
+            //部门
+            var listOrg = await _orderVisitDetailRepository.Queryable()
+                .LeftJoin<OrderVisit>((it, o) => it.VisitId == o.Id)
+                .Where((it, o) => it.VisitTarget == EVisitTarget.Org && o.VisitTime >= StartTime && o.VisitTime <= EndTime && o.VisitState == EVisitState.Visited)
+                 .Select((it, o) => new Satisfaction
+                 {
+                     Dissatisfied = SqlFunc.AggregateSum(SqlFunc.IIF(SqlFunc.JsonListObjectAny(it.OrgProcessingResults, "key", "1") || SqlFunc.JsonListObjectAny(it.OrgProcessingResults, "key", "2"), 1, 0)),
+                     Satisfied = SqlFunc.AggregateSum(SqlFunc.IIF(SqlFunc.JsonListObjectAny(it.OrgProcessingResults, "key", "1") || SqlFunc.JsonListObjectAny(it.OrgProcessingResults, "key", "2"), 0, 1)),
+                 })
+                .FirstAsync();
+            //计算部门满意度
+            if (listOrg != null)
+            {
+                var satisfiedCount = listOrg.Satisfied + listOrg.Dissatisfied;
+                if (satisfiedCount > 0 && listOrg.Satisfied > 0)
+                    centerReportVisitd.OrgRate = Math.Round((listOrg.Satisfied / (double)satisfiedCount) * 100, 2);
+            }
+
+            //坐席
+            var listSet = await _orderVisitDetailRepository.Queryable()
+                .LeftJoin<OrderVisit>((it, o) => it.VisitId == o.Id)
+                .Where((it, o) => it.VisitTarget == EVisitTarget.Seat && o.VisitTime >= StartTime && o.VisitTime <= EndTime && o.VisitState == EVisitState.Visited)
+                .Select((it, o) => new Satisfaction
+                {
+                    Dissatisfied = SqlFunc.AggregateSum(SqlFunc.IIF(it.SeatEvaluate == ESeatEvaluate.NoSatisfied, 1, 0)),
+                    Satisfied = SqlFunc.AggregateSum(SqlFunc.IIF(it.SeatEvaluate != ESeatEvaluate.NoSatisfied, 1, 0)),
+                })
+                .FirstAsync();
+            //计算坐席满意度
+            if (listSet != null)
+            {
+                var satisfiedCount = listSet.Satisfied + listSet.Dissatisfied;
+                if (satisfiedCount > 0 && listSet.Satisfied > 0)
+                    centerReportVisitd.OrgRate = Math.Round((listSet.Satisfied / (double)satisfiedCount) * 100, 2);
+            }
+
+            centerReportStatisticsDto.CenterReportVisitd = centerReportVisitd;
+            #endregion
+
+            #region 3、工单
+            //工单
+            var orderData = await _orderRepository.Queryable()
+                .Where(p => p.CreationTime >= StartTime && p.CreationTime <= EndTime)
+                .Select(x => new CenterReportOrderDto
+                {
+                    EffectiveCount = SqlFunc.AggregateSum(SqlFunc.IIF(true, 1, 0)),//有效
+                    InvalidCount = 0,//无效
+                    CompletedCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status >= EOrderStatus.Filed, 1, 0)),//已办结
+                    OnTimeCompletedCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status >= EOrderStatus.Filed && x.ExpiredTime < x.ActualHandleTime, 1, 0)),//按时办结
+                    CenterCompletedCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status >= EOrderStatus.Filed && x.ProcessType == EProcessType.Zhiban, 1, 0)),//中心办结
+                    OrgCompletedCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status >= EOrderStatus.Filed && x.ProcessType == EProcessType.Jiaoban, 1, 0)),//部门办结
+                    InProgressCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status < EOrderStatus.Filed, 1, 0)),//在办
+                    CenterInProgressCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status < EOrderStatus.Filed && x.ProcessType == EProcessType.Zhiban, 1, 0)),//中心在办
+                    OrgInProgressCount = SqlFunc.AggregateSum(SqlFunc.IIF(x.Status < EOrderStatus.Filed && x.ProcessType == EProcessType.Jiaoban, 1, 0))//部门在办
+                })
+                .FirstAsync();
+            centerReportStatisticsDto.CenterReportOrder = orderData;
+            #endregion
+
+            #region 4、信件来源
+
+            //信件来源
+            var sourceChannelData = _orderRepository.Queryable()
+                .Where(p => p.CreationTime >= StartTime && p.CreationTime <= EndTime)
+                .Select(it => new
+                {
+                    SourceChannelCode = SqlFunc.IIF(SqlFunc.IsNullOrEmpty(it.SourceChannelCode), "QT", it.SourceChannelCode)
+                })
+                .MergeTable()//将查询出来的结果合并成一个新表
+                 .GroupBy(it => new { it.SourceChannelCode })//对新表进行分组
+                 .Select(it => new CenterReportOrderSourceChannelDto
+                 {
+                     Code = it.SourceChannelCode,
+                     CountNum = SqlFunc.AggregateCount(it.SourceChannelCode)
+                 });
+
+            var sourceData = await _systemDicDataRepository.Queryable()
+               .LeftJoin(sourceChannelData, (s, p) => s.DicDataValue == p.Code)
+              .Where((s, p) => s.DicTypeCode == "SourceChannel" && s.IsShow == true)
+              .Select((s, p) => new CenterReportOrderSourceChannelDto
+              {
+                  Code = s.DicDataValue,
+                  Name = s.DicDataName,
+                  CountNum = p.CountNum
+              })
+
+              .ToListAsync();
+
+            centerReportStatisticsDto.CenterReportOrderSourceChannels = sourceData;
+            #endregion
+
+            #region 5、信件分类
+            //信件来源 AcceptType
+
+            var acceptTypeData = _orderRepository.Queryable(false, false, false)
+                .Where(p => p.CreationTime >= StartTime && p.CreationTime <= EndTime)
+                .Select(it => new
+                {
+                    AcceptTypeCode = SqlFunc.IIF(SqlFunc.IsNullOrEmpty(it.AcceptTypeCode), "40", it.AcceptTypeCode)
+                })
+                .MergeTable()//将查询出来的结果合并成一个新表
+                 .GroupBy(it => new { it.AcceptTypeCode })//对新表进行分组
+                 .Select(it => new CenterReportOrderSourceChannelDto
+                 {
+                     Code = it.AcceptTypeCode,
+                     CountNum = SqlFunc.AggregateCount(it.AcceptTypeCode)
+                 });
+
+            var acceptType = await _systemDicDataRepository.Queryable()
+              .LeftJoin(acceptTypeData, (s, p) => s.DicDataValue == p.Code)
+             .Where((s, p) => s.DicTypeCode == "AcceptType" && s.IsShow == true)
+             .Select((s, p) => new CenterReportOrderSourceChannelDto
+             {
+                 AllCountNum = sourceChannelCount,
+                 Code = s.DicDataValue,
+                 Name = s.DicDataName,
+                 CountNum = p.CountNum
+             })
+             .ToListAsync();
+
+            centerReportStatisticsDto.CenterReportOrderAcceptTypes = acceptType;
+            #endregion
+
+            #region 信件分布情况
+            //市直部门
+
+            var listOrgStatisticsCityAll = await _orderRepository.Queryable()
+                .Where(o => o.CreationTime >= StartTime && o.CreationTime <= EndTime)
+                .Select(o => new
+                {
+                    OrgCode = o.ActualHandleOrgCode == null || o.ActualHandleOrgCode == "" ? "001" : o.ActualHandleOrgCode.Substring(SqlFunc.MappingColumn<int>("0"), SqlFunc.MappingColumn<int>("6")),
+                })
+                .MergeTable()
+                .LeftJoin<SystemOrganize>((o, s) => o.OrgCode == s.Id)
+                 .Where((o, s) => s.OrgType == EOrgType.City || s.OrgType == EOrgType.Province)
+                .GroupBy((o, s) => new
+                {
+                    o.OrgCode,
+                    s.Name
+                })
+                .Select((o, s) => new OrgStatistics
+                {
+                    CountNum = SqlFunc.AggregateCount(o.OrgCode),
+                    OrgName = s.Name
+                }).ToListAsync();
 
             centerReportStatisticsDto.OrgStatisticsCityAll = new OrgStatisticsAll
             {
@@ -1571,19 +1803,6 @@ namespace Hotline.Api.Controllers.Bi
 
                     OrgName = s.Name
                 }).ToListAsync();
-            //var listOrgStatisticsAreaAll = await _orderRepository.Queryable()
-            // .LeftJoin<SystemOrganize>((it, o) => it.OrgLevelOneCode == o.Id)
-            // .Where((it, o) => o.OrgType == EOrgType.County && it.CreationTime >= StartTime && it.CreationTime <= EndTime)
-            //.GroupBy((it, o) => new
-            //{
-            //    it.OrgLevelOneCode,
-            //    o.Name
-            //})
-            // .Select((it, o) => new OrgStatistics
-            // {
-            //     CountNum = SqlFunc.AggregateCount(it.OrgLevelOneCode),
-            //     OrgName = it.OrgLevelOneCode == "001" ? "市民热线服务中心" : o.Name
-            // }).ToListAsync();
 
             centerReportStatisticsDto.OrgStatisticsAreaAll = new OrgStatisticsAll
             {
@@ -2214,7 +2433,7 @@ namespace Hotline.Api.Controllers.Bi
             .WhereIF(!string.IsNullOrEmpty(dto.PhoneNo), d => d.Contact == dto.PhoneNo!) //联系电话
             //.WhereIF(!string.IsNullOrEmpty(dto.PushTypeCode), d => d.PushTypeCode == dto.PushTypeCode) //推送分类
             .WhereIF(!string.IsNullOrEmpty(dto.PushTypeCode), d => d.OrderPushTypes.Any(opt => opt.PushTypeCode == dto.PushTypeCode)) //推送分类
-			.WhereIF(dto.ExpiredTimeStart.HasValue, d => d.ExpiredTime >= dto.ExpiredTimeStart) //超期时间开始
+            .WhereIF(dto.ExpiredTimeStart.HasValue, d => d.ExpiredTime >= dto.ExpiredTimeStart) //超期时间开始
             .WhereIF(dto.ExpiredTimeEnd.HasValue, d => d.ExpiredTime <= dto.ExpiredTimeEnd) //超期时间结束
             //.WhereIF(dto.Statuses.Any(), d => dto.Statuses.Contains(d.Status))  //工单状态
             .WhereIF(dto.Status.HasValue, d => d.Status == dto.Status)//工单状态
@@ -3136,6 +3355,11 @@ namespace Hotline.Api.Controllers.Bi
         [HttpGet("org-visitdetail-list-basedata")]
         public async Task<object> OrgVisitDetailListBaseData()
         {
+            return _baseDataApplication
+                .VisitSatisfaction()
+                .OrgsOptions(_sessionContext)
+                .AttitudeType()
+                .Build();
             var VisitSatisfaction = _systemDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitSatisfaction);
 
             if (_sessionContext.OrgIsCenter)
@@ -4200,5 +4424,32 @@ namespace Hotline.Api.Controllers.Bi
             var stream = ExcelHelper.CreateStream(dtos);
             return ExcelStreamResult(stream, "扭转信件统计");
         }
+
+        /// <summary>
+        /// 部门延期统计明细-单独菜单
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("order_delay_detail")]
+        public async Task<PagedDto<OrderDelayDto>> QueryOrderDelayDataDetailAsync([FromQuery] QueryOrderDelayDataDetailRequest dto)
+        {
+            var query = _orderDelayRepository.Queryable()
+                 .Includes(x => x.Order)
+                 .WhereIF(dto.StartTime.HasValue, x => x.CreationTime >= dto.StartTime)
+                 .WhereIF(dto.EndTime.HasValue, x => x.CreationTime <= dto.EndTime)
+                .WhereIF(dto.TypeId != null && dto.TypeId == 1, x => x.Order.IdentityType == EIdentityType.Citizen)
+                .WhereIF(dto.TypeId != null && dto.TypeId == 2, x => x.Order.IdentityType == EIdentityType.Enterprise)
+                 .WhereIF(dto.Type is 1, x => x.DelayState == EDelayState.Pass)
+                 .WhereIF(dto.Type is 2, x => x.DelayState == EDelayState.NoPass)
+                 .WhereIF(dto.Type is 3, x => x.DelayState == EDelayState.Examining)
+                 .WhereIF(dto.Type is 4, x => x.DelayState < EDelayState.Withdraw)
+             .MergeTable();
+
+            if (_sessionContext.OrgIsCenter == false)
+                query = query.Where(x => x.ApplyOrgCode.StartsWith(_sessionContext.OrgId));
+
+            var (total, items) = await query.ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+            return new PagedDto<OrderDelayDto>(total, _mapper.Map<IReadOnlyList<OrderDelayDto>>(items));
+        }
     }
 }

+ 2 - 0
src/Hotline.Api/Controllers/CallController.cs

@@ -174,6 +174,8 @@ namespace Hotline.Api.Controllers
 				var Id = call.TransliterationId;
 				var setting = _systemSettingCacheManager.GetSetting(SettingConstants.ViteRecordPrefix);
 
+
+
 				var handler = new AiQualityHandler()
 				{
 					Id = Id,

+ 5 - 3
src/Hotline.Api/Controllers/HomeController.cs

@@ -181,12 +181,14 @@ public class HomeController : BaseController
             IsTelNeedVerify = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.IsTelNeedVerify).SettingValue[0]),
             TalkingDealTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.TalkingDealTime).SettingValue[0]),
             IsCustomEvent = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.IsCustomEvent).SettingValue[0]),
-			IsTranspondCity = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.TranspondCity).SettingValue[0]),
-			IsAverageSendOrder = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.AverageSendOrder).SettingValue[0]),
+            IsTranspondCity = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.TranspondCity).SettingValue[0]),
+            IsAverageSendOrder = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.AverageSendOrder).SettingValue[0]),
             IsOpenJudicialManagement = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.IsOpenJudicialManagement).SettingValue[0]),
             NoSignOrgCode = _systemSettingCacheManager.GetSetting(SettingConstants.NoSignOrgCode).SettingValue,
             ApplyDelayTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.ApplyDelayTime).SettingValue[0]),
-			IsOpenRepeatedWorkOrders = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.IsOpenRepeatedWorkOrders).SettingValue[0])
+            IsOpenRepeatedWorkOrders = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.IsOpenRepeatedWorkOrders).SettingValue[0]),
+            OldHotlineUrl = _systemSettingCacheManager.GetSetting(SettingConstants.OldHotlineUrl).SettingValue[0],
+            OldHotlineOrderState  = _systemSettingCacheManager.GetSetting(SettingConstants.OldHotlineOrderState).SettingValue[0]
         };
         return rsp;
     }

+ 356 - 101
src/Hotline.Api/Controllers/KnowledgeController.cs

@@ -1,14 +1,20 @@
-using DotNetCore.CAP;
+using DocumentFormat.OpenXml.Wordprocessing;
+using DotNetCore.CAP;
 using Hotline.Api.Filter;
 using Hotline.Application.Bulletin;
+using Hotline.Application.ExportExcel;
+using Hotline.Application.ExportWord;
 using Hotline.Application.FlowEngine;
 using Hotline.Application.Knowledge;
+using Hotline.Application.Systems;
+using Hotline.Application.Tools;
 using Hotline.File;
 using Hotline.FlowEngine.WorkflowModules;
 using Hotline.KnowledgeBase;
 using Hotline.KnowledgeBase.Notifies;
 using Hotline.Permissions;
 using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Repository.SqlSugar.Knowledge;
 using Hotline.Repository.SqlSugar.Ts;
 using Hotline.Settings;
 using Hotline.Settings.Hotspots;
@@ -16,13 +22,19 @@ using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.File;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Knowledge;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Article;
 using Hotline.Share.Enums.KnowledgeBase;
 using Hotline.Share.Mq;
+using Hotline.Share.Tools;
 using Hotline.Users;
+using J2N.Text;
+using Mapster;
 using MapsterMapper;
 using MediatR;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Org.BouncyCastle.Utilities.IO;
 using SqlSugar;
 using System.Threading;
 using XF.Domain.Authentications;
@@ -37,6 +49,11 @@ namespace Hotline.Api.Controllers
     {
 
         #region 注入
+        private readonly IExportApplication _exportApplication;
+        private readonly IRepository<KnowledgeHotWord> _knowledgeHotWordRepository;
+        private readonly IRepository<KnowledgeWord> _knowledgeWordRepository;
+        private readonly IWordHelperService _wordHelperService;
+        private readonly BaseDataApplication _baseDataApplication;
         private readonly IKnowledgeRepository _knowledgeRepository;
         private readonly ISessionContext _sessionContext;
         private readonly IKnowledgeDomainService _knowledgeDomainService;
@@ -60,6 +77,8 @@ namespace Hotline.Api.Controllers
         private readonly ICapPublisher _capPublisher;
         private readonly IRepository<KnowledgeRelationType> _knowledgeRelationTypeRepository;
         private readonly IBulletinApplication _bulletinApplication;
+        private readonly IRepository<KnowledgeCollectGroup> _knowledgeCollectGroupRepository;
+        private readonly IRepository<KnowledgePv> _knowledgePvepository;
 
 
         public KnowledgeController(
@@ -85,8 +104,15 @@ namespace Hotline.Api.Controllers
            IFileRepository fileRepository,
            ICapPublisher capPublisher,
            IRepository<KnowledgeRelationType> knowledgeRelationTypeRepository,
-            IBulletinApplication bulletinApplication
-		   )
+            IBulletinApplication bulletinApplication,
+            IRepository<KnowledgeCollectGroup> knowledgeCollectGroupRepository,
+            IExportApplication exportApplication,
+            BaseDataApplication baseDataApplication,
+            IWordHelperService wordHelperService,
+            IRepository<KnowledgePv> knowledgePvepository,
+            IRepository<KnowledgeWord> knowledgeWordRepository,
+            IRepository<KnowledgeHotWord> knowledgeWordHotRepository,
+            IRepository<KnowledgeHotWord> knowledgeHotWordRepository)
         {
             _knowledgeRepository = knowledgeRepository;
             _sessionContext = sessionContext;
@@ -110,7 +136,14 @@ namespace Hotline.Api.Controllers
             _fileRepository = fileRepository;
             _capPublisher = capPublisher;
             _knowledgeRelationTypeRepository = knowledgeRelationTypeRepository;
-            _bulletinApplication= bulletinApplication;
+            _bulletinApplication = bulletinApplication;
+            _knowledgeCollectGroupRepository = knowledgeCollectGroupRepository;
+            _exportApplication = exportApplication;
+            _baseDataApplication = baseDataApplication;
+            _wordHelperService = wordHelperService;
+            _knowledgePvepository = knowledgePvepository;
+            _knowledgeWordRepository = knowledgeWordRepository;
+            _knowledgeHotWordRepository = knowledgeHotWordRepository;
         }
 
         #endregion
@@ -130,7 +163,7 @@ namespace Hotline.Api.Controllers
             //var addDto = _mapper.Map<AddKnowledgeDto>(dto.Data);
             var kn = _mapper.Map<Knowledge>(dto.Data);
             kn.SourceOrganizeId = _sessionContext.RequiredOrgId;
-			var any = await _knowledgeRepository.Queryable().Where(x => x.Status == EKnowledgeStatus.OnShelf && x.Title == kn.Title).AnyAsync();
+            var any = await _knowledgeRepository.Queryable().Where(x => x.Status == EKnowledgeStatus.OnShelf && x.Title == kn.Title).AnyAsync();
             if (any) throw UserFriendlyException.SameMessage("当前知识标题存在重复标题!");
 
             //Code为空,从新生成Code
@@ -141,13 +174,14 @@ namespace Hotline.Api.Controllers
             kn.InitId();
             if (dto.Data.Files.Any()) kn.FileJson = await _fileRepository.AddFileAsync(dto.Data.Files, kn.Id, "", HttpContext.RequestAborted);
             await _knowledgeRepository.AddAsync(kn, HttpContext.RequestAborted);
+
             if (dto.Data.KnowledgeType.Any())
             {
-				List<KnowledgeRelationType> types = _mapper.Map<List<KnowledgeRelationType>>(dto.Data.KnowledgeType);
-				types.ForEach(x => x.KnowledgeId = kn.Id);
-				await _knowledgeRelationTypeRepository.AddRangeAsync(types, HttpContext.RequestAborted);
-			}
-			if (dto.Workflow != null && !string.IsNullOrEmpty(kn.Id))
+                List<KnowledgeRelationType> types = _mapper.Map<List<KnowledgeRelationType>>(dto.Data.KnowledgeType);
+                types.ForEach(x => x.KnowledgeId = kn.Id);
+                await _knowledgeRelationTypeRepository.AddRangeAsync(types, HttpContext.RequestAborted);
+            }
+            if (dto.Workflow != null && !string.IsNullOrEmpty(kn.Id))
             {
                 var startDto = _mapper.Map<StartWorkflowDto>(dto.Workflow);
                 startDto.DefinitionModuleCode = WorkflowModuleConsts.KnowledgeAdd;
@@ -159,11 +193,10 @@ namespace Hotline.Api.Controllers
                 //knowledge.Status = EKnowledgeStatus.Auditing;
                 //await _knowledgeRepository.UpdateAsync(knowledge, HttpContext.RequestAborted);
             }
-         
+
             return kn.Id;
         }
 
-
         /// <summary>
         /// 知识库-知识下架
         /// </summary>
@@ -253,22 +286,22 @@ namespace Hotline.Api.Controllers
             _mapper.Map(dto.Data, knowledge);
             //if (update.Tags.Any()) await _repositoryts.UpdateVectorAsync(update.Id, update.Tags, HttpContext.RequestAborted);
 
-            if (dto.Data.Files.Any()) 
+            if (dto.Data.Files.Any())
                 knowledge.FileJson = await _fileRepository.AddFileAsync(dto.Data.Files, knowledge.Id, "", HttpContext.RequestAborted);
             else
-	            knowledge.FileJson = new List<Share.Dtos.File.FileJson>();
-			if (dto.Workflow != null) knowledge.Renewaln = knowledge.Status != EKnowledgeStatus.Drafts;
+                knowledge.FileJson = new List<Share.Dtos.File.FileJson>();
+            if (dto.Workflow != null) knowledge.Renewaln = knowledge.Status != EKnowledgeStatus.Drafts;
             await _knowledgeRepository.UpdateAsync(knowledge, HttpContext.RequestAborted);
             if (dto.Data.KnowledgeType.Any())
             {
-	            var anyRelationTypes = await _knowledgeRelationTypeRepository.Queryable().Where(x => x.KnowledgeId == knowledge.Id).ToListAsync();
-	            if (anyRelationTypes.Any())
-		            await _knowledgeRelationTypeRepository.RemoveRangeAsync(anyRelationTypes);
-	            List<KnowledgeRelationType> types = _mapper.Map<List<KnowledgeRelationType>>(dto.Data.KnowledgeType);
-	            types.ForEach(x => x.KnowledgeId = update.Id);
-	            await _knowledgeRelationTypeRepository.AddRangeAsync(types, HttpContext.RequestAborted);
+                var anyRelationTypes = await _knowledgeRelationTypeRepository.Queryable().Where(x => x.KnowledgeId == knowledge.Id).ToListAsync();
+                if (anyRelationTypes.Any())
+                    await _knowledgeRelationTypeRepository.RemoveRangeAsync(anyRelationTypes);
+                List<KnowledgeRelationType> types = _mapper.Map<List<KnowledgeRelationType>>(dto.Data.KnowledgeType);
+                types.ForEach(x => x.KnowledgeId = update.Id);
+                await _knowledgeRelationTypeRepository.AddRangeAsync(types, HttpContext.RequestAborted);
             }
-			if (dto.Workflow != null)
+            if (dto.Workflow != null)
             {
                 if (knowledge.Status == EKnowledgeStatus.Drafts)
                 {
@@ -287,8 +320,30 @@ namespace Hotline.Api.Controllers
                     await StartFlow(knowledge.Id, WorkflowModuleConsts.KnowledgeUpdate, EKnowledgeApplyType.Update, startDto);
                 }
             }
-            
-		}
+
+        }
+
+
+        /// <summary>
+        /// 批量审核
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("batch_audit")]
+        public async Task KnowledgeBatchAuditAsync()
+        { 
+            // TODO: qcy 批量审核 
+        }
+
+        /// <summary>
+        /// 根据知识标题自动分词并保存到关键词表中
+        /// </summary>
+        /// <param name="title"></param>
+        /// <returns></returns>
+        [HttpGet("participle")]
+        public async Task<IList<KnowledgeWordOutDto>> KnowledgeKeyWord([FromQuery]string title)
+        { 
+            return await _knowApplication.TitleParticiple(title);
+        }
 
         /// <summary>
         /// 删除知识
@@ -344,11 +399,20 @@ namespace Hotline.Api.Controllers
         [HttpGet("search_num/list")]
         public async Task<PagedDto<KnowledgeDto>> SearchNumList([FromQuery] KnowledgeCollectListDto dto)
         {
-            var (total, items) = await _knowledgeRepository.Queryable(false, false, false)
+
+            var query = _knowledgeRepository.Queryable(false, true, false)
                 .Where(x => x.Status == EKnowledgeStatus.OnShelf)
+                .Where(m => m.IsDeleted == false)
                 .Where(x => (x.ExpiredTime != null && x.ExpiredTime >= DateTime.Now) || x.ExpiredTime == null)
-                .OrderByDescending(x => x.SearchNum)
-                .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+                .OrderByDescending(x => x.SearchNum);
+
+            if (_sessionContext.OrgIsCenter == false)
+            {
+                query = query.Where(m => m.KnowledgeType.Any(a => a.IsDeleted == false && a.KnowledgeType.KnowledgeTypeOrgs
+                .Any(k => k.IsDeleted == false && k.OrgId.StartsWith(_sessionContext.OrgId))));
+                query = query.Where(m => m.Attribution == "部门知识库");
+            }
+            var (total, items) = await query.ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
             return new PagedDto<KnowledgeDto>(total, _mapper.Map<IReadOnlyList<KnowledgeDto>>(items));
         }
 
@@ -362,7 +426,7 @@ namespace Hotline.Api.Controllers
         {
             //var know = await _knowledgeRepository.GetAsync(Id, HttpContext.RequestAborted);
             var know = await _knowledgeDomainService.KnowledgeInfo(Id, HttpContext.RequestAborted);
-			if (know is null)
+            if (know is null)
                 throw UserFriendlyException.SameMessage("知识查询失败!");
 
             var knowledgeInfoDto = _mapper.Map<KnowledgeInfoDto>(know);
@@ -397,7 +461,7 @@ namespace Hotline.Api.Controllers
             var knowledge = await _knowledgeDomainService.KnowledgeInfo(Id, HttpContext.RequestAborted);
             if (knowledge is null)
                 throw UserFriendlyException.SameMessage("知识查询失败!");
-            if (knowledge.Workflow != null) 
+            if (knowledge.Workflow != null)
                 knowledge.IsCanHandle = knowledge.Workflow.IsCanHandle(_sessionContext.RequiredUserId, _sessionContext.RequiredOrgId, _sessionContext.Roles);
             //转化
             var knowledgeShowInfoDto = _mapper.Map<KnowledgeInfoDto>(knowledge);
@@ -435,6 +499,26 @@ namespace Hotline.Api.Controllers
             return knowledgeShowInfoDto;
         }
 
+        /// <summary>
+        /// 知识详情--导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("info/export")]
+        public async Task<IActionResult> KnowledgeInfoExport([FromBody] KnowledgeInfoExportInDto dto)
+        {
+            if (dto.Ids.Length > 1)
+            {
+                var streams = await _knowApplication.KnowledgeInfoListExportAsync(dto, HttpContext.RequestAborted);
+                byte[] fileBytes = _wordHelperService.ConvertZipStream(streams);
+                var name = DateTime.Now.ToString("yyyyMMddHHmmss");
+                return File(fileBytes, "application/octet-stream", $"{name}.zip");
+            }
+
+            var info = await _knowledgeRepository.GetAsync(dto.Ids[0]) ?? throw UserFriendlyException.SameMessage("知识不存在");
+            return info.Content.HtmlToStream(dto.FileType).GetFileStreamResult(dto.FileType, "知识详情");
+        }
+
         /// <summary>
         /// 知识申请-关联知识-获取知识列表
         /// </summary>
@@ -496,15 +580,20 @@ namespace Hotline.Api.Controllers
         /// </summary>
         /// <returns></returns>
         [HttpGet("knowledge-status-data")]
-        public async Task<object> KnowledgeStatus()
+        public async Task<Dictionary<string, dynamic>> KnowledgeStatus()
         {
-            return new List<KeyValuePair<int, string>>
+            var tabNames = new List<KeyValuePair<int, string>>
             {
-                new KeyValuePair<int, string>(1, "审核中"),
                 new KeyValuePair<int, string>(3, "已上架"),
                 new KeyValuePair<int, string>(4, "已下架"),
+                new KeyValuePair<int, string>(1, "审核中"),
                 new KeyValuePair<int, string>(5, "审核不通过")
             };
+
+            return _baseDataApplication
+                .FileType(EFileType.excel | EFileType.pdf)
+                .Add("tabNames", tabNames)
+                .Build();
         }
 
         /// <summary>
@@ -515,68 +604,43 @@ namespace Hotline.Api.Controllers
         [HttpGet]
         public async Task<PagedDto<KnowledgeDataDto>> GetKnowList([FromQuery] KnowPagedListDto pagedDto)
         {
-            var typeSpliceName = string.Empty;
-            var hotspotHotSpotFullName = string.Empty;
-			if (!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId))
-            {
-                var type = await _knowledgeTypeRepository.GetAsync(x => x.Id == pagedDto.KnowledgeTypeId);
-                typeSpliceName = type?.SpliceName;
-            }
-            if (!string.IsNullOrEmpty(pagedDto.HotspotId))
-            {
-                var hotspot = await _hotspotTypeRepository.GetAsync(x => x.Id == pagedDto.HotspotId);
-                hotspotHotSpotFullName = hotspot?.HotSpotFullName;
-            }
-            //var aa = _knowledgeRepository.Queryable().OrderByDescending(d => d.CreationTime).ToSql();
-            var (total, temp) = await _knowledgeRepository.Queryable(false, false, false)
-                .Includes(x => x.User)
-                .Includes(x => x.SystemOrganize)
-                .Includes(x => x.SourceOrganize)
-                .Includes(x => x.HotspotType)
-                .Includes(x => x.Workflow)
-                .Includes(x=>x.KnowledgeType)
-                //.Includes(x=>x.KnowledgeRelationTypes,t=> t.)
-                .Where(x => x.IsDeleted == false)
-                .Where(x=>x.KnowledgeType.Any(t=>t.KnowledgeType.KnowledgeTypeOrgs.Any(to=>to.OrgId == _sessionContext.RequiredOrgId) || t.KnowledgeType.KnowledgeTypeOrgs.Any() == false))
-                .Where(x => (x.Status == EKnowledgeStatus.Drafts && x.CreatorId == _sessionContext.UserId) || (x.Status != EKnowledgeStatus.Drafts))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.Title), x => x.Title.Contains(pagedDto.Title!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), x => x.Title.Contains(pagedDto.Keyword!) || x.CreatorName.Contains(pagedDto.Keyword!) || x.CreatorOrgName.Contains(pagedDto.Keyword!) || x.SourceOrganize.Name.Contains(pagedDto.Keyword!))
-                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status != EKnowledgeStatus.OffShelf, x => x.Status == pagedDto.Status && ((x.ExpiredTime != null && x.ExpiredTime > DateTime.Now) || x.ExpiredTime == null))
-                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == EKnowledgeStatus.OffShelf, x => x.Status == pagedDto.Status || (x.ExpiredTime != null && x.ExpiredTime < DateTime.Now && x.Status != EKnowledgeStatus.Drafts))
-                .WhereIF(pagedDto.IsPublic.HasValue, x => x.IsPublic == pagedDto.IsPublic)
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.Summary), x => x.Summary != null && x.Summary.Contains(pagedDto.Summary!))
-				//.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.KnowledgeType, typeSpliceName))
-				.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t=>t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
-				.WhereIF(!string.IsNullOrEmpty(hotspotHotSpotFullName), x => x.HotspotType.HotSpotFullName.EndsWith(hotspotHotSpotFullName!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.CreateOrgId), x => x.SourceOrganizeId != null && x.SourceOrganizeId.EndsWith(pagedDto.CreateOrgId!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.ModuleCode), x => x.Workflow.ModuleCode == pagedDto.ModuleCode)
-                .OrderByDescending(d => d.CreationTime)
-                .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, HttpContext.RequestAborted);
-            //temp.ForEach(x => x.IsCanHandle = x.Workflow.CanHandle(_sessionContext.RequiredUserId, _sessionContext.RequiredOrgId));
-            //返回数据
-            return new PagedDto<KnowledgeDataDto>(total, _mapper.Map<IReadOnlyList<KnowledgeDataDto>>(temp));
+            return (await _knowApplication.GetKnowList(pagedDto, HttpContext.RequestAborted))
+                .ToPaged();
+        }
+
+        /// <summary>
+        /// 知识查询-导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("export")]
+        public async Task<IActionResult> GetKnowListExportAsync([FromBody] ExportExcelDto<KnowPagedListDto> dto)
+        {
+            var items = (await _knowApplication.GetKnowList(dto.QueryDto, HttpContext.RequestAborted)).Item2;
+            return _exportApplication.GetExcelFile(dto, items, "知识明细导出");
         }
 
         /// <summary>
         /// 知识检索
         /// </summary>
-        /// <param name="pagedDto"></param>
+        /// <param name="dto"></param>
         /// <returns></returns>
         [HttpGet("knowretrieval")]
-        public async Task<PagedDto<KnowledgeRetrievalDataDto>> KnowRetrieval([FromQuery] KnowledgeRetrievalPagedListDto pagedDto)
+        public async Task<PagedDto<KnowledgeRetrievalDataDto>> KnowRetrieval([FromQuery] KnowledgeRetrievalPagedListDto dto)
         {
             var typeSpliceName = string.Empty;
             var hotspotHotSpotFullName = string.Empty;
-            if (!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId))
+            if (!string.IsNullOrEmpty(dto.KnowledgeTypeId))
             {
-                var type = await _knowledgeTypeRepository.GetAsync(x => x.Id == pagedDto.KnowledgeTypeId);
+                var type = await _knowledgeTypeRepository.GetAsync(x => x.Id == dto.KnowledgeTypeId);
                 typeSpliceName = type?.SpliceName;
             }
-            if (!string.IsNullOrEmpty(pagedDto.HotspotId))
+            if (!string.IsNullOrEmpty(dto.HotspotId))
             {
-                var hotspot = await _hotspotTypeRepository.GetAsync(x => x.Id == pagedDto.HotspotId);
+                var hotspot = await _hotspotTypeRepository.GetAsync(x => x.Id == dto.HotspotId);
                 hotspotHotSpotFullName = hotspot?.HotSpotFullName;
             }
+
             var sugar = _knowledgeRepository
                 .Queryable(false, false, false)
                 .Includes(x => x.User)
@@ -585,20 +649,44 @@ namespace Hotline.Api.Controllers
                 .Where(x => x.IsDeleted == false)
                 .Where(x => x.Status == EKnowledgeStatus.OnShelf)
                 .Where(x => x.KnowledgeType.Any(t => t.KnowledgeType.KnowledgeTypeOrgs.Any(to => to.OrgId == _sessionContext.RequiredOrgId) || t.KnowledgeType.KnowledgeTypeOrgs.Any() == false))
-				.WhereIF(pagedDto.RetrievalType == EKnowledgeRetrievalType.All && !string.IsNullOrEmpty(pagedDto.Keyword), d => d.Title.Contains(pagedDto.Keyword!) || d.Content.Contains(pagedDto.Keyword!))// || d.Additions.Contains(pagedDto.Keyword)
-                .WhereIF(pagedDto.RetrievalType == EKnowledgeRetrievalType.Title && !string.IsNullOrEmpty(pagedDto.Keyword), d => d.Title.Contains(pagedDto.Keyword!))
-                .WhereIF(pagedDto.RetrievalType == EKnowledgeRetrievalType.Content && !string.IsNullOrEmpty(pagedDto.Keyword), d => d.Content.Contains(pagedDto.Keyword!))
-                .WhereIF(pagedDto.RetrievalType == EKnowledgeRetrievalType.Summary && !string.IsNullOrEmpty(pagedDto.Keyword), d => d.Summary != null && d.Summary.Contains(pagedDto.Keyword!))
-				//.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.KnowledgeType, typeSpliceName))
-				.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t=>t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
-				.WhereIF(!string.IsNullOrEmpty(hotspotHotSpotFullName), x => x.HotspotType.HotSpotFullName.EndsWith(hotspotHotSpotFullName!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.HotspotName), x => x.HotspotType.HotSpotFullName.EndsWith(pagedDto.HotspotName!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.CreateOrgId), x => x.CreatorOrgId != null && x.CreatorOrgId.EndsWith(pagedDto.CreateOrgId!))
-                .WhereIF(!string.IsNullOrEmpty(pagedDto.Attribution), x => x.Attribution == pagedDto.Attribution!);
-            switch (pagedDto.Sort)
+                // .WhereIF(dto.RetrievalType == EKnowledgeRetrievalType.All && !string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!) || d.Content.Contains(dto.Keyword!))
+                //.WhereIF(dto.RetrievalType == EKnowledgeRetrievalType.Title && !string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!))
+                //.WhereIF(dto.RetrievalType == EKnowledgeRetrievalType.Content && !string.IsNullOrEmpty(dto.Keyword), d => d.Content.Contains(dto.Keyword!))
+                //.WhereIF(dto.RetrievalType == EKnowledgeRetrievalType.Summary && !string.IsNullOrEmpty(dto.Keyword), d => d.Summary != null && d.Summary.Contains(dto.Keyword!))
+                //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.KnowledgeType, typeSpliceName))
+                .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
+                .WhereIF(!string.IsNullOrEmpty(hotspotHotSpotFullName), x => x.HotspotType.HotSpotFullName.EndsWith(hotspotHotSpotFullName!))
+                .WhereIF(!string.IsNullOrEmpty(dto.HotspotName), x => x.HotspotType.HotSpotFullName.EndsWith(dto.HotspotName!))
+                .WhereIF(!string.IsNullOrEmpty(dto.CreateOrgId), x => x.CreatorOrgId != null && x.CreatorOrgId.EndsWith(dto.CreateOrgId!))
+                .WhereIF(!string.IsNullOrEmpty(dto.Attribution), x => x.Attribution == dto.Attribution!);
+            if (dto.Keyword.NotNullOrEmpty())
+            {
+                var keywords = dto.Keyword!.SplitKeywords();
+                var exp = Expressionable.Create<Knowledge>();
+                foreach (var keyword in keywords)
+                {
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.All)
+                        exp.Or(m => m.Title.Contains(keyword) || m.Content.Contains(keyword));
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.Title)
+                        exp.Or(m => m.Title.Contains(keyword));
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.Content)
+                        exp.Or(m => m.Content.Contains(keyword));
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.Summary)
+                        exp.Or(m => m.Summary != null && m.Summary.Contains(keyword));
+                    if (dto.RetrievalType == EKnowledgeRetrievalType.KeyWord)
+                    {
+                        var keywordEntity = await _knowledgeWordRepository.GetAsync(m => m.Tag == keyword && m.IsEnable == 0);
+                        if (keywordEntity is null) continue;
+                        exp.Or(m => SqlFunc.JsonArrayAny(m.Keywords, keywordEntity.Id));
+                    }
+                }
+                sugar.Where(exp.ToExpression());
+            }
+
+            switch (dto.Sort)
             {
                 case "2":
-                    sugar = sugar.OrderByDescending(p => p.Score);
+                    sugar = sugar.OrderByDescending(p => p.CollectCount);
                     break;
                 case "3":
                     sugar = sugar.OrderByDescending(p => p.CreationTime);
@@ -607,10 +695,22 @@ namespace Hotline.Api.Controllers
                     sugar = sugar.OrderByDescending(p => p.PageView);
                     break;
             }
-            var (total, temp) = await sugar.ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize);
+            var (total, temp) = await sugar.ToPagedListAsync(dto.PageIndex, dto.PageSize);
             return new PagedDto<KnowledgeRetrievalDataDto>(total, _mapper.Map<IReadOnlyList<KnowledgeRetrievalDataDto>>(temp));
         }
 
+        /// <summary>
+        /// 知识检索页面基础数据
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("knowretrieval/base_data")]
+        public async Task<Dictionary<string, dynamic>> GetKnowretrievalBaseData()
+        {
+            return _baseDataApplication
+                .KnowledgeRetrievalType(new[] { 3, 4})
+                .Build();
+        }
+
         /// <summary>
         /// 获取知识审批信息
         /// </summary>
@@ -626,7 +726,7 @@ namespace Hotline.Api.Controllers
                 .Includes(x => x.Workflow)
                 .Where(x => x.KnowledgeId == pagedDto.id)
                 .Where(x => x.IsDeleted == false)
-                .OrderBy(x=>x.CreationTime)
+                .OrderBy(x => x.CreationTime)
                 .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize);
             return new PagedDto<KnowledgeWorkFlowDto>(total, _mapper.Map<IReadOnlyList<KnowledgeWorkFlowDto>>(temp));
         }
@@ -643,7 +743,7 @@ namespace Hotline.Api.Controllers
                 .WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.Title.Equals(dto.Title))
                 .WhereIF(!string.IsNullOrEmpty(dto.Summary), x => x.Summary.Equals(dto.Summary))
                 .WhereIF(!string.IsNullOrEmpty(dto.Content), x => x.Content.Equals(dto.Content))
-                .WhereIF(!string.IsNullOrEmpty(dto.Id),x=> x.Id !=  dto.Id)
+                .WhereIF(!string.IsNullOrEmpty(dto.Id), x => x.Id != dto.Id)
                 .AnyAsync();
             return any;
         }
@@ -718,7 +818,7 @@ namespace Hotline.Api.Controllers
                 .Includes(it => it.Knowledge)
                 .Includes(it => it.User)
                 .Includes(it => it.SystemOrganize)
-                .Includes(it => it.Workflow, d=>d.Steps)
+                .Includes(it => it.Workflow, d => d.Steps)
                 .Where(it => it.WorkflowId != null)
                 .WhereIF(pagedDto.EKnowledgeApplyType.HasValue, d => d.WorkflowModuleStatus == pagedDto.EKnowledgeApplyType)
                 .WhereIF(pagedDto.EKnowledgeWorkFlowStatus.HasValue, d => d.WorkFlowApplyStatus == pagedDto.EKnowledgeWorkFlowStatus)
@@ -1032,9 +1132,9 @@ namespace Hotline.Api.Controllers
                 .Includes(x => x.Knowledge)
                 //.WhereIF(!string.IsNullOrEmpty(dto.KnowledgeTypeId), x => x.Knowledge.KnowledgeTypeId == dto.KnowledgeTypeId!)
                 .WhereIF(!string.IsNullOrEmpty(dto.CreatorName), x => x.CreatorName == dto.CreatorName!)
-				 //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.Knowledge.KnowledgeType, typeSpliceName))
-				 .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.Knowledge.KnowledgeType.Any(t=>t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
-				.Where(x => !string.IsNullOrEmpty(x.Knowledge.Id))
+                 //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.Knowledge.KnowledgeType, typeSpliceName))
+                 .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.Knowledge.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
+                .Where(x => !string.IsNullOrEmpty(x.Knowledge.Id))
                 .OrderByDescending(x => x.CreationTime)
                 .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
             return new PagedDto<KnowledgeCorrectionDto>(total, _mapper.Map<IReadOnlyList<KnowledgeCorrectionDto>>(items));
@@ -1135,10 +1235,10 @@ namespace Hotline.Api.Controllers
             }
             var (total, items) = await _knowledgeQuestionsRepository.Queryable()
                 .Includes(x => x.Knowledge)
-			   //.WhereIF(!string.IsNullOrEmpty(dto.KnowledgeTypeId), x => x.Knowledge.KnowledgeTypeId == dto.KnowledgeTypeId!)
-			   //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.Knowledge.KnowledgeType, typeSpliceName))
-			   .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.Knowledge.KnowledgeType.Any(t=>t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
-				.WhereIF(!string.IsNullOrEmpty(dto.CreatorName), x => x.CreatorName == dto.CreatorName!)
+               //.WhereIF(!string.IsNullOrEmpty(dto.KnowledgeTypeId), x => x.Knowledge.KnowledgeTypeId == dto.KnowledgeTypeId!)
+               //.WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => SqlFunc.JsonLike(x.Knowledge.KnowledgeType, typeSpliceName))
+               .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.Knowledge.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
+                .WhereIF(!string.IsNullOrEmpty(dto.CreatorName), x => x.CreatorName == dto.CreatorName!)
                 .Where(x => !string.IsNullOrEmpty(x.Knowledge.Id))
                 .OrderByDescending(x => x.CreationTime)
                 .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
@@ -1174,6 +1274,7 @@ namespace Hotline.Api.Controllers
             var (total, items) = await _knowledgeCollectRepository.Queryable()
                 .Includes(x => x.Knowledge)
                 .WhereIF(!string.IsNullOrEmpty(dto.Keyword), x => x.Knowledge.Title.Contains(dto.Keyword!) || (x.Knowledge.Summary != null && x.Knowledge.Summary.Contains(dto.Keyword!)))
+                .WhereIF(dto.KnowledgeCollectGroupId.NotNullOrEmpty(), x => x.KnowledgeCollectGroupId == dto.KnowledgeCollectGroupId)
                 .Where(x => x.CreatorId == _sessionContext.UserId)
                 .Where(x => !string.IsNullOrEmpty(x.Knowledge.Id))
                 .Where(x => x.Collect!.Value)
@@ -1203,6 +1304,13 @@ namespace Hotline.Api.Controllers
                 var collectNew = _mapper.Map<KnowledgeCollect>(dto);
                 await _knowledgeCollectRepository.AddAsync(collectNew, HttpContext.RequestAborted);
             }
+            var count = await _knowledgeCollectRepository.Queryable()
+                .Where(m => m.KnowledgeId == dto.KnowledgeId && m.Collect == true).CountAsync();
+
+            await _knowledgeRepository.Updateable()
+                .Where(m => m.Id == dto.KnowledgeId)
+                .SetColumns(m => m.CollectCount == count)
+                .ExecuteCommandAsync();
         }
 
         /// <summary>
@@ -1313,5 +1421,152 @@ namespace Hotline.Api.Controllers
             return new List<KnowledgeCommentDto>(_mapper.Map<IReadOnlyList<KnowledgeCommentDto>>(comments));
         }
         #endregion
+
+        #region 收藏分组
+
+        /// <summary>
+        /// 增加收藏分组
+        /// </summary>
+        [HttpPost("group")]
+        public async Task<KnowledgeCollectGroupOutDto> SaveKnowledgeCoolectGroupAsync([FromBody] KnowledgeCollectGroupInDto dto)
+        {
+            var entity = dto.Adapt<KnowledgeCollectGroup>();
+            if (await _knowledgeCollectGroupRepository
+                .Queryable()
+                .Where(m => m.Name == dto.Name && m.CreatorId == _sessionContext.UserId)
+                .AnyAsync() == true) throw UserFriendlyException.SameMessage("分组名重复, 请重新输入!");
+
+            var key = await _knowledgeCollectGroupRepository.AddAsync(entity);
+            if (key.IsNullOrEmpty()) throw UserFriendlyException.SameMessage("添加失败, 请重试");
+            var outDto = dto.Adapt<KnowledgeCollectGroupOutDto>();
+            outDto.Id = key;
+            return outDto;
+        }
+
+        /// <summary>
+        /// 删除收藏分组
+        /// </summary>
+        [HttpDelete("group")]
+        public async Task DeleteKnowledgeCoolectGroupAsync([FromQuery] string id)
+        {
+            var entity = await _knowledgeCollectGroupRepository.Queryable()
+                .Where(m => m.Id == id && m.CreatorId == _sessionContext.UserId)
+                .FirstAsync() ?? throw new UserFriendlyException("数据不存在");
+
+            entity.IsDeleted = true;
+            await _knowledgeCollectGroupRepository.UpdateAsync(entity);
+            await _knowledgeCollectRepository.Updateable()
+                .Where(m => m.KnowledgeCollectGroupId == id && m.CreatorId == _sessionContext.UserId)
+                .SetColumns(m => m.IsDeleted == true)
+                .ExecuteCommandAsync();
+        }
+
+        /// <summary>
+        /// 获取分组收藏列表
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("group")]
+        public async Task<PagedDto<KnowledgeCollectGroupOutDto>> GetKnowledgeCollectGroupListAsync([FromQuery] KnowledgeCollectGroupListInDto dto)
+        {
+            return (await _knowledgeCollectGroupRepository.Queryable()
+                .WhereIF(dto.Keyword.NotNullOrEmpty(), m => m.Name.Contains(dto.Keyword))
+                .Where(m => m.CreatorId == _sessionContext.UserId)
+                .Select<KnowledgeCollectGroupOutDto>()
+                .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted)
+                ).ToPaged();
+        }
+        #endregion
+
+        #region PageView 浏览记录
+        /// <summary>
+        /// 浏览记录集合
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("pageview")]
+        public async Task<PagedDto<PageViewOutDto>> GetPageViewListAsync([FromQuery] PageViewInDto dto)
+        {
+            return (await _knowApplication.GetPageViewListAsync(dto, HttpContext.RequestAborted))
+                .ToPaged();
+
+        }
+
+        /// <summary>
+        /// 浏览记录集合-导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("pageview/export")]
+        public async Task<FileStreamResult> GetPageViewListAsync([FromBody] ExportExcelDto<PageViewInDto> dto)
+        {
+            var items = (await _knowApplication.GetPageViewListAsync(dto.QueryDto, HttpContext.RequestAborted))
+                .Item2;
+            return _exportApplication.GetExcelFile(dto, items, "浏览记录");
+        }
+
+        #endregion
+
+        #region 热词
+
+        /// <summary>
+        /// 新增热词
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("hotword")]
+        public async Task AddKnowledgeHotWordAsync([FromBody] AddKnowledgeHotWordInDto dto)
+        {
+            await _knowApplication.AddKnowledgeHotWordAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 新增热词页面基础数据
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("hotword/basedata")]
+        public async Task<Dictionary<string, dynamic>> AddKnowledgeHotWordBaseDataAsync()
+        {
+            return _baseDataApplication
+                .KnowledgeHotWordType()
+                .Build();
+        }
+
+        /// <summary>
+        /// 热词集合
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("hotword")]
+        public async Task<PagedDto<KnowledgeHotWordOutDto>> GetKnowledgeHotWordListAsync([FromQuery] KnowledgeHotWordInDto dto)
+        {
+            return (await _knowApplication.GetKnowledgeHotWordListAsync(dto, HttpContext.RequestAborted))
+                .ToPaged();
+        }
+
+        /// <summary>
+        /// 修改热词
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("hotword")]
+        public async Task UpdateKnowledgeHotWordAsync([FromBody] UpdateKnowledgeHotWordInDto dto)
+        {
+            await _knowApplication.UpdateKnowledgeHotWordAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 删除热词
+        /// </summary>
+        /// <param name="id"></param>
+        /// <returns></returns>
+        [HttpDelete("hotword")]
+        public async Task DeleteKnowledgeHotWordAsync([FromQuery] string id)
+        {
+            await _knowledgeHotWordRepository
+                .RemoveAsync(id);
+        }
+
+        #endregion
     }
 }

+ 399 - 0
src/Hotline.Api/Controllers/OldHotlineController.cs

@@ -0,0 +1,399 @@
+using Hotline.Api.Filter;
+using Hotline.Application.Identity;
+using Hotline.Application.Systems;
+using Hotline.Application.Tools;
+using Hotline.Caching.Interfaces;
+using Hotline.Caching.Services;
+using Hotline.FlowEngine.Workflows;
+using Hotline.Identity.Accounts;
+using Hotline.Orders;
+using Hotline.Share.Dtos.Identity;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.FlowEngine;
+using Hotline.Share.Enums.Order;
+using Hotline.Users;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using NPOI.POIFS.Macros;
+using SqlSugar;
+using XF.Domain.Authentications;
+using XF.Domain.Entities;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+
+namespace Hotline.Api.Controllers
+{
+    public class OldHotlineController: BaseController
+    {
+        private readonly IIdentityAppService _identityAppService;
+        private readonly ISystemLogApplication _iSystemLogApplication;
+        private readonly IOrderRepository _orderRepository;
+        private readonly IOrderVisitRepository _orderVisitRepository;
+        private readonly IOrderDelayRepository _orderDelayRepository;
+        private readonly IOrderScreenRepository _orderScreenRepository;
+        private readonly IRepository<OrderSendBackAudit> _orderSendBackAuditRepository;
+        private readonly IRepository<OrderVisitDetail> _orderVisitedDetailRepository;
+        private readonly ISessionContext _sessionContext;
+        private readonly IRepository<Account> _accountRepository;
+
+        public OldHotlineController(IIdentityAppService identityAppService, ISystemLogApplication iSystemLogApplication, IOrderRepository orderRepository, IOrderVisitRepository orderVisitRepository, IOrderDelayRepository orderDelayRepository, IOrderScreenRepository orderScreenRepository, IRepository<OrderSendBackAudit> orderSendBackAuditRepository, IRepository<OrderVisitDetail> orderVisitedDetailRepository, ISessionContext sessionContext, IRepository<Account> accountRepository)
+        {
+            _identityAppService = identityAppService;
+            _iSystemLogApplication = iSystemLogApplication;
+            _orderRepository = orderRepository;
+            _orderVisitRepository = orderVisitRepository;
+            _orderDelayRepository = orderDelayRepository;
+            _orderScreenRepository = orderScreenRepository;
+            _orderSendBackAuditRepository = orderSendBackAuditRepository;
+            _orderVisitedDetailRepository = orderVisitedDetailRepository;
+            _sessionContext = sessionContext;
+            _accountRepository = accountRepository;
+        }
+
+        /// <summary>
+        /// 老系统登录新系统
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [AllowAnonymous]
+        [HttpPost("loing-oldtonew")]
+        [LogFilter("",false)]
+        public async Task<string> HotlineLoginOldToNew([FromBody] HotlineLoginOldToNewDto dto)
+        {
+            try
+            {
+                dto.UserName = RSA.RSADecrypt(dto.UserName, RSA_Create.RSA_PRIVATE_KEY, "PEM");
+            }
+            catch 
+            {
+                throw UserFriendlyException.SameMessage("帐号解密失败");
+            }
+            var res = await _identityAppService.OldToNewLoginAsync(dto, HttpContext.RequestAborted);
+            await _iSystemLogApplication.AddLog("账号登录", res, dto, HttpContext, dto.UserName);
+            return res;
+        }
+
+        /// <summary>
+        /// 是否存在可办信息
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [AllowAnonymous]
+        [HttpGet("ishas_canhandler")]
+        public async Task<bool> IsHasCanHandler([FromQuery] HotlineLoginOldToNewDto dto)
+        {
+            try
+            {
+                dto.UserName = RSA.RSADecrypt(dto.UserName, RSA_Create.RSA_PRIVATE_KEY, "PEM");
+            }
+            catch
+            {
+                throw UserFriendlyException.SameMessage("帐号解密失败");
+            }
+
+            var isHas = false;
+
+            var (isAdmin, isCenter, user) = await _identityAppService.IsCheckAdmin(dto.UserName);
+            List<string> Roles = user.Roles.Select(x => x.Name).ToList();
+            if (isCenter)
+            {
+                #region 待办
+                //待办
+                var waitedDataList = await _orderRepository
+                    .Queryable(hasHandled: false, isAdmin: isAdmin)
+                    .Where(d => SqlFunc.Subqueryable<WorkflowStep>()
+                        .Where(step => step.ExternalId == d.Id && step.Status != EWorkflowStepStatus.Handled &&
+                                       ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == user.Id) ||
+                                        (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == user.OrgId) ||
+                                        (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && 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)
+                    .Where(d => d.Status != EOrderStatus.BackToProvince && d.Status < EOrderStatus.Filed)
+                    .Where(d => d.OrderSpecials.Any() == false || d.OrderSpecials.Any(s => s.State > 0))
+                    .OrderByDescending(d => new { d.IsUrgent, d.StartTime })
+                    .Select(d => new HomeOrderDto
+                    {
+                        No = d.No,
+                        OrderId = d.Id,
+                        Title = d.Title,
+                        AcceptType = d.AcceptType,
+                        HotspotName = d.HotspotName,
+                        Type = "Waited",
+                        Time = d.ExpiredTime,
+                        Status = d.Status,
+                        ActualHandleTime = d.ActualHandleTime,
+                        CounterSignType = d.CounterSignType
+                    })
+                    .ToListAsync();
+                isHas = waitedDataList.Any();
+
+                //待办是否有数据
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 已超期
+                // 已超期
+                isHas = waitedDataList.Any(x => (x.Time < DateTime.Now && x.Status < EOrderStatus.Filed) ||
+                                                                      (x.Time < x.ActualHandleTime && x.Status >= EOrderStatus.Filed));
+                //超期是否有待办
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 回访待办
+                //回访待办
+                isHas = await _orderVisitRepository.Queryable()
+                    .AnyAsync(d => (d.VisitState == EVisitState.WaitForVisit || d.VisitState == EVisitState.NoSatisfiedWaitForVisit) && d.EmployeeId == user.Id);
+
+                //回访待办是否有数据
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 会签待办
+                //会签待办
+                isHas = waitedDataList.Any(x => x.CounterSignType == ECounterSignType.Center || x.CounterSignType == ECounterSignType.Department);
+
+                //会签待办是否有数据
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 延期待审批
+                //延期待审批
+                isHas = await _orderDelayRepository.Queryable(canView: true, isAdmin: isAdmin)
+                    .AnyAsync(d => d.DelayState == EDelayState.Examining);
+
+                //延期待审批是否有数据
+                if (isHas)
+                {
+                    return isHas;
+                }
+
+                #endregion
+
+                #region 部门即将超期
+                //部门即将超期
+                isHas = await _orderRepository.Queryable(canView: !isCenter)
+                .AnyAsync(d => SqlFunc.Subqueryable<WorkflowStep>()
+                        .Where(step => step.ExternalId == d.Id && step.Status != EWorkflowStepStatus.Handled &&
+                                       ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == user.Id) ||
+                                        (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == user.OrgId) ||
+                                        (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && Roles.Contains(step.RoleId))))
+                        .Any()
+                    && d.Status < EOrderStatus.Filed && DateTime.Now > d.NearlyExpiredTime && DateTime.Now < d.ExpiredTime);
+
+                //部门即将超期是否有数据
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 甄别待审批
+                //甄别待审批
+                isHas = await _orderScreenRepository.Queryable(hasHandled: !true, isAdmin: isAdmin)
+                    .Includes(d => d.Order)
+                    .Includes(d => d.ScreenDetails.Where(sd => sd.AuditUserId == user.Id).OrderByDescending(sd => sd.AuditTime).Take(1).ToList())
+                    .AnyAsync(d => (d.Status == EScreenStatus.Apply || d.Status == EScreenStatus.Approval || (d.Status == EScreenStatus.SendBack && d.SendBackApply == false)));
+
+                //甄别待审批是否有数据
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 退回待审批
+                //退回待审批
+                isHas = await _orderSendBackAuditRepository.Queryable()
+                    .Where(d => d.State == ESendBackAuditState.Apply)
+                    .WhereIF(Roles.Contains("role_sysadmin") == false, x => x.SendBackOrgId == user.Id) // 123 系统管理员;
+                    .AnyAsync();
+
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+            }
+            else
+            {
+                #region 待办
+                //待办
+                var waitedDataList = await _orderRepository
+                    .Queryable(hasHandled: false, isAdmin: isAdmin)
+                    .Where(d => SqlFunc.Subqueryable<WorkflowStep>()
+                        .Where(step => step.ExternalId == d.Id && step.Status != EWorkflowStepStatus.Handled &&
+                                       ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == user.Id) ||
+                                        (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == user.OrgId) ||
+                                        (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && 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)
+                    .Where(d => d.Status != EOrderStatus.BackToProvince && d.Status < EOrderStatus.Filed)
+                    .Where(d => d.OrderSpecials.Any() == false || d.OrderSpecials.Any(s => s.State > 0))
+                    .OrderByDescending(d => new { d.IsUrgent, d.StartTime })
+                    .Select(d => new HomeOrderDto
+                    {
+                        No = d.No,
+                        OrderId = d.Id,
+                        Title = d.Title,
+                        AcceptType = d.AcceptType,
+                        HotspotName = d.HotspotName,
+                        Type = "Waited",
+                        Time = d.ExpiredTime,
+                        Status = d.Status,
+                        ActualHandleTime = d.ActualHandleTime,
+                        CounterSignType = d.CounterSignType
+                    })
+                    .ToListAsync();
+                isHas = waitedDataList.Any();
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 已超期
+                //已超期
+                isHas = waitedDataList.Any(x => (x.Time < DateTime.Now && x.Status < EOrderStatus.Filed) ||
+                                                                      (x.Time < x.ActualHandleTime && x.Status >= EOrderStatus.Filed));
+                //超期是否有待办
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 会签待办
+                //会签待办
+                isHas = waitedDataList.Any(x => x.CounterSignType == ECounterSignType.Center || x.CounterSignType == ECounterSignType.Department);
+
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 甄别待审批
+                //甄别待审批
+                isHas = await _orderScreenRepository.Queryable(hasHandled: !true, isAdmin: isAdmin)
+                    .Includes(d => d.Order)
+                    .Includes(d => d.ScreenDetails.Where(sd => sd.AuditUserId == user.Id).OrderByDescending(sd => sd.AuditTime).Take(1).ToList())
+                    .AnyAsync(d => (d.Status == EScreenStatus.Apply || d.Status == EScreenStatus.Approval || (d.Status == EScreenStatus.SendBack && d.SendBackApply == false)));
+
+                if (isHas)
+                {
+                    return isHas;
+                }
+
+                #endregion
+
+                #region 延期待审批
+                //延期待审批
+                isHas = await _orderDelayRepository.Queryable(canView: true, isAdmin: isAdmin)
+                    .AnyAsync(d => d.DelayState == EDelayState.Examining);
+
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 甄别待申请
+                //甄别待申请
+                isHas = await _orderVisitedDetailRepository.Queryable(false, true)
+                .Includes(x => x.OrderVisit)
+                .Includes(x => x.OrderVisit, y => y.Order)
+                .Includes(x => x.OrderVisit, y => y.Employee)
+                .Includes(x => x.OrderScreens)
+                .Where(x => x.OrderScreens.Any(s => s.Status == EScreenStatus.SendBack && s.SendBackApply == true) || x.OrderScreens.Any(s => (s.Status != EScreenStatus.SendBack && s.SendBackApply != true)) == false)
+                .Where(x => x.OrderVisit.VisitState == EVisitState.Visited && x.OrderVisit.IsCanHandle)
+                .Where(x => x.VisitTarget == EVisitTarget.Org && x.VisitOrgCode == user.OrgId && (
+                    SqlFunc.JsonField(x.OrgProcessingResults, "Key") == "1" ||
+                    SqlFunc.JsonField(x.OrgProcessingResults, "Key") == "2" ||
+                    SqlFunc.JsonField(x.OrgHandledAttitude, "Key") == "1" ||
+                    SqlFunc.JsonField(x.OrgHandledAttitude, "Key") == "2"
+                ))
+               .AnyAsync();
+
+                if (isHas)
+                {
+                    return isHas;
+                }
+                #endregion
+
+                #region 退回待审批
+                //退回待审批
+                isHas = await _orderSendBackAuditRepository.Queryable()
+                    .Where(d => d.State == ESendBackAuditState.Apply)
+                    .WhereIF(Roles.Contains("role_sysadmin") == false, x => x.SendBackOrgId == user.OrgId) // 123 系统管理员;
+                    .AnyAsync();
+                if (isHas)
+                {
+                    return isHas;
+                }
+
+                #endregion
+
+                #region 部门即将超期
+                //部门即将超期
+                isHas = await _orderRepository.Queryable(canView: !isCenter)
+                .Includes(d => d.OrderDelays)
+                .Where(d => SqlFunc.Subqueryable<WorkflowStep>()
+                        .Where(step => step.ExternalId == d.Id && step.Status != EWorkflowStepStatus.Handled &&
+                        ((step.FlowAssignType == EFlowAssignType.User && !string.IsNullOrEmpty(step.HandlerId) && step.HandlerId == user.Id) ||
+                        (step.FlowAssignType == EFlowAssignType.Org && !string.IsNullOrEmpty(step.HandlerOrgId) && step.HandlerOrgId == user.OrgId) ||
+                                        (step.FlowAssignType == EFlowAssignType.Role && !string.IsNullOrEmpty(step.RoleId) && Roles.Contains(step.RoleId))))
+                        .Any())
+                    .Where(d => d.Status < EOrderStatus.Filed && DateTime.Now > d.NearlyExpiredTime && DateTime.Now < d.ExpiredTime)
+                    .AnyAsync();
+                if (isHas)
+                {
+                    return isHas;
+                }
+
+                #endregion
+            }
+            return isHas;
+        }
+
+
+        /// <summary>
+        /// 获取用户加密后的用户名
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("get_rsa_loginname")]
+        public async Task<string> GetRASLoginName()
+        {
+            var account = await _accountRepository.GetAsync(_sessionContext.RequiredUserId,HttpContext.RequestAborted);
+            if (account == null)
+            {
+                throw UserFriendlyException.SameMessage("获取用户加密失败");
+            }
+            try
+            {
+                string userName = System.Web.HttpUtility.UrlEncode(RSA.RSAEncrypt(account.UserName, RSA_Create.RSA_PUBLIC_KEY, "PEM"));
+                //string userName = RSA.RSAEncrypt(account.UserName, RSA_Create.RSA_PUBLIC_KEY, "PEM");
+                return userName;
+            }
+            catch
+            {
+                throw UserFriendlyException.SameMessage("账号加密失败!");
+            }
+        }
+    }
+}

+ 82 - 55
src/Hotline.Api/Controllers/OrderController.cs

@@ -64,6 +64,7 @@ using XF.Domain.Entities;
 using XF.Domain.Exceptions;
 using XF.Domain.Repository;
 using XF.Utility.EnumExtensions;
+using Newtonsoft.Json;
 
 namespace Hotline.Api.Controllers;
 
@@ -75,7 +76,6 @@ public class OrderController : BaseController
     #region 注入
 
     private readonly IOrderDomainService _orderDomainService;
-    private readonly ITypedCache<string> _typeCache;
     private readonly BaseDataApplication _baseDataApplication;
     private readonly IOrderRepository _orderRepository;
     private readonly IWorkflowApplication _workflowApplication;
@@ -84,6 +84,7 @@ public class OrderController : BaseController
     private readonly ISystemOrganizeRepository _organizeRepository;
     private readonly IDefinitionDomainService _definitionDomainService;
     private readonly ISystemDicDataCacheManager _sysDicDataCacheManager;
+    private readonly ITypedCache<string> _typeCache;
     private readonly ISessionContext _sessionContext;
     private readonly IMapper _mapper;
     private readonly IMediator _mediator;
@@ -137,8 +138,8 @@ public class OrderController : BaseController
     private readonly IOrderAnalysisApplication _orderAnalysisApplication;
     private readonly ICalcExpireTime _expireTime;
     private readonly IRepository<OrderPushType> _orderPushTypeRepository;
-    private readonly IOptions<CityBaseConfiguration> _cityBaseConfiguration;
     private readonly IRepository<OrderRevoke> _orderRevokeRepository;
+    private readonly IOrderTerminateRepository _orderTerminateRepository;
 
     public OrderController(
         IOrderDomainService orderDomainService,
@@ -199,12 +200,12 @@ public class OrderController : BaseController
         Publisher publisher,
         IOrderAnalysisApplication orderAnalysisApplication,
         ICalcExpireTime expireTime,
-        IOptions<CityBaseConfiguration> cityBaseConfiguration,
         IRepository<OrderPushType> orderPushTypeRepository,
         ICallNativeRepository callNativeRepository,
         ICallNativeApplication callNativeApplication,
         IRepository<OrderRevoke> orderRevokeRepository,
         BaseDataApplication baseDataApplication,
+        IOrderTerminateRepository orderTerminateRepository,
         ITypedCache<string> typeCache)
     {
         _orderDomainService = orderDomainService;
@@ -268,10 +269,11 @@ public class OrderController : BaseController
         _orderPushTypeRepository = orderPushTypeRepository;
         _callNativeRepository = callNativeRepository;
         _callNativeApplication = callNativeApplication;
-        _orderRevokeRepository = orderRevokeRepository;
-        _cityBaseConfiguration = cityBaseConfiguration;
         _baseDataApplication = baseDataApplication;
+        _orderTerminateRepository = orderTerminateRepository;
+        _orderRevokeRepository = orderRevokeRepository;
         _typeCache = typeCache;
+        _baseDataApplication = baseDataApplication;
     }
 
     #endregion
@@ -299,11 +301,11 @@ public class OrderController : BaseController
             .WhereIF(!string.IsNullOrEmpty(dto.CenterToOrgHandlerName), d => d.CenterToOrgHandlerName == dto.CenterToOrgHandlerName!) //派单人
             .WhereIF(!string.IsNullOrEmpty(dto.NameOrNo), d => d.AcceptorName == dto.NameOrNo! || d.AcceptorStaffNo == dto.NameOrNo!) //受理人/坐席
             .WhereIF(!string.IsNullOrEmpty(dto.ActualHandleOrgName), d => d.ActualHandleOrgName.Contains(dto.ActualHandleOrgName)) //接办部门(综合查询模糊)
-            .WhereIF(!string.IsNullOrEmpty(dto.AcceptType), d => d.AcceptTypeCode == dto.AcceptType) //受理类型
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptType), d => d.AcceptTypeCode == dto.AcceptType)//受理类型
             .WhereIF(!string.IsNullOrEmpty(dto.Hotspot), d => d.HotspotSpliceName != null && d.HotspotSpliceName.Contains(dto.Hotspot))
             .WhereIF(!string.IsNullOrEmpty(dto.FromPhone), d => d.FromPhone == dto.FromPhone) //来电号码
-            //.WhereIF(!string.IsNullOrEmpty(dto.PubMan),
-            //    d => d.AcceptorName.Contains(dto.PubMan!) || d.AcceptorStaffNo.Contains(dto.PubMan!))
+                                                                                              //.WhereIF(!string.IsNullOrEmpty(dto.PubMan),
+                                                                                              //    d => d.AcceptorName.Contains(dto.PubMan!) || d.AcceptorStaffNo.Contains(dto.PubMan!))
             .WhereIF(dto.PubRange == EPublicState.Pub, d => d.OrderPublish.PublishState)
             .WhereIF(dto.PubRange == EPublicState.NoPub, d => !d.OrderPublish.PublishState)
             .WhereIF(dto.IsProvinceOrder.HasValue && dto.IsProvinceOrder == true, d => d.Source == ESource.ProvinceStraight)
@@ -382,12 +384,23 @@ public class OrderController : BaseController
                     orderVisit.PublishTime = DateTime.Now;
                     orderVisit.IsCanHandle = true;
                     orderVisit.EmployeeId = _sessionContext.RequiredUserId;
+
+                    if (_appOptions.Value.IsZiGong)
+                    {
+                        orderVisit.EmployeeId = string.Empty;
+                    }
                     if (order is { ProcessType: EProcessType.Zhiban, CounterSignType: null } && !order.IsProvince)
                     {
                         orderVisit.VisitState = EVisitState.Visited;
                         orderVisit.VisitTime = DateTime.Now;
                         orderVisit.VisitType = EVisitType.OtherVisit;
                         orderVisit.NowEvaluate = new Kv() { Key = "4", Value = "满意" };
+                        if (_appOptions.Value.IsZiGong)
+                        {
+                            // 根据禅道 自贡需求 Id_361, 第一条, 3小条需求;
+                            // 直办件归档后自动回访量需统计在“胡玲”的默认回访量中;
+                            orderVisit.EmployeeId = _systemSettingCacheManager.DefaultVisitEmployeeId;
+                        }
                     }
 
                     if (order.CounterSignType != ECounterSignType.Center)
@@ -395,10 +408,6 @@ public class OrderController : BaseController
                         orderVisit.IsCanAiVisit = true;
                     }
 
-                    if (_appOptions.Value.IsZiGong)
-                    {
-                        orderVisit.EmployeeId = string.Empty;
-                    }
 
                     string visitId = await _orderVisitRepository.AddAsync(orderVisit);
 
@@ -533,6 +542,10 @@ public class OrderController : BaseController
         {
             orderVisit.EmployeeId = _sessionContext.RequiredUserId;
         }
+        if (_appOptions.Value.IsZiGong)
+        {
+            orderVisit.EmployeeId = string.Empty;
+        }
 
         if (order is { ProcessType: EProcessType.Zhiban, CounterSignType: null } && !order.IsProvince)
         {
@@ -540,14 +553,15 @@ public class OrderController : BaseController
             orderVisit.VisitTime = DateTime.Now;
             orderVisit.VisitType = EVisitType.OtherVisit;
             orderVisit.NowEvaluate = new Kv() { Key = "4", Value = "满意" };
-        }
 
-        if (_appOptions.Value.IsZiGong)
-        {
-            orderVisit.EmployeeId = string.Empty;
+            if (_appOptions.Value.IsZiGong)
+            {
+                // 根据禅道 自贡需求 Id_361, 第一条, 3小条需求;
+                // 直办件归档后自动回访量需统计在“胡玲”的默认回访量中;
+                orderVisit.EmployeeId = _systemSettingCacheManager.DefaultVisitEmployeeId;
+            }
         }
 
-
         if (order.CounterSignType != ECounterSignType.Center)
         {
             orderVisit.IsCanAiVisit = true;
@@ -668,18 +682,16 @@ public class OrderController : BaseController
         var (total, items) = await _orderPublishRepository.Queryable()
             .Includes(x => x.Order)
             .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Order.Title.StartsWith(dto.Keyword!))
-            .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.Order.No.Contains(dto.No))
-            .WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Order.Title.Contains(dto.Title))
+            .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.Order.No.Contains(dto.No!))
+            .WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Order.Title.Contains(dto.Title!))
             .WhereIF(dto.CreationTimeStart.HasValue, d => d.Order.CreationTime >= dto.CreationTimeStart)
             .WhereIF(dto.CreationTimeEnd.HasValue, d => d.Order.CreationTime <= dto.CreationTimeEnd)
             .WhereIF(!string.IsNullOrEmpty(dto.FromPhone), d => d.Order.FromPhone == dto.FromPhone) //来电号码
-            .WhereIF(!string.IsNullOrEmpty(dto.ActualHandleOrgName),
-                d => d.Order.ActualHandleOrgName.Contains(dto.ActualHandleOrgName)) //接办部门(综合查询模糊)
-            .WhereIF(!string.IsNullOrEmpty(dto.AcceptType), d => d.Order.AcceptTypeCode == dto.AcceptType) //受理类型
+            .WhereIF(!string.IsNullOrEmpty(dto.ActualHandleOrgName), d => d.Order.ActualHandleOrgName.Contains(dto.ActualHandleOrgName)) //接办部门(综合查询模糊)
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptType), d => d.Order.AcceptTypeCode == dto.AcceptType)//受理类型
             .WhereIF(!string.IsNullOrEmpty(dto.Hotspot), d => d.Order.HotspotSpliceName != null && d.Order.HotspotSpliceName.Contains(dto.Hotspot))
             .WhereIF(!string.IsNullOrEmpty(dto.PublishName), d => d.CreatorName.Contains(dto.PublishName!))
-            .WhereIF(!string.IsNullOrEmpty(dto.NameOrNo),
-                d => d.Order.AcceptorName == dto.NameOrNo! || d.Order.AcceptorStaffNo == dto.NameOrNo!) //受理人/坐席
+            .WhereIF(!string.IsNullOrEmpty(dto.NameOrNo), d => d.Order.AcceptorName == dto.NameOrNo! || d.Order.AcceptorStaffNo == dto.NameOrNo!) //受理人/坐席
             .WhereIF(dto.StartTime.HasValue, d => d.CreationTime >= dto.StartTime)
             .WhereIF(dto.EndTime.HasValue, d => d.CreationTime <= dto.EndTime)
             .WhereIF(dto.Resolve.HasValue, x => x.Resolve == dto.Resolve)
@@ -905,6 +917,7 @@ public class OrderController : BaseController
             .VisitSatisfaction()
             .VisitManner()
             .VisitStateQuery()
+            .SourceChannel()
             .Build();
 
     /// <summary>
@@ -1113,6 +1126,10 @@ public class OrderController : BaseController
                 }
 
                 await _orderApplication.SaveOrderVisit(visitDto, HttpContext.RequestAborted);
+                await _orderVisitRepository.Updateable()
+                    .Where(m => m.Id == visit.VisitId)
+                    .SetColumns(m => m.IsBatchVisit == true)
+                    .ExecuteCommandAsync();
                 outDto.CompleteCount += 1;
             }
             catch (Exception e)
@@ -1283,7 +1300,6 @@ public class OrderController : BaseController
     /// </summary>
     /// <param name="dto"></param>
     /// <returns></returns>
-    [Permission(EPermission.OrderVisitAgainList)]
     [HttpGet("visitapply/visitagainlist")]
     public async Task<PagedDto<OrderCanVisitAgainDto>> OrderVisitAgainList([FromQuery] OrderVisitAgainListDto dto)
     {
@@ -1308,7 +1324,6 @@ public class OrderController : BaseController
     /// <param name="dto"></param>
     /// <returns></returns>
     /// <exception cref="UserFriendlyException"></exception>
-    [Permission(EPermission.ApplyOrderVisit)]
     [HttpPost("visitapply/add")]
     [LogFilter("新增工单二次回访申请")]
     public async Task ApplyOrderVisit([FromBody] AddVisitApplyDto dto)
@@ -1346,7 +1361,6 @@ public class OrderController : BaseController
     /// </summary>
     /// <param name="dto"></param>
     /// <returns></returns>
-    [Permission(EPermission.ExaminOrderVisit)]
     [HttpPost("visitapply/examin")]
     [LogFilter("新增工单二次回访审核")]
     public async Task ExaminOrderVisit([FromBody] ExaminOrderVisitDto dto)
@@ -1434,7 +1448,6 @@ public class OrderController : BaseController
     /// </summary>
     /// <param name="dto"></param>
     /// <returns></returns>
-    [Permission(EPermission.VisitApplyList)]
     [HttpGet("visitapply/list")]
     public async Task<PagedDto<VisitApplyDto>> VisitApplyList([FromQuery] VisitApplyListDto dto)
     {
@@ -2429,10 +2442,10 @@ public class OrderController : BaseController
                 {
                 }
             }
-
+            var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
             //推省上
-            if (!string.IsNullOrEmpty(model.Id) && (_cityBaseConfiguration.Value.CityProvince.OrgId.Equals(model.OrgId) ||
-                                                    _cityBaseConfiguration.Value.CityProvinceAssign.OrgId.Equals(model.OrgId)))
+            if (!string.IsNullOrEmpty(model.Id) && (cityBase.CityProvince.OrgId.Equals(model.OrgId) || cityBase.CityProvinceAssign.OrgId.Equals(model.OrgId)))
             {
                 var orderDto = _mapper.Map<OrderDto>(order);
                 var supervise = await _orderSuperviseRepository.GetAsync(x => x.Id == model.Id);
@@ -2675,10 +2688,10 @@ public class OrderController : BaseController
                 {
                 }
             }
-
+            var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
             //推省上
-            if (!string.IsNullOrEmpty(model.Id) && (_cityBaseConfiguration.Value.CityProvince.OrgId.Equals(model.OrgId) ||
-                                                    _cityBaseConfiguration.Value.CityProvinceAssign.OrgId.Equals(model.OrgId)))
+            if (!string.IsNullOrEmpty(model.Id) && (cityBase.CityProvince.OrgId.Equals(model.OrgId) || cityBase.CityProvinceAssign.OrgId.Equals(model.OrgId)))
             {
                 var orderDto = _mapper.Map<OrderDto>(order);
                 var urge = await _orderUrgeRepository.GetAsync(x => x.Id == model.Id);
@@ -3148,8 +3161,13 @@ public class OrderController : BaseController
         {
             dto.ProvinceRevokeString = "该工单已由省平台发送撤单!请直接归档办理!";
         }
+        //终止
+        var orderTerminateList = await _orderTerminateRepository.Queryable().Where(x => x.OrderId == order.Id).ToListAsync();
+        dto.OrderTerminateStatus = orderTerminateList.Any(x => x.Status == ETerminateStatus.End) ? "同意" : orderTerminateList.Any(x => x.Status == ETerminateStatus.Refuse) ?
+            "不同意" : orderTerminateList.Any(x => x.Status == ETerminateStatus.Approval || x.Status == ETerminateStatus.SendBack) ? "审批中" : null;
 
-        return _sessionContext.OrgIsCenter ? dto : dto.DataMask();
+
+		return _sessionContext.OrgIsCenter ? dto : dto.DataMask();
     }
 
     /// <summary>
@@ -3468,8 +3486,9 @@ public class OrderController : BaseController
             throw new UserFriendlyException($"该工单已开启办理流程, No:{order.No}", "该工单已开启办理流程");
 
         ExpiredTimeWithConfig expiredTimeConfig;
-        if (dto.Workflow.NextHandlers.Any(d =>
-                d.Key == _cityBaseConfiguration.Value.CityProvince.OrgId || d.Key == _cityBaseConfiguration.Value.CityProvinceAssign.OrgId))
+        var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+        CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+        if (dto.Workflow.NextHandlers.Any(d => d.Key == cityBase.CityProvince.OrgId || d.Key == cityBase.CityProvinceAssign.OrgId))
         {
             var timeResult = await _expireTime.CalcEndTime(DateTime.Now, ETimeType.WorkDay, 45, 80, 50);
             expiredTimeConfig = new ExpiredTimeWithConfig
@@ -3511,7 +3530,7 @@ public class OrderController : BaseController
             var startDto = _mapper.Map<StartWorkflowDto>(dto.Workflow);
             startDto.DefinitionModuleCode = WorkflowModuleConsts.OrderHandle;
             startDto.Title = order.Title;
-            startDto.Transpond = dto.IsForwarded;
+            startDto.IsForwarded = dto.IsForwarded;
             await _workflowApplication.StartWorkflowAsync(startDto, order.Id, order.ExpiredTime, HttpContext.RequestAborted);
         }
         catch (Exception e)
@@ -3618,7 +3637,7 @@ public class OrderController : BaseController
     [HttpPost("steps/temp")]
     public async Task TempSaveAsync([FromBody] StepTempInDto dto)
     {
-        await _typeCache.SetAsync($"tmp_opinion_{dto.OrderId}{_sessionContext.UserId}", dto.Opinion, TimeSpan.FromDays(3), HttpContext.RequestAborted);
+        //await _typeCache.SetAsync($"tmp_opinion_{dto.OrderId}{_sessionContext.UserId}", dto.Opinion, TimeSpan.FromDays(3), HttpContext.RequestAborted);
     }
 
     /// <summary>
@@ -3859,6 +3878,7 @@ public class OrderController : BaseController
             .WhereIF(dto.StartTime.HasValue, d => d.StartTime >= dto.StartTime)
             .WhereIF(dto.EndTime.HasValue, d => d.StartTime <= dto.EndTime)
             .WhereIF(dto.IsUrgent.HasValue, d => d.IsUrgent == dto.IsUrgent!.Value)
+            .WhereIF(dto.Status.HasValue, d => d.Status == dto.Status)
             .OrderByDescending(d => new { d.IsUrgent, d.StartTime })
             .ToPagedListAsync(dto, HttpContext.RequestAborted);
 
@@ -3918,6 +3938,7 @@ public class OrderController : BaseController
             //.Where(d => (string.IsNullOrEmpty(d.WorkflowId) && (string.IsNullOrEmpty(d.SignerId) || d.SignerId == _sessionContext.RequiredUserId)))
             //.Where(d => string.IsNullOrEmpty(d.SignerId) || d.SignerId == _sessionContext.RequiredUserId)
             .WhereIF(dto.IsUrgent.HasValue, d => d.IsUrgent == dto.IsUrgent.Value)
+            .WhereIF(dto.Status.HasValue, d => d.Status == dto.Status)
             .Where(x => x.Source < ESource.MLSQ || x.Source > ESource.WZSC)
             .Where(x => x.Status != EOrderStatus.BackToProvince && x.Status < EOrderStatus.Filed)
             .OrderBy(d => d.Status)
@@ -4750,10 +4771,11 @@ public class OrderController : BaseController
                 }
             }
 
-            if (order != null && (_cityBaseConfiguration.Value.CityProvince.OrgId.Equals(model.OrgId) ||
-                                  _cityBaseConfiguration.Value.CityProvinceAssign.OrgId.Equals(model.OrgId) ||
-                                  _cityBaseConfiguration.Value.CityEnterprise.OrgId.Equals(model.OrgId) ||
-                                  _cityBaseConfiguration.Value.PublicSecurity.OrgId.Equals(model.OrgId)))
+            var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+            if (order != null && (cityBase.CityProvince.OrgId.Equals(model.OrgId) ||
+                                  cityBase.CityProvinceAssign.OrgId.Equals(model.OrgId) || cityBase.CityEnterprise.OrgId.Equals(model.OrgId) ||
+                                  cityBase.PublicSecurity.OrgId.Equals(model.OrgId)))
             {
                 await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderFlowRecalled,
                     new PublishSpecialDto { Order = _mapper.Map<OrderDto>(order), Special = _mapper.Map<OrderSpecialDto>(model) },
@@ -5018,10 +5040,11 @@ public class OrderController : BaseController
                 }
             }
 
-            if (order != null && (_cityBaseConfiguration.Value.CityProvince.OrgId.Equals(special.OrgId) ||
-                                  _cityBaseConfiguration.Value.CityProvinceAssign.OrgId.Equals(special.OrgId) ||
-                                  _cityBaseConfiguration.Value.CityEnterprise.OrgId.Equals(special.OrgId) ||
-                                  _cityBaseConfiguration.Value.PublicSecurity.OrgId.Equals(special.OrgId)))
+            var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+            if (order != null && (cityBase.CityProvince.OrgId.Equals(special.OrgId) ||
+                                  cityBase.CityProvinceAssign.OrgId.Equals(special.OrgId) || cityBase.CityEnterprise.OrgId.Equals(special.OrgId) ||
+                                  cityBase.PublicSecurity.OrgId.Equals(special.OrgId)))
             {
                 await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderFlowRecalled,
                     new PublishSpecialDto { Order = _mapper.Map<OrderDto>(order), Special = _mapper.Map<OrderSpecialDto>(special) },
@@ -5136,11 +5159,11 @@ public class OrderController : BaseController
                         await _orderVisitRepository.UpdateAsync(visit, HttpContext.RequestAborted);
                     }
                 }
-
-                if (order != null && (_cityBaseConfiguration.Value.CityProvince.OrgId.Equals(special.OrgId) ||
-                                      _cityBaseConfiguration.Value.CityProvinceAssign.OrgId.Equals(special.OrgId) ||
-                                      _cityBaseConfiguration.Value.CityEnterprise.OrgId.Equals(special.OrgId) ||
-                                      _cityBaseConfiguration.Value.PublicSecurity.OrgId.Equals(special.OrgId)))
+                var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+                CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+                if (order != null && (cityBase.CityProvince.OrgId.Equals(special.OrgId) ||
+                                      cityBase.CityProvinceAssign.OrgId.Equals(special.OrgId) || cityBase.CityEnterprise.OrgId.Equals(special.OrgId) ||
+                                      cityBase.PublicSecurity.OrgId.Equals(special.OrgId)))
                 {
                     await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderFlowRecalled,
                         new PublishSpecialDto { Order = _mapper.Map<OrderDto>(order), Special = _mapper.Map<OrderSpecialDto>(special) },
@@ -5321,6 +5344,8 @@ public class OrderController : BaseController
         var step = await _workflowApplication.GetRecallStepsAsync(id, HttpContext.RequestAborted);
         var specialSeats = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.SpecialSeats).SettingValue[0]);
         var specialSendOrder = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.SpecialSendOrder).SettingValue[0]);
+        var order = await _orderRepository.Queryable().Where(d => d.WorkflowId == id).FirstAsync(HttpContext.RequestAborted);
+        if (order == null) throw UserFriendlyException.SameMessage("无效工单信息!");
 
         var baseTypeId = string.Empty;
         if (step != null && step.Steps.Any() && _sessionContext.Roles.Contains("zuoxi") && specialSeats &&
@@ -5343,7 +5368,8 @@ public class OrderController : BaseController
             SpecialReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.SpecialReason),
             InstaShotSpecialReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.InstaShotSpecialReason),
             Step = step,
-            BaseTypeId = baseTypeId
+			IsTerminate = await _orderTerminateRepository.Queryable().Where(d=>d.OrderId == order.Id && d.Status == ETerminateStatus.End).AnyAsync(),
+			BaseTypeId = baseTypeId
         };
         return rsp;
     }
@@ -5371,7 +5397,8 @@ public class OrderController : BaseController
                 ? _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.InstaShotSpecialReason)
                 : _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.SpecialReason),
             ReTransactErrorType = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.ReTransactErrorType),
-            Step = step,
+            IsTerminate = await _orderTerminateRepository.Queryable().Where(d => d.OrderId == order.Id && d.Status == ETerminateStatus.End).AnyAsync(),
+			Step = step,
             Orgs = orgs,
         };
         return rsp;
@@ -5818,7 +5845,7 @@ public class OrderController : BaseController
 
     #endregion
 
-    #region 工单观察
+    #region 工单观察、关注
 
     /// <summary>
     /// 新增工单观察

+ 2 - 2
src/Hotline.Api/Controllers/OrderRevocationController.cs

@@ -44,8 +44,8 @@ namespace Hotline.Api.Controllers
             ISystemSettingCacheManager systemSettingCacheManager,
             IRepository<SystemOrganize> systemOrganizeRepository,
             IRepository<User> userRepository,
-            IMediator mediator,
-            IWorkflowDomainService workflowDomainService)
+             IMediator mediator,
+             IWorkflowDomainService workflowDomainService)
         {
             _mapper = mapper;
             _orderRevocationRepository = orderRevocationRepository;

+ 309 - 0
src/Hotline.Api/Controllers/OrderTerminateController.cs

@@ -0,0 +1,309 @@
+using Hotline.Orders;
+using Hotline.Permissions;
+using Hotline.Quality;
+using Hotline.Share.Dtos.Quality;
+using Hotline.Share.Dtos;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Authentications;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+using Hotline.Api.Filter;
+using Hotline.Caching.Services;
+using Hotline.FlowEngine.WorkflowModules;
+using Hotline.Repository.SqlSugar.Orders;
+using Hotline.Settings;
+using Hotline.Share.Dtos.FlowEngine;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.FlowEngine;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Enums.Settings;
+using SqlSugar;
+using XF.Utility.EnumExtensions;
+using MapsterMapper;
+using Hotline.File;
+using Hotline.Application.FlowEngine;
+using Hotline.FlowEngine.Workflows;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Application.Orders;
+using Hotline.Share.Requests;
+using Hotline.Tools;
+
+namespace Hotline.Api.Controllers
+{
+	/// <summary>
+	/// 终止管理
+	/// </summary>
+	public class OrderTerminateController : BaseController
+	{
+		private readonly ISessionContext _sessionContext;
+		private readonly IMapper _mapper;
+		private readonly IFileRepository _fileRepository;
+		private readonly IWorkflowApplication _workflowApplication;
+		private readonly IRepository<Workflow> _workflowRepository;
+		private readonly IWorkflowDomainService _workflowDomainService;
+		private readonly IOrderRepository _orderRepository;
+		private readonly IOrderDomainService _orderDomainService;
+		private readonly IOrderTerminateRepository _orderTerminateRepository;
+		private readonly IOrderApplication _orderApplication;
+
+		public OrderTerminateController(
+			ISessionContext sessionContext,
+			IMapper mapper,
+			IFileRepository fileRepository,
+			IWorkflowApplication workflowApplication,
+			IRepository<Workflow> workflowRepository,
+			IWorkflowDomainService workflowDomainService,
+			IOrderRepository orderRepository,
+			IOrderDomainService orderDomainService,
+			IOrderTerminateRepository orderTerminateRepository,
+			IOrderApplication orderApplication
+			) 
+		{ 
+			_sessionContext = sessionContext;
+			_mapper = mapper;
+			_fileRepository = fileRepository;
+			_workflowApplication = workflowApplication;
+			_workflowRepository = workflowRepository;
+			_workflowDomainService = workflowDomainService;
+			_orderRepository = orderRepository;
+			_orderDomainService = orderDomainService;
+			_orderTerminateRepository = orderTerminateRepository;
+			_orderApplication= orderApplication;
+		}
+
+		/// <summary>
+		/// 工单终止待申请列表
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("may-terminate")]
+		public async Task<PagedDto<OrderDto>> MayOrderTerminateList([FromQuery] OrderTerminateListDto dto)
+		{
+			var isAdmin = _orderDomainService.IsCheckAdmin();
+			var (total, items) =await _orderRepository.Queryable(isAdmin:isAdmin)
+				.Includes(d=>d.OrderTerminates)
+				.Where(d=> SqlFunc.Subqueryable<OrderTerminate>().Where(t=> t.OrderId  == d.Id && t.Status != ETerminateStatus.SendBackStart).NotAny())
+				.Where(d => d.Status >= EOrderStatus.Filed && d.ActualHandleOrgCode.StartsWith(_sessionContext.OrgId))
+				.WhereIF(!string.IsNullOrEmpty(dto.No),d=>d.No!.Contains(dto.No!))
+				.WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Title!.Contains(dto.Title!))
+				.WhereIF(dto.ApplyStartTime.HasValue && dto.ApplyEndTime.HasValue,
+					d => d.OrderTerminates.Any(t=>t.CreationTime >= dto.ApplyStartTime && t.CreationTime<= dto.ApplyEndTime))
+				.WhereIF(dto.StartTime.HasValue && dto.EndTime.HasValue,d=>d.StartTime >= dto.StartTime && d.StartTime <= dto.EndTime)
+				.OrderByDescending(d => d.StartTime)
+				.ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+			return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
+		}
+
+
+		/// <summary>
+		/// 工单终止列表
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet]
+		public async Task<PagedDto<OrderTerminateEntityDto>> OrderTerminateList([FromQuery] OrderTerminateListDto dto)
+		{
+			var (total, items) = await _orderApplication.OrderTerminateList(dto)
+				.ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+			return new PagedDto<OrderTerminateEntityDto>(total, _mapper.Map<IReadOnlyList<OrderTerminateEntityDto>>(items));
+		}
+
+		/// <summary>
+		/// 工单终止列表导出
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+
+		[HttpPost("export")]
+		public async Task<FileStreamResult> OrderTerminateListExport([FromBody] ExportExcelDto<OrderTerminateListDto> dto)
+		{
+			var query = _orderApplication.OrderTerminateList(dto.QueryDto);
+			List<OrderTerminate> data;
+			if (dto.IsExportAll)
+			{
+				data = await query.ToListAsync(HttpContext.RequestAborted);
+			}
+			else
+			{
+				var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+				data = items;
+			}
+
+			var dataDtos = _mapper.Map<ICollection<OrderTerminateEntityDto>>(data);
+
+			dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+			var dtos = dataDtos
+				.Select(stu => _mapper.Map(stu, typeof(OrderTerminateEntityDto), dynamicClass))
+				.Cast<object>()
+				.ToList();
+
+			var stream = ExcelHelper.CreateStream(dtos);
+
+			return ExcelStreamResult(stream, "工单终止列表");
+		}
+
+
+		/// <summary>
+		/// 开始工单终止流程
+		/// </summary>
+		//[Permission(EPermission.ApplyTerminate)]
+		[HttpPost("startflow")]
+		[LogFilter("开始工单终止流程")]
+		public async Task StartFlow([FromBody] TerminateStartFlowDto dto)
+		{
+			var screenAny = await _orderTerminateRepository.AnyAsync(x =>
+				x.OrderId == dto.Data.OrderId && x.Status == ETerminateStatus.Approval);
+			if (screenAny)
+				throw UserFriendlyException.SameMessage("该工单已提起终止申请,正在审批过程中,不能申请");
+
+			var isNoPass = await _orderTerminateRepository.AnyAsync(x => x.Status ==  ETerminateStatus.Refuse && x.OrderId == dto.Data.OrderId);
+			if (isNoPass)
+				throw UserFriendlyException.SameMessage("该工单已被拒绝过甄别申请,不能再次申请");
+
+			var model = _mapper.Map<OrderTerminate>(dto.Data);
+			model.Status = ETerminateStatus.Approval ;
+			model.InitId();
+			if (dto.Data.Files.Any())
+				model.FileJson = await _fileRepository.AddFileAsync(dto.Data.Files, model.Id, "", HttpContext.RequestAborted);
+			else
+				model.FileJson = new List<Share.Dtos.File.FileJson>();
+			await _orderTerminateRepository.AddAsync(model, HttpContext.RequestAborted);
+			try
+			{
+				var startDto = _mapper.Map<StartWorkflowDto>(dto.Workflow);
+				startDto.DefinitionModuleCode = WorkflowModuleConsts.OrderTerminate;
+				startDto.Opinion = dto.Data.Content;
+				startDto.Title = "申请终止流程";
+				await _workflowApplication.StartWorkflowAsync(startDto, model.Id, cancellationToken: HttpContext.RequestAborted);
+			}
+			catch (Exception e)
+			{
+				await _orderTerminateRepository.RemoveAsync(model.Id);
+				model.Id = string.Empty;
+				throw new UserFriendlyException($"工单开启终止流程失败!, {e.Message}", "工单开启终止流程失败");
+			}
+		}
+
+
+		/// <summary>
+		/// 工单终止修改后下一步流程
+		/// </summary>
+		[HttpPost("initial_nextFlow")]
+		[LogFilter("办理工单终止流程")]
+		public async Task InitialNextFlow([FromBody] TerminateNextFlowDto dto)
+		{
+			var model = await _orderTerminateRepository.GetAsync(dto.Data.Id);
+
+			if (dto.Data.Files.Any())
+				model.FileJson = await _fileRepository.AddFileAsync(dto.Data.Files, model.Id, "", HttpContext.RequestAborted);
+			else
+				model.FileJson = new List<Share.Dtos.File.FileJson>();
+
+			model.Content = dto.Data.Content;
+			model.IsRecommit = true;
+			await _orderTerminateRepository.UpdateAsync(model, HttpContext.RequestAborted);
+			try
+			{
+				dto.NextWorkflow.WorkflowId = model.WorkflowId;
+				await _workflowApplication.NextAsync(dto.NextWorkflow, cancellationToken: HttpContext.RequestAborted);
+			}
+			catch (Exception e)
+			{
+				throw new UserFriendlyException($"工单终止下一步流程失败!, {e.Message}", "工单终止下一步流程失败");
+			}
+		}
+
+		/// <summary>
+		/// 查询工单终止流程开启参数
+		/// </summary>
+		/// <returns></returns>
+		[HttpGet("screen/startflow")]
+		public async Task<NextStepsDto> GetTerminateFlowStartOptionsAsync()
+		{
+			return await _workflowApplication.GetStartStepsAsync(WorkflowModuleConsts.OrderTerminate,
+				HttpContext.RequestAborted);
+		}
+
+		/// <summary>
+		/// 查询工单终止流程参数
+		/// </summary>
+		/// <returns></returns>
+		[HttpGet("workflow/{id}")]
+		public async Task<string> GetTerminateWorkFlowAsync(string id)
+		{
+			var workflow = await _workflowRepository.Queryable().FirstAsync(x => x.ExternalId == id);
+			return workflow.Id;
+		}
+
+		/// <summary>
+		///  甄别查询流程办理下一步可选节点
+		/// </summary>
+		/// <param name="workflowId"></param>
+		/// <returns></returns>
+		[HttpGet("{workflowId}/nextsteps")]
+		public async Task<NextStepsDto> OrderTerminateNextsteps(string workflowId)
+		{
+			return await _workflowApplication.GetNextStepsAsync(workflowId, HttpContext.RequestAborted);
+		}
+
+		/// <summary>
+		/// 终止详情
+		/// </summary>
+		/// <param name="id"></param>
+		/// <returns></returns>
+		[HttpGet("{id}")]
+		public async Task<OrderTerminateEntityDto> Entity(string id)
+		{
+			var model = await _orderTerminateRepository.Queryable()
+				.Includes(x => x.Order) 
+				.Includes(x => x.Workflow, d => d.Steps)
+				.FirstAsync(x => x.Id == id);
+			var rspModel = _mapper.Map<OrderTerminateEntityDto>(model);
+			rspModel.IsCanHandle = model.Workflow?.IsCanHandle(
+				_sessionContext.RequiredUserId, _sessionContext.RequiredOrgId, _sessionContext.Roles) ?? false;
+			if (model.Status ==  ETerminateStatus.SendBackStart)
+				rspModel.IsCanHandle = false;
+			rspModel.Handle = false;
+			if (!string.IsNullOrEmpty(rspModel.WorkflowId))
+			{
+				rspModel.Handle = await _workflowDomainService.CheckCurrentIsStartStepAsync(rspModel.WorkflowId, _sessionContext.RequiredUserId,
+					_sessionContext.RequiredOrgId, HttpContext.RequestAborted);
+			}
+
+			if (rspModel.FileJson != null && rspModel.FileJson.Any())
+			{
+				var ids = rspModel.FileJson.Select(x => x.Id).ToList();
+				rspModel.Files = await _fileRepository.GetFilesAsync(ids, HttpContext.RequestAborted);
+			}
+			return rspModel;
+		}
+
+		/// <summary>
+		/// 列表页面基础数据
+		/// </summary>
+		/// <returns></returns>
+		[HttpGet("base")]
+		public async Task<object> BaseData()
+		{
+			var rsp = new
+			{
+				Status = EnumExts.GetDescriptions<ETerminateStatus>(),
+			};
+			return rsp;
+		}
+
+		/// <summary>
+		/// 终止修改理由
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpPut("update_content")]
+		[LogFilter("终止修改理由")]
+		public async Task Update([FromBody] OrderTerminateContentDto dto)
+		{
+			await _orderTerminateRepository.Updateable().SetColumns(x => new OrderTerminate { Content = dto.Content }).Where(x => x.Id == dto.Id).ExecuteCommandAsync();
+		}
+
+	}
+}

+ 16 - 16
src/Hotline.Api/Controllers/TestController.cs

@@ -50,7 +50,9 @@ using Microsoft.AspNetCore.Builder.Extensions;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Options;
 using MiniExcelLibs;
+using Newtonsoft.Json;
 using SqlSugar;
+using StackExchange.Redis;
 using XC.RSAUtil;
 using XF.Domain.Authentications;
 using XF.Domain.Cache;
@@ -126,8 +128,6 @@ public class TestController : BaseController
     //private readonly ITypedCache<List<User>> _cache;
     //private readonly ICacheManager<User> _cache;
     private readonly ICalcExpireTime _expireTime;
-    private readonly IOptions<CityBaseConfiguration> _cityBaseConfiguration;
-    private readonly ISessionContextProvider _sessionContextProvider;
 
 
     public TestController(
@@ -179,10 +179,8 @@ IOrderDomainService orderDomainService,
 ICallApplication callApplication,
         IOptionsSnapshot<AppConfiguration> appOptions,
         ISystemSettingCacheManager systemSettingCacheManager,
-        ICalcExpireTime expireTime,
-        IOptions<CityBaseConfiguration> cityBaseConfiguration,
-        ISessionContextProvider sessionContextProvider
-        )
+        ICalcExpireTime expireTime
+		)
     {
         _logger = logger;
         //_authorizeGenerator = authorizeGenerator;
@@ -231,9 +229,7 @@ ICallApplication callApplication,
         _appOptions = appOptions;
         _systemSettingCacheManager = systemSettingCacheManager;
         _expireTime = expireTime;
-        _cityBaseConfiguration = cityBaseConfiguration;
-        _sessionContextProvider = sessionContextProvider;
-    }
+	}
 
 
     [HttpGet("time")]
@@ -696,13 +692,17 @@ ICallApplication callApplication,
     [HttpGet("t5")]
     public async Task<string> GetUserAllowAnonymous()
     {
-        //var users = await _userRepository.Queryable()
-        //    .FirstAsync(d => d.Name == "xf", HttpContext.RequestAborted);
-        //return users.Id;
-        var b = _cityBaseConfiguration.Value;
-
-		var a = await _expireTime.GetWorkDay(DateTime.Now);
-        return string.Empty;
+		//var users = await _userRepository.Queryable()
+		//    .FirstAsync(d => d.Name == "xf", HttpContext.RequestAborted);
+		//return users.Id;
+		//var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+		//CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+		//var b = cityBase;
+
+		//var a = await _expireTime.GetWorkDay(DateTime.Now);
+		//自动延期订阅
+		_capPublisher.PublishDelay(DateTime.Parse("2024-10-14 14:28:00") - DateTime.Now.AddHours(1), EventNames.HotlineOrderAutomaticDelay, new PublishAutomaticDelayDto() { OrderId = "08dcebff-2fae-4c30-824f-fe2ef4d582ae" });
+		return string.Empty;
 	}
 
     [HttpGet("t6")]

+ 76 - 32
src/Hotline.Api/Controllers/UserController.cs

@@ -1,29 +1,30 @@
-using Hotline.CallCenter.Tels;
+using Hotline.Application.Users;
+using Hotline.Caching.Interfaces;
+using Hotline.CallCenter.Tels;
 using Hotline.Identity.Accounts;
 using Hotline.Identity.Roles;
 using Hotline.Permissions;
 using Hotline.Repository.SqlSugar.Extensions;
-using Hotline.Users;
-using MapsterMapper;
-using Microsoft.AspNetCore.Mvc;
-using XF.Domain.Authentications;
-using XF.Domain.Exceptions;
+using Hotline.Settings.CommonOpinions;
 using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Order;
 using Hotline.Share.Dtos.Users;
+using Hotline.Share.Enums.CallCenter;
 using Hotline.Share.Enums.Order;
-using Microsoft.AspNetCore.Authorization;
+using Hotline.Share.Enums.User;
+using Hotline.Share.Requests;
+using Hotline.Tools;
+using Hotline.Users;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Options;
 using SqlSugar;
+using System.Data;
+using XF.Domain.Authentications;
+using XF.Domain.Exceptions;
 using XF.Domain.Options;
-using XF.Utility.EnumExtensions;
-using Hotline.Caching.Interfaces;
-using Hotline.Share.Enums.CallCenter;
-using System;
-using Hotline.Settings.CommonOpinions;
-using Hotline.Share.Enums.Identity;
-using Hotline.Share.Enums.User;
 using XF.Domain.Repository;
-using Hotline.SeedData;
+using XF.Utility.EnumExtensions;
 
 namespace Hotline.Api.Controllers;
 
@@ -45,6 +46,7 @@ public class UserController : BaseController
     private readonly IOptions<IdentityConfiguration> _identityConfigurationAccessor;
     private readonly ITelRestRepository _telRestRepository;
     private readonly IRepository<SystemCommonOpinion> _commonOpinionRepository;
+    private readonly IUserApplication _userApplication;
 
     public UserController(
         ISessionContext sessionContext,
@@ -59,7 +61,8 @@ public class UserController : BaseController
         IAccountDomainService accountDomainService,
         IOptions<IdentityConfiguration> identityConfigurationAccessor,
         ITelRestRepository telRestRepository,
-        IRepository<SystemCommonOpinion> commonOpinionRepository)
+        IRepository<SystemCommonOpinion> commonOpinionRepository,
+        IUserApplication userApplication)
     {
         _sessionContext = sessionContext;
         _userDomainService = userDomainService;
@@ -74,6 +77,7 @@ public class UserController : BaseController
         _identityConfigurationAccessor = identityConfigurationAccessor;
         _telRestRepository = telRestRepository;
         _commonOpinionRepository = commonOpinionRepository;
+        _userApplication = userApplication;
     }
 
 
@@ -155,26 +159,66 @@ public class UserController : BaseController
     [HttpGet("paged")]
     public async Task<PagedDto<UserDto>> QueryPaged([FromQuery] UserPagedDto dto)
     {
-        var (total, items) = await _userRepository.Queryable(includeDeleted: true)
-            .Includes(d => d.Account)
-            .Includes(d => d.Roles)
-            .Includes(d => d.Organization)
-            .Where(d => d.Account.AccountType == EAccountType.Personal && d.Id != SysAccountSeedData.Id)
-            .WhereIF(_sessionContext.OrgIsCenter == false, d => d.OrgId.StartsWith(_sessionContext.RequiredOrgId))
-            .WhereIF(!string.IsNullOrEmpty(dto.Keyword),
-                d => d.Name.Contains(dto.Keyword!) || d.PhoneNo.Contains(dto.Keyword!) ||
-                     d.Account.UserName.Contains(dto.Keyword))
-            .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => d.OrgId == dto.OrgCode)
-            .WhereIF(!string.IsNullOrEmpty(dto.Role), d => d.Roles.Any(x => x.Id == dto.Role))
-            .OrderBy(d => d.Account.Status)
-            .OrderBy(d => d.Organization.OrgType)
-            //.OrderBy(d => d.Organization.Id)
-            .OrderByDescending(d => d.CreationTime)
-            .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+        var query =  _userApplication.QueryPaged(dto);
+        var (total, items) = await query.ToPagedListAsync(dto, HttpContext.RequestAborted);
+
+        //var (total, items) = await _userRepository.Queryable(includeDeleted: true)
+        //    .Includes(d => d.Account)
+        //    .Includes(d => d.Roles)
+        //    .Includes(d => d.Organization)
+        //    .Where(d => d.Account.AccountType == EAccountType.Personal && d.Id != SysAccountSeedData.Id)
+        //    .WhereIF(_sessionContext.OrgIsCenter == false, d => d.OrgId.StartsWith(_sessionContext.RequiredOrgId))
+        //    .WhereIF(!string.IsNullOrEmpty(dto.Keyword),
+        //        d => d.Name.Contains(dto.Keyword!) || d.PhoneNo.Contains(dto.Keyword!) ||
+        //             d.Account.UserName.Contains(dto.Keyword))
+        //    .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => d.OrgId == dto.OrgCode)
+        //    .WhereIF(!string.IsNullOrEmpty(dto.Role), d => d.Roles.Any(x => x.Id == dto.Role))
+        //    .WhereIF(!string.IsNullOrEmpty(dto.Name), d => d.Name.Contains(dto.Name))
+        //    .WhereIF(!string.IsNullOrEmpty(dto.PhoneNo), d => d.PhoneNo.Contains(dto.PhoneNo))
+        //    .WhereIF(!string.IsNullOrEmpty(dto.OrgName), d => d.Organization.Name.Contains(dto.OrgName))
+        //    .OrderBy(d => d.Account.Status)
+        //    .OrderBy(d => d.Organization.OrgType)
+        //    //.OrderBy(d => d.Organization.Id)
+        //    .OrderByDescending(d => d.CreationTime)
+        //    .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
 
         return new PagedDto<UserDto>(total, _mapper.Map<IReadOnlyList<UserDto>>(items));
     }
 
+    /// <summary>
+    /// 分页查询用户---导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("paged/_export")]
+    public async Task<FileStreamResult> QueryPagedExport([FromBody] ExportExcelDto<UserPagedDto> dto)
+    {
+        var query =  _userApplication.QueryPaged(dto.QueryDto);
+        List<User> data;
+        if (dto.IsExportAll)
+        {
+            data = await query.ToListAsync(HttpContext.RequestAborted);
+        }
+        else
+        {
+            var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+            data = items;
+        }
+
+        var dataDtos = _mapper.Map<ICollection<UserDto>>(data);
+
+        dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+        var dtos = dataDtos
+            .Select(stu => _mapper.Map(stu, typeof(UserDto), dynamicClass))
+            .Cast<object>()
+            .ToList();
+
+        var stream = ExcelHelper.CreateStream(dtos);
+
+        return ExcelStreamResult(stream, "部门通讯录数据");
+    }
+
     /// <summary>
     /// 更新用户
     /// </summary>

+ 34 - 0
src/Hotline.Api/Middleware/HeaderMiddleware.cs

@@ -0,0 +1,34 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hotline.Api.Middleware;
+
+/// <summary>
+/// 为特定的返回结果添加 头部信息 的中间件
+/// </summary>
+public class HeaderMiddleware
+{
+    private readonly RequestDelegate _next;
+
+    public HeaderMiddleware(RequestDelegate next)
+    {
+        _next = next;
+    }
+
+    public async Task InvokeAsync(HttpContext context)
+    {
+        var originalBodyStream = context.Response.Body;
+        using var responseBody = new MemoryStream();
+        context.Response.Body = responseBody;
+
+        await _next(context);
+
+        // 为返回类型是 FileStreamResult 的请求添加 头部信息
+        if (context.Response.StatusCode == 200 && context.Response is FileStreamResult)
+        {
+            context.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
+        }
+
+        context.Response.Body.Seek(0, SeekOrigin.Begin);
+        await responseBody.CopyToAsync(originalBodyStream);
+    }
+}

+ 5 - 2
src/Hotline.Api/StartupExtensions.cs

@@ -30,6 +30,7 @@ using Hotline.Configurations;
 using Hotline.DI;
 using Hotline.Share.Tools;
 using Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+using Hotline.Api.Middleware;
 using XF.Domain.Authentications;
 
 namespace Hotline.Api;
@@ -197,7 +198,7 @@ internal static class StartupExtensions
         services.AddScoped<IExpireTimeSupplier, HourSupplier>();
 
         //services.AddScoped<LogFilterAttribute>();
-        // ServiceLocator.Instance = services.BuildServiceProvider();
+        //ServiceLocator.Instance = services.BuildServiceProvider();
         return builder.Build();
     }
 
@@ -227,7 +228,9 @@ internal static class StartupExtensions
         app.MapControllers()
             .RequireAuthorization();
         //app.MapSubscribeHandler();
-
+        
+        // 为特定返回结果添加 头部信息 的中间件
+        app.UseMiddleware<HeaderMiddleware>(); 
         return app;
     }
 }

+ 20 - 2
src/Hotline.Application.Contracts/Validators/CallCenter/StartEndTimeDtoValidator.cs

@@ -1,5 +1,6 @@
 using FluentValidation;
 using Hotline.Share.Dtos.CallCenter;
+using Hotline.Share.Dtos.Order;
 using Hotline.Share.Requests;
 using System;
 using System.Collections.Generic;
@@ -14,8 +15,16 @@ public class StartEndTimeDtoValidator : AbstractValidator<StartEndTimeDto>
 {
     public StartEndTimeDtoValidator()
     {
-        RuleFor(d => d.StartTime).NotEmpty();
-        RuleFor(d => d.EndTime).NotEmpty();
+        RuleFor(d => d.StartTime).Must(m => m != default(DateTime)).WithMessage("请选择开始时间");
+        RuleFor(d => d.EndTime).Must(m => m != default(DateTime)).WithMessage("请选择结束时间");
+    }
+}
+
+public class QueryOrderVisitQuantityValidator : AbstractValidator<QueryOrderVisitQuantity>
+{
+    public QueryOrderVisitQuantityValidator()
+    {
+        Include(new StartEndTimeDtoValidator());
     }
 }
 
@@ -31,6 +40,15 @@ public class ReportRequiredPagedRequestValidator : AbstractValidator<ReportRequi
 public class QueryCallListDtoValidator : AbstractValidator<QueryCallListDto>
 {
     public QueryCallListDtoValidator()
+    {
+        RuleFor(d => d.StartTime).Must(m => m != default(DateTime)).WithMessage("请选择开始时间");
+        RuleFor(d => d.EndTime).Must(m => m != default(DateTime)).WithMessage("请选择结束时间"); ;
+    }
+}
+
+public class PagedStartEndTimeDtoValidator : AbstractValidator<PagedStartEndTimeDto>
+{
+    public PagedStartEndTimeDtoValidator()
     {
         RuleFor(d => d.StartTime).NotEmpty();
         RuleFor(d => d.EndTime).NotEmpty();

+ 21 - 0
src/Hotline.Application.Contracts/Validators/Knowledge/KnowledageCollectGroupValidator.cs

@@ -0,0 +1,21 @@
+using FluentValidation;
+using Hotline.Share.Dtos.Knowledge;
+
+namespace Hotline.Application.Contracts.Validators.Knowledge
+{
+    public class KnowledgeCollectGroupInDtoValidator : AbstractValidator<KnowledgeCollectGroupInDto>
+    {
+        public KnowledgeCollectGroupInDtoValidator()
+        {
+            RuleFor(d => d.Name).NotEmpty();
+        }
+    }
+
+    public class KnowledgeCollectAddDtoValidator : AbstractValidator<KnowledgeCollectAddDto>
+    {
+        public KnowledgeCollectAddDtoValidator()
+        {
+            // RuleFor(d => d.KnowledgeCollectGroupId).NotEmpty();
+        }
+    }
+}

+ 109 - 0
src/Hotline.Application.Tests/Application/KnowApplicationTest.cs

@@ -0,0 +1,109 @@
+using Hotline.Application.Knowledge;
+using Hotline.KnowledgeBase;
+using Hotline.KnowledgeBase.Notifies;
+using Hotline.Share.Dtos.Knowledge;
+using Hotline.Share.Tools;
+using Mapster;
+using MediatR;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Tests.Application;
+public class KnowApplicationTest
+{
+    private readonly IKnowApplication _knowApplication;
+    private readonly IRepository<KnowledgeRelationType> _knowledgeRelationTypeRepository;
+    private readonly IMediator _mediator;
+    private readonly IRepository<KnowledgeBase.Knowledge> _knowledgeRepository;
+    private readonly IKnowledgeDomainService _knowledgeDomainService;
+    private readonly IRepository<KnowledgeWord> _knowledgeWordRepository;
+    private readonly IRepository<KnowledgeHotWord> _knowledgeHotWordRepository;
+
+    public KnowApplicationTest(IKnowApplication knowApplication, IRepository<KnowledgeRelationType> knowledgeRelationTypeRepository, IMediator mediator, IRepository<KnowledgeBase.Knowledge> knowledgeRepository, IKnowledgeDomainService knowledgeDomainService, IRepository<KnowledgeWord> knowledgeWordRepository, IRepository<KnowledgeHotWord> knowledgeHotWordRepository)
+    {
+        _knowApplication = knowApplication;
+        _knowledgeRelationTypeRepository = knowledgeRelationTypeRepository;
+        _mediator = mediator;
+        _knowledgeRepository = knowledgeRepository;
+        _knowledgeDomainService = knowledgeDomainService;
+        _knowledgeWordRepository = knowledgeWordRepository;
+        _knowledgeHotWordRepository = knowledgeHotWordRepository;
+    }
+
+    [Fact]
+    public async Task TitleParticiple_Test()
+    {
+        var result = await _knowApplication.TitleParticiple("短信回访的相关解释口径、规范用语");
+        result.ShouldNotBeNull();
+    }
+
+    [Fact]
+    public void SplitKeywords_Test()
+    {
+        var keywords = "短信 筷子, 天才, 脑洞, 大开";
+        var items = keywords.SplitKeywords();
+        items.Count.ShouldBe(5);
+        items[0] = "短信";
+        items[1] = "筷子";
+        items[2] = "天才";
+        items[3] = "脑洞";
+        items[4] = "大开";
+    }
+
+    [Fact]
+    public async Task GetPageViewList_Test()
+    {
+        var r = await _knowledgeRelationTypeRepository.Queryable()
+            .OrderByDescending(m => m.CreationTime)
+            .FirstAsync();
+        var knowledge = await _knowledgeRepository.GetAsync(r.KnowledgeId);
+        await _knowledgeDomainService.KnowledgePvIncreaseAsync(knowledge, new CancellationToken());
+
+        var inDto = new PageViewInDto
+        {
+            KnowledgeTypeId = r.KnowledgeTypeId
+        };
+        var (total, items) = await _knowApplication.GetPageViewListAsync(inDto);
+        total.ShouldNotBe(0);
+    }
+
+    [Fact]
+    public async Task UpdateKnowledgeWord_Test()
+    {
+
+        var entity = await _knowledgeHotWordRepository.Queryable()
+            .OrderByDescending(m => m.CreationTime)
+            .FirstAsync();
+        entity.KeyWord = "单元测试修改";
+        var inDto = entity.Adapt<UpdateKnowledgeHotWordInDto>();
+        await _knowApplication.UpdateKnowledgeHotWordAsync(inDto);
+        var updateEntity = await _knowledgeWordRepository.GetAsync(entity.Id);
+        updateEntity.Tag.ShouldBe(entity.KeyWord);
+    }
+
+    [Fact]
+    public async Task AddKnowledgeHotWord_Test()
+    {
+        var addDto = new AddKnowledgeHotWordInDto
+        {
+            KeyWord = "单元测试" + new Random().Next(1, 100),
+        };
+        await _knowApplication.AddKnowledgeHotWordAsync(addDto);
+
+        try
+        {
+            await _knowApplication.AddKnowledgeHotWordAsync(addDto);
+
+        }
+        catch (Exception e)
+        {
+            e.Message.ShouldBe("热词已存在");
+        }
+    }
+}

+ 44 - 0
src/Hotline.Application.Tests/Application/OrderVisitApplicationTest.cs

@@ -0,0 +1,44 @@
+using Hotline.Application.Orders;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Tools;
+using Shouldly;
+using SqlSugar.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Application.Tests.Application;
+public class OrderVisitApplicationTest
+{
+    private readonly IOrderVisitApplication _orderVisitApplication;
+
+    public OrderVisitApplicationTest(IOrderVisitApplication orderVisitApplication)
+    {
+        _orderVisitApplication = orderVisitApplication;
+    }
+
+    /// <summary>
+    /// 统计测试
+    /// </summary>
+    [Theory]
+    [InlineData("2024/07/01", "2024/10/10", null)]
+    [InlineData("2024/07/01", "2024/10/10", "test")]
+    public async Task QueryOrderVisitQuantity_Test(string start, string end, string? name)
+    {
+        var inDto = new QueryOrderVisitQuantity
+        {
+            StartTime = DateTime.Parse(start),
+            EndTime = DateTime.Parse(end),
+            EmployeeName = name
+        };
+        var items = await _orderVisitApplication.QueryOrderVisitQuantityAsync(inDto);
+        items.ShouldNotBeNull();
+
+        if (name.NotNullOrEmpty())
+        {
+            items.Any().ShouldBeTrue();
+        }
+    }
+}

+ 52 - 2
src/Hotline.Application.Tests/Application/ZiGongCallReportApplicationTest.cs

@@ -1,6 +1,8 @@
 using Hotline.Application.StatisticalReport.CallReport;
 using Hotline.Share.Dtos.CallCenter;
+using Hotline.Share.Enums.CallCenter;
 using Hotline.Share.Requests;
+using Hotline.Share.Tools;
 using Shouldly;
 using SqlSugar.Extensions;
 using System;
@@ -13,16 +15,19 @@ namespace Hotline.Application.Tests.Application;
 public class ZiGongCallReportApplicationTest
 {
     private readonly ZiGongCallReportApplication _ziGongCallReportApplication;
+    private CancellationToken cancellation;
 
     public ZiGongCallReportApplicationTest(ZiGongCallReportApplication ziGongCallReportApplication)
     {
         _ziGongCallReportApplication = ziGongCallReportApplication;
+        cancellation = new CancellationToken();
     }
 
     [Fact]
     public async Task QueryCallsDetailInTotal_Test()
     {
-        var inDto = new BiQueryCallsDto {
+        var inDto = new BiQueryCallsDto
+        {
             StartTime = "2024-07-29".ObjToDate(),
             EndTime = "2024-07-29 23:59:59".ObjToDate()
         };
@@ -39,7 +44,52 @@ public class ZiGongCallReportApplicationTest
             EndTime = DateTime.Now
         };
 
-        var result = await _ziGongCallReportApplication.QuerySeatCallAsync(inDto, new CancellationToken());
+        var result = await _ziGongCallReportApplication.QuerySeatCallAsync(inDto, cancellation);
+        result.ShouldNotBeNull();
+    }
+
+    [Theory]
+    [InlineData(null, null, null, null)]
+    [InlineData(null, "19136073037", null, null)]
+    [InlineData(null, null, "67387546", null)]
+    [InlineData("20240805000001", null, null, null)]
+    [InlineData(null, null, null, "From")]
+    public async Task QueryCallsStatisticsDetail_Test(string? orderNo, string? fromNo, string? toNo, string? endBy)
+    {
+        var inDto = new QueryCallsStatisticsDetailInDto
+        {
+            StartTime = "2024-07-29".ObjToDate(),
+            EndTime = "2024-07-29 23:59:59".ObjToDate(),
+            OrderNo = orderNo,
+            FromNo = fromNo,
+            ToNo = toNo,
+            EndBy = endBy?.ToEnum<EEndBy>()
+        };
+        var (total, items) = await _ziGongCallReportApplication.QueryCallsStatisticsDetailAsync(inDto, cancellation);
+        total.ShouldNotBe(0);
+        items.Any(m => m.OrderNo.NotNullOrEmpty()).ShouldBeTrue();
+        items.Any(m => m.OrderTitle.NotNullOrEmpty()).ShouldBeTrue();
+        if (fromNo != null)
+            items.Any(m => m.FromNo != fromNo).ShouldBeFalse();
+        if (toNo != null)
+            items.Any(m => m.ToNo != toNo).ShouldBeFalse();
+        if (orderNo != null)
+            items.Any(m => m.OrderNo != orderNo).ShouldBeFalse();
+        if (endBy != null)
+            items.Any(m => m.EndBy != inDto.EndBy).ShouldBeFalse();
+    }
+
+    [Theory]
+    [InlineData()]
+    public async Task QueryCallsDetailStatistics_Test()
+    {
+        var inDto = new StartEndTimeDto 
+        { 
+            StartTime = "2024-07-29".ObjToDate(),
+            EndTime = "2024-07-29 23:59:59".ObjToDate(),
+        };
+        var result = await _ziGongCallReportApplication.QueryCallsDetailStatisticsAsync(inDto, cancellation);
+        result.Any(m => m.InConnectionRate.IsNullOrEmpty()).ShouldBeFalse();
         result.ShouldNotBeNull();
     }
 }

+ 3 - 1
src/Hotline.Application.Tests/DefaultHttpContextAccessor.cs

@@ -7,11 +7,13 @@ using System.Security.Claims;
 using System.Text;
 using System.Threading.Tasks;
 using XF.Domain.Authentications;
+using XF.Domain.Dependency;
 
 namespace Hotline.Application.Tests;
-public class DefaultHttpContextAccessor : IHttpContextAccessor, ISessionContext
+public class DefaultHttpContextAccessor : ISessionContext, IScopeDependency
 {
     private HttpContext _content = new DefaultHttpContext();
+
     private HttpContext GetContext()
     { 
         var context = new DefaultHttpContext();

+ 1 - 0
src/Hotline.Application.Tests/Domain/YiBinExpireTimeTest.cs

@@ -67,6 +67,7 @@ public class YiBinExpireTimeTest
 
     [Theory]
     [InlineData("2024-09-04 14:00:00", "CenterToOrg", "08dccc8f-37b0-40d8-8112-1afb2230c5a3", "2024-09-05 14:00:00")]
+    [InlineData("2024-10-03 09:23:46", "CenterToCenter", "08dcce43-1b95-4a12-813a-b48d7f4ec3bd", "2024-10-14 9:00:00")]
     public async Task CalcExpiredTime_Test(string beginTxt, string flowTxt, string orderId, string expected)
     {
         var beginTime = DateTime.Parse(beginTxt);

+ 4 - 1
src/Hotline.Application.Tests/Startup.cs

@@ -46,6 +46,7 @@ using Hotline.Application.CallCenter.Calls;
 using Hotline.CallCenter.Configs;
 using Tr.Sdk;
 using Hotline.Application.StatisticalReport.CallReport;
+using XF.Domain.Authentications;
 
 namespace Hotline.Application.Tests;
 public class Startup
@@ -150,7 +151,9 @@ public class Startup
             services.AddScoped<YiBinCallReportApplication>();
             services.AddScoped<IMediator, MediatorMock>();
 
-            ServiceLocator.Instance = services.BuildServiceProvider();
+            services.AddScoped<ISessionContext, DefaultHttpContextAccessor>();
+
+            //ServiceLocator.Instance = services.BuildServiceProvider();
         }
 
         public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

+ 64 - 6
src/Hotline.Application/ExportExcel/ExportApplication.cs

@@ -1,21 +1,31 @@
 
+using Hotline.Application.Tools;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Article;
+using Hotline.Share.Tools;
 using Hotline.Tools;
+using Mapster;
 using MapsterMapper;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
 using MiniExcelLibs;
+using System.Reflection;
 using XF.Domain.Dependency;
+using XF.Utility.EnumExtensions;
 
 namespace Hotline.Application.ExportExcel
 {
     public class ExportApplication : IExportApplication, IScopeDependency
     {
         private readonly IMapper _mapper;
+        private readonly IHttpContextAccessor _httpContextAccessor;
 
-        public ExportApplication(IMapper mapper)
+        public ExportApplication(IMapper mapper, IHttpContextAccessor httpContextAccessor)
         {
             _mapper = mapper;
+            _httpContextAccessor = httpContextAccessor;
         }
 
         /// <summary>
@@ -25,7 +35,7 @@ namespace Hotline.Application.ExportExcel
         /// <param name="list">数据集</param>
         /// <param name="name">导出文件名(不传则生成yyyyMMddhhmmss)</param>
         /// <returns></returns>
-        public FileStreamResult ExportData<T>(List<T> list, string? name)
+        public FileStreamResult ExportData<T>(IList<T> list, string? name)
         {
             var stream = new MemoryStream();
             stream.SaveAs(list);
@@ -36,21 +46,69 @@ namespace Hotline.Application.ExportExcel
             };
         }
 
-        public async Task<Stream> FittingAsync<T, D>(ExportExcelDto<D> dto, List<T> items, Action<List<T>>? action = null)
+        public Stream GetExcelStream<T, D>(ExportExcelDto<D> dto, IList<T> items, Func<IList<T>, T>? func = null)
         {
-            if (items != null && items.Count > 0)
+            if (items != null && items.Count > 0 && func != null)
             {
-                action?.Invoke(items);
+                items.Add(func.Invoke(items));
             }
 
             dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
 
             var dtos = items
-                .Select(stu => _mapper.Map(stu, typeof(BiCallDto), dynamicClass))
+                .Select(stu => _mapper.Map(stu, typeof(T), dynamicClass))
                 .Cast<object>()
                 .ToList();
 
             return ExcelHelper.CreateStream(dtos);
         }
+
+        public FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items, string fileName, Func<IList<T>, T>? func = null)
+        {
+            return GetExcelStream(dto, items, func).GetExcelFile(fileName);
+        }
+
+        public FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items, string fileName, string totalName) where T : new()
+        {
+            var fieldsAll = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+            var fields = fieldsAll.Where(f => IsNumericType(f.FieldType)); // 只选择数值类型字段
+            var totalField = fieldsAll.Where(m => m.Name.Contains(totalName)).First();
+
+            var sumDict = new Dictionary<FieldInfo, dynamic>();
+
+            foreach (var field in fields)
+            {
+                sumDict[field] = 0;
+            }
+
+            foreach (var item in items)
+            {
+                foreach (var field in fields)
+                {
+                    var value = field.GetValue(item);
+                    if (value != null)
+                    {
+                        sumDict[field] += (dynamic)value; // 使用 dynamic 累加
+                    }
+                }
+            }
+
+            var resultItem = new T();
+            totalField.SetValue(resultItem, "合计");
+            foreach (var field in fields)
+            {
+                field.SetValue(resultItem, sumDict[field]);
+            }
+            items.Add(resultItem);
+            return GetExcelFile(dto, items, fileName);
+        }
+
+        private static bool IsNumericType(Type type)
+        {
+            return type == typeof(int) || type == typeof(float) || type == typeof(double) ||
+                   type == typeof(decimal) || type == typeof(long) || type == typeof(short) ||
+                   type == typeof(byte) || type == typeof(uint) || type == typeof(ulong) ||
+                   type == typeof(ushort) || type == typeof(sbyte);
+        }
     }
 }

+ 20 - 3
src/Hotline.Application/ExportExcel/IExportApplication.cs

@@ -1,4 +1,7 @@
-using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.CallCenter;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Article;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using System;
 using System.Collections.Generic;
@@ -17,8 +20,22 @@ namespace Hotline.Application.ExportExcel
         /// <param name="list">数据集List<T></param>
         /// <param name="name">导出文件名(不传则生成yyyyMMddhhmmss)</param>
         /// <returns></returns>
-        FileStreamResult ExportData<T>(List<T> list, string? name);
+        FileStreamResult ExportData<T>(IList<T> list, string? name);
 
-        Task<Stream> FittingAsync<T, D>(ExportExcelDto<D> dto, List<T> items, Action<List<T>>? action = null);
+        Stream GetExcelStream<T, D>(ExportExcelDto<D> dto, IList<T> items, Func<IList<T>, T>? func = null);
+
+        FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items,string fileName, Func<IList<T>, T>? func = null);
+
+        /// <summary>
+        /// 导入数据
+        /// </summary>
+        /// <typeparam name="T">导出数据的类型</typeparam>
+        /// <typeparam name="D">导出请求入参类型</typeparam>
+        /// <param name="dto">请求入参</param>
+        /// <param name="items">被导出的数据</param>
+        /// <param name="fileName">excel 文件名</param>
+        /// <param name="totalName">需要填写"统计"两字的字段名称, 字段名称必须是 T 中的字段</param>
+        /// <returns></returns>
+        FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items, string fileName, string totalName) where T : new();
     }
 }

+ 41 - 1
src/Hotline.Application/ExportWord/WordHelper.cs

@@ -1,9 +1,49 @@
-using Hotline.Share.Dtos.ExportWord;
+using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Wordprocessing;
+using Hotline.Share.Dtos.ExportWord;
+using HtmlToOpenXml;
+using OpenHtmlToPdf;
 
 namespace Hotline.Application.ExportWord
 {
     public static class WordHelper
     {
+        public static Stream ConvertHtmlToPdf(string htmlContent)
+        {
+            var pdf = Pdf
+                .From(htmlContent)
+                .WithObjectSetting("web.defaultEncoding", "utf-8")
+                .Content();
+            var stream = new MemoryStream(pdf)
+            {
+                Position = 0
+            };
+            return stream;
+        }
+
+        public static Stream ConvertHtmlToWord(string htmlContent)
+        {
+            var memoryStream = new MemoryStream();
+            using (var wordDoc = WordprocessingDocument.Create(memoryStream, DocumentFormat.OpenXml.WordprocessingDocumentType.Document))
+            {
+                MainDocumentPart mainPart = wordDoc.AddMainDocumentPart();
+                mainPart.Document = new Document(new Body());
+
+                HtmlConverter converter = new HtmlConverter(mainPart);
+                var paragraphs = converter.Parse(htmlContent);
+
+                foreach (var paragraph in paragraphs)
+                {
+                    mainPart.Document.Body.Append(paragraph);
+                }
+
+                mainPart.Document.Save();
+            }
+
+            memoryStream.Position = 0;
+            return memoryStream;
+        }
+
         public static byte[] WordByte<T>(string templatePath, T data) where T : IWordExportTemplate
         {
             return new WordExportService(new WordExportProvider()).TemplateCreateWord(templatePath, data).WordBytes;

+ 19 - 32
src/Hotline.Application/FlowEngine/WorkflowApplication.cs

@@ -32,6 +32,7 @@ using System.Text;
 using System.Diagnostics;
 using Hotline.Configurations;
 using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
 using NPOI.SS.Formula.Functions;
 
 namespace Hotline.Application.FlowEngine;
@@ -59,7 +60,6 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     private readonly IFileRepository _fileRepository;
     private readonly ILogger<WorkflowApplication> _logger;
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
-    private readonly IOptions<CityBaseConfiguration> _cityBaseConfiguration;
 
     public WorkflowApplication(
         IDefinitionDomainService definitionDomainService,
@@ -101,7 +101,6 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
         _fileRepository = fileRepository;
         _logger = logger;
         _systemSettingCacheManager = systemSettingCacheManager;
-        _cityBaseConfiguration = cityBaseConfiguration;
         _sessionContext = sessionContextProvider.SessionContext;
     }
 
@@ -593,7 +592,8 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             CurrentStepBusinessType = currentStep.BusinessType,
             TimeTypeOptions = EnumExts.GetDescriptions<ETimeType>().ToList(),
             IsMainHandlerShow = workflow.WorkflowDefinition.IsMainHandlerShow,
-            StepId = currentStep.Id
+            StepId = currentStep.Id,
+            CurrentOrgLevel = string.IsNullOrEmpty(currentStep.HandlerOrgId) ? null : currentStep.HandlerOrgId.CalcOrgLevel()
         };
 
         var currentStepDefine = _workflowDomainService.GetStepDefine(workflow.WorkflowDefinition, currentStep.Code);
@@ -606,7 +606,8 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             //动态生成下一步
             var nextStepOption = await GetDynamicStepAsync(currentStep.InstancePolicy.Value, currentStep.StepType,
                 currentStep.BusinessType, settingHandle?.SettingValue[0], settingLead?.SettingValue[0], cancellationToken);
-            dto.Steps = new List<NextStepOption> { nextStepOption };
+            dto.Steps = new List<NextStepOption> { nextStepOption
+};
             return dto;
         }
 
@@ -851,9 +852,6 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 if (summaryTargetStep is null)
                     throw UserFriendlyException.SameMessage("未查询到汇总对象节点");
 
-                //var handlers = summaryTargetStep.Handlers
-                //    .Where(d => d.Key == summaryTargetStep.HandlerId || d.Key == summaryTargetStep.HandlerOrgId).ToList();
-
                 var handler = new FlowStepHandler
                 {
                     Key = summaryTargetStep.HandlerId,
@@ -938,7 +936,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                         Key = d.Id,
                         Value = d.Name,
                         OrgId = d.Id,
-                        OrgName = d.Name
+                        OrgName = d.Name,
                     })
                     .ToListAsync(cancellationToken);
                 break;
@@ -1032,6 +1030,8 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 throw new ArgumentOutOfRangeException();
         }
 
+        var success = int.TryParse(stepDefine.HandlerTypeItems.First().Key, out var level);
+        int? orgLevel = success ? level : null;
         return new NextStepOption
         {
             Key = stepDefine.Code,
@@ -1039,6 +1039,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             StepType = stepDefine.StepType,
             BusinessType = stepDefine.BusinessType,
             HandlerType = stepDefine.HandlerType,
+            OrgLevel = stepDefine.HandlerType is EHandlerType.OrgLevel ? orgLevel : null,
             Items = handlers
         };
     }
@@ -1266,10 +1267,8 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             case EDynamicPolicy.OrgUpLeadCenterTop:
                 orgLevel = _sessionContext.OrgLevel - 1;
                 if (orgLevel < 0) orgLevel = 0;
-                isLead = _sessionContext.Roles.Any(x => x == leadRoleCode);
-                isSkip = await _userRepository.Queryable()
-                    .AnyAsync(x => x.OrgId == _sessionContext.RequiredOrgId && x.Roles.Any(r => r.Name == leadRoleCode), cancellationToken);
-                if (orgLevel == 0 && (isLead || !isSkip))
+
+                if (orgLevel == 0)
                 {
                     businessType = EBusinessType.Send;
                     if (currentBusinessType == EBusinessType.Department)
@@ -1290,15 +1289,16 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 {
                     businessType = EBusinessType.Department;
                     upperOrgId = _sessionContext.RequiredOrgId.GetHigherOrgId(_sessionContext.OrgLevel);
+                    isLead = _sessionContext.Roles.Any(x => x == leadRoleCode);
                     if (!isLead)
                     {
+                        isSkip = await _userRepository.Queryable().AnyAsync(x => x.OrgId == _sessionContext.RequiredOrgId && x.Roles.Any(r => r.Name == leadRoleCode), cancellationToken);
                         if (isSkip)
                         {
                             roleId = leadRoleCode;
                             roleName = leadRoleName;
                         }
                     }
-
                     if (isLead || !isSkip)
                     {
                         //上级部门Id
@@ -1306,11 +1306,6 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                         roleId = handleRoleCode;
                         roleName = handleRoleName;
                     }
-                    else
-                    {
-                        orgLevel += 1;
-                    }
-
                     items = await _organizeRepository.Queryable()
                         .Where(d => d.Id == upperOrgId)
                         .Select(d => new FlowStepHandler
@@ -1338,15 +1333,13 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 isLead = _sessionContext.Roles.Any(x => x == leadRoleCode);
                 if (!isLead)
                 {
-                    isSkip = await _userRepository.Queryable()
-                        .AnyAsync(x => x.OrgId == _sessionContext.RequiredOrgId && x.Roles.Any(r => r.Name == leadRoleCode), cancellationToken);
+                    isSkip = await _userRepository.Queryable().AnyAsync(x => x.OrgId == _sessionContext.RequiredOrgId && x.Roles.Any(r => r.Name == leadRoleCode), cancellationToken);
                     if (isSkip)
                     {
                         roleId = leadRoleCode;
                         roleName = leadRoleName;
                     }
                 }
-
                 if (isLead || !isSkip)
                 {
                     //上级部门Id
@@ -1354,11 +1347,6 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                     roleId = handleRoleCode;
                     roleName = handleRoleName;
                 }
-                else
-                {
-                    orgLevel += 1;
-                }
-
                 items = await _organizeRepository.Queryable()
                     .Where(d => d.Id == upperOrgId)
                     .Select(d => new FlowStepHandler
@@ -1395,9 +1383,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 businessType = _sessionContext.OrgIsCenter ? EBusinessType.Send : EBusinessType.Department;
                 orgLevel = _sessionContext.OrgIsCenter ? 0 : 1;
                 //上级部门Id
-                upperOrgId = _sessionContext.OrgIsCenter
-                    ? _sessionContext.RequiredOrgId.Substring(0, 3)
-                    : _sessionContext.RequiredOrgId.Substring(0, 6);
+                upperOrgId = _sessionContext.OrgIsCenter ? _sessionContext.RequiredOrgId.Substring(0, 3) : _sessionContext.RequiredOrgId.Substring(0, 6);
                 items = await _organizeRepository.Queryable()
                     .Where(d => d.Id == upperOrgId)
                     .Select(d => new FlowStepHandler
@@ -1476,6 +1462,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     /// 会签 动态策略
     /// </summary>
     /// <returns></returns>
+
     private async Task<NextStepOption> GetDynamicStepAsync(
         EDynamicPolicyCountersign policy, EStepType stepType,
         EBusinessType currentBusinessType, CancellationToken cancellationToken)
@@ -1610,7 +1597,6 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             Items = items
         };
     }
-
     /// <summary>
     /// 查询下一节点办理对象类型(user or org)及实际办理对象
     /// </summary>
@@ -1797,12 +1783,13 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
     /// </summary>
     public async Task UpdateProvinceHandleResultFilesAsync(string workflowId, List<FileDto> files, CancellationToken cancellationToken)
     {
+        var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+        CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
         var workflow = await _workflowDomainService.GetWorkflowAsync(workflowId, withSteps: true, withTraces: true,
             cancellationToken: cancellationToken);
         var step = workflow.Steps.FirstOrDefault(d =>
             //d.StepHandlers.Any(d => d.OrgId == _cityBaseConfiguration.Value.CityProvince.OrgId || d.OrgId == _cityBaseConfiguration.Value.CityProvinceAssign.OrgId));
-            d.HandlerOrgId == _cityBaseConfiguration.Value.CityProvince.OrgId ||
-            d.HandlerOrgId == _cityBaseConfiguration.Value.CityProvinceAssign.OrgId);
+            d.HandlerOrgId == cityBase.CityProvince.OrgId || d.HandlerOrgId == cityBase.CityProvinceAssign.OrgId);
         if (step is not null)
         {
             step.FileJson = await _fileRepository.AddFileAsync(files, workflow.ExternalId, step.Id, cancellationToken);

+ 15 - 4
src/Hotline.Application/Handlers/FlowEngine/WorkflowEndHandler.cs

@@ -45,8 +45,9 @@ public class WorkflowEndHandler : INotificationHandler<EndWorkflowNotify>
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
     private readonly Publisher _publisher;
     private readonly ICalcExpireTime _expireTime;
+    private readonly IRepository<OrderTerminate> _orderTerminateRepository;
 
-    public WorkflowEndHandler(
+	public WorkflowEndHandler(
         IMapper mapper,
         IKnowledgeDomainService knowledgeDomainService,
         IOrderDomainService orderDomainService,
@@ -61,9 +62,9 @@ public class WorkflowEndHandler : INotificationHandler<EndWorkflowNotify>
         IOptionsSnapshot<AppConfiguration> appOptions,
         ISystemSettingCacheManager systemSettingCacheManager,
         Publisher publisher,
-        ILogger<WorkflowEndHandler> logger
-,
-        ICalcExpireTime expireTime)
+        ILogger<WorkflowEndHandler> logger,
+        ICalcExpireTime expireTime,
+        IRepository<OrderTerminate> orderTerminateRepository)
     {
         _mapper = mapper;
         _knowledgeDomainService = knowledgeDomainService;
@@ -81,6 +82,7 @@ public class WorkflowEndHandler : INotificationHandler<EndWorkflowNotify>
         _publisher = publisher;
         _logger = logger;
         _expireTime = expireTime;
+        _orderTerminateRepository = orderTerminateRepository;
     }
 
     /// <summary>Handles a notification</summary>
@@ -331,6 +333,15 @@ public class WorkflowEndHandler : INotificationHandler<EndWorkflowNotify>
                         }
                     }
                     break;
+                case WorkflowModuleConsts.OrderTerminate:
+                    var orderTerminate = await _orderTerminateRepository.Queryable()
+                        .Where(x => x.Id == workflow.ExternalId).FirstAsync(cancellationToken);
+                    if (orderTerminate != null)
+                    {
+                        orderTerminate.Status = isReviewPass ? ETerminateStatus.End : ETerminateStatus.Refuse;
+                        await _orderTerminateRepository.UpdateAsync(orderTerminate, cancellationToken);
+                    }
+                    break;
             }
 
         }

+ 20 - 4
src/Hotline.Application/Handlers/FlowEngine/WorkflowPreviousHandler.cs

@@ -35,8 +35,9 @@ namespace Hotline.Application.Handlers.FlowEngine
         private readonly IMediator _mediator;
         private readonly ISessionContext _sessionContext;
         private readonly IRepository<OrderScreenDetail> _orderScreenDetailRepository;
+        private readonly IRepository<OrderTerminate> _orderTerminateRepository;
 
-        public WorkflowPreviousHandler(
+		public WorkflowPreviousHandler(
             IOrderDomainService orderDomainService,
             IOrderRepository orderRepository,
             IOrderScreenRepository orderScreenRepository,
@@ -49,8 +50,9 @@ namespace Hotline.Application.Handlers.FlowEngine
             IRepository<User> userRepository,
             IMediator mediator,
             ISessionContext sessionContext,
-            IRepository<OrderScreenDetail> orderScreenDetailRepository
-            )
+            IRepository<OrderScreenDetail> orderScreenDetailRepository,
+            IRepository<OrderTerminate> orderTerminateRepository
+			)
         {
             _orderDomainService = orderDomainService;
             _orderRepository = orderRepository;
@@ -65,6 +67,7 @@ namespace Hotline.Application.Handlers.FlowEngine
             _orderDelayRepository = orderDelayRepository;
             _sessionContext = sessionContext;
             _orderScreenDetailRepository = orderScreenDetailRepository;
+            _orderTerminateRepository = orderTerminateRepository;
         }
 
         /// <summary>Handles a notification</summary>
@@ -190,7 +193,20 @@ namespace Hotline.Application.Handlers.FlowEngine
                     case WorkflowModuleConsts.KnowledgeDelete:
                     case WorkflowModuleConsts.TelRestApply:
                         break;
-                }
+                    case WorkflowModuleConsts.OrderTerminate:
+	                    var orderTerminate = await _orderTerminateRepository.Queryable()
+		                    .Where(x => x.Id == workflow.ExternalId).FirstAsync(cancellationToken);
+	                    if (orderTerminate != null)
+	                    {
+		                    orderTerminate.Status = ETerminateStatus.SendBack;
+							if (notification.TargetStep.StepType is EStepType.Start)
+		                    {
+                                orderTerminate.Status = ETerminateStatus.SendBackStart;
+							}
+		                    await _orderTerminateRepository.UpdateAsync(orderTerminate, cancellationToken);
+	                    }
+	                    break;
+				}
 
             }
             catch (Exception e)

+ 18 - 5
src/Hotline.Application/Handlers/FlowEngine/WorkflowStartHandler.cs

@@ -42,8 +42,9 @@ namespace Hotline.Application.Handlers.FlowEngine
         private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
         private readonly IMediator _mediator;
         private readonly ISystemSettingCacheManager _systemSettingCacheManager;
+        private readonly IRepository<OrderTerminate> _orderTerminateRepository;
 
-        public WorkflowStartHandler(
+		public WorkflowStartHandler(
             IOrderDomainService orderDomainService,
             IKnowledgeDomainService knowledgeDomainService,
             IOrderRepository orderRepository,
@@ -56,8 +57,9 @@ namespace Hotline.Application.Handlers.FlowEngine
             ICallApplication callApplication,
             IOptionsSnapshot<AppConfiguration> appOptions,
             IMediator mediator,
-            ISystemSettingCacheManager systemSettingCacheManager
-        )
+            ISystemSettingCacheManager systemSettingCacheManager,
+            IRepository<OrderTerminate> orderTerminateRepository
+		)
         {
             _orderDomainService = orderDomainService;
             _knowledgeDomainService = knowledgeDomainService;
@@ -73,7 +75,8 @@ namespace Hotline.Application.Handlers.FlowEngine
             _appOptions = appOptions;
             _mediator = mediator;
             _systemSettingCacheManager = systemSettingCacheManager;
-        }
+            _orderTerminateRepository = orderTerminateRepository;
+		}
 
         /// <summary>Handles a notification</summary>
         /// <param name="notification">The notification</param>
@@ -239,7 +242,17 @@ namespace Hotline.Application.Handlers.FlowEngine
                         }
 
                         break;
-                }
+                    case WorkflowModuleConsts.OrderTerminate:
+                        var orderTerminate = await _orderTerminateRepository.Queryable()
+	                        .Where(x => x.Id == workflow.ExternalId).FirstAsync(cancellationToken);
+                        if (orderTerminate != null)
+                        {
+	                        orderTerminate.WorkflowId = workflow.Id;
+							orderTerminate.Status = ETerminateStatus.Approval;
+                            await _orderTerminateRepository.UpdateAsync(orderTerminate, cancellationToken);
+						}
+                        break;
+				}
 
             }
             catch (Exception e)

+ 2 - 0
src/Hotline.Application/Hotline.Application.csproj

@@ -7,9 +7,11 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <PackageReference Include="HtmlToOpenXml.dll" Version="3.2.0" />
     <PackageReference Include="JiebaAspNetCore.Segmenter" Version="1.0.1" />
     <PackageReference Include="JV.PanGu.Core" Version="1.0.1" />
     <PackageReference Include="NPOI" Version="2.7.0" />
+    <PackageReference Include="OpenHtmlToPdf" Version="1.12.0" />
     <PackageReference Include="XC.RSAUtil" Version="1.3.6" />
     <PackageReference Include="Quartz.Jobs" Version="3.8.0" />
   </ItemGroup>

+ 6 - 0
src/Hotline.Application/Identity/IIdentityAppService.cs

@@ -3,12 +3,18 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using Hotline.Settings;
 using Hotline.Share.Dtos.Identity;
+using Hotline.Users;
 
 namespace Hotline.Application.Identity
 {
     public interface IIdentityAppService
     {
         Task<string> LoginAsync(LoginDto dto, CancellationToken cancellationToken);
+
+        Task<string> OldToNewLoginAsync(HotlineLoginOldToNewDto dto, CancellationToken cancellationToken);
+
+        Task<(bool,bool,User)> IsCheckAdmin(string userName);
     }
 }

+ 106 - 0
src/Hotline.Application/Identity/IdentityAppService.cs

@@ -5,6 +5,7 @@ using Hotline.Identity.Accounts;
 using Hotline.Orders;
 using Hotline.Push;
 using Hotline.Schedulings;
+using Hotline.SeedData;
 using Hotline.Settings;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Identity;
@@ -63,6 +64,111 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
 
 	}
 
+    public async Task<string> OldToNewLoginAsync(HotlineLoginOldToNewDto dto, CancellationToken cancellationToken)
+    {
+        var account = await _accountRepository.GetExtAsync(
+           d => d.UserName == dto.UserName,
+           d => d.Includes(x => x.Roles));
+
+        if (account is null)
+            throw UserFriendlyException.SameMessage("未找到帐号,请联系管理员查看");
+
+        if (account.Status != EAccountStatus.Normal)
+            throw UserFriendlyException.SameMessage("帐号已被注销,请联系管理员查看");
+
+        if (account.LockoutEnabled && account.LockoutEnd >= DateTime.Now)
+            throw UserFriendlyException.SameMessage("账号被锁定!请联系管理员查看");
+
+        var userInfo = await _userRepository.GetAsync(p => p.Id == account.Id, cancellationToken);
+
+        //限制系统类型账户频繁获取token的行为
+        //todo
+
+        if (account.LockoutEnd.HasValue || account.AccessFailedCount > 0)
+        {
+            account.LockoutEnd = null;
+            account.AccessFailedCount = 0;
+            await _accountRepository.UpdateAsync(account, cancellationToken);
+        }
+
+        var user = await _userRepository.Queryable()
+            .Includes(d => d.Organization)
+            .FirstAsync(d => d.Id == account.Id);
+
+        //平均派单
+        var averageSendOrder = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.AverageSendOrder).SettingValue[0]);
+        if (averageSendOrder)
+        {
+            await AverageOrderScheduling(account.Id, cancellationToken);
+        }
+        var jwtOptions = _identityOptionsAccessor.Value.Jwt;
+        var claims = new List<Claim>
+        {
+            //new(JwtClaimTypes.Id, account.Id),
+            new(JwtClaimTypes.Subject, account.Id),
+            new(JwtClaimTypes.PhoneNumber, account.PhoneNo ?? string.Empty),
+            new(AppClaimTypes.UserDisplayName, account.Name),
+            new(JwtClaimTypes.Scope, jwtOptions.Scope),
+            new(AppClaimTypes.UserPasswordChanged, account.PasswordChanged.ToString()),
+            new(AppClaimTypes.StaffNo, user.StaffNo ?? string.Empty),
+        };
+        if (!string.IsNullOrEmpty(user.OrgId) && user.Organization is not null)
+        {
+            claims.AddRange(
+                new List<Claim>
+                {
+                    new(AppClaimTypes.DepartmentId, user.OrgId ?? string.Empty),
+                    new(AppClaimTypes.DepartmentIsCenter, user.Organization?.IsCenter.ToString()),
+                    new(AppClaimTypes.DepartmentName, user.Organization?.Name ?? string.Empty),
+                    new(AppClaimTypes.DepartmentAreaCode, user.Organization?.AreaCode ?? string.Empty),
+                    new(AppClaimTypes.DepartmentAreaName, user.Organization?.AreaName ?? string.Empty),
+                    new(AppClaimTypes.DepartmentLevel, user.Organization?.Level.ToString() ?? string.Empty),
+                    new(AppClaimTypes.AreaId, user.OrgId?.GetHigherOrgId() ?? string.Empty),
+                }
+            );
+        }
+        claims.AddRange(account.Roles.Select(d => new Claim(JwtClaimTypes.Role, d.Name)));
+        var audience = new AudienceTicket(account.Id);
+        var expiredSeconds = jwtOptions.Expired <= 0 ? 3600 : jwtOptions.Expired;
+        await _cacheAudience.SetAsync(audience.Id, audience, TimeSpan.FromSeconds(expiredSeconds), cancellationToken);
+        var token = _jwtSecurity.EncodeJwtToken(claims, audience.Ticket);
+        return token;
+    }
+
+    public async Task<(bool,bool, User)> IsCheckAdmin(string userName)
+    {
+        var isAdmin = false;
+        var isCenter = false;
+        var account = await _accountRepository.GetExtAsync(
+           d => d.UserName == userName,
+           d => d.Includes(x => x.Roles)
+           );
+
+        if (account is null)
+            throw UserFriendlyException.SameMessage("未找到帐号,请联系管理员查看");
+
+        if (account.Status != EAccountStatus.Normal)
+            throw UserFriendlyException.SameMessage("帐号已被注销,请联系管理员查看");
+
+        if (account.LockoutEnabled && account.LockoutEnd >= DateTime.Now)
+            throw UserFriendlyException.SameMessage("账号被锁定!请联系管理员查看");
+
+        var user = await _userRepository.Queryable()
+            .Includes(d => d.Organization)
+            .Includes(d => d.Roles)
+            .FirstAsync(d => d.Id == account.Id);
+
+        var systemAdministrator = _systemSettingCacheManager.GetSetting(SettingConstants.SystemAdministrator)?.SettingValue[0];
+        if (!string.IsNullOrEmpty(systemAdministrator) && (user.Roles.Any(x=>x.Name.Contains(systemAdministrator)) || user.Roles.Any(x=>x.Name.Contains(RoleSeedData.AdminRole))))
+            isAdmin = true;
+        else
+            isAdmin = false;
+
+        isCenter = user.Organization.IsCenter;
+
+        return (isAdmin, isCenter,user);
+    }
+
     public async Task<string> LoginAsync(LoginDto dto, CancellationToken cancellationToken)
     {
         var account = await _accountRepository.GetExtAsync(

+ 43 - 1
src/Hotline.Application/Knowledge/IKnowApplication.cs

@@ -1,6 +1,7 @@
 using Hotline.KnowledgeBase;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Knowledge;
+using Microsoft.AspNetCore.Mvc;
 
 namespace Hotline.Application.Knowledge
 {
@@ -11,7 +12,7 @@ namespace Hotline.Application.Knowledge
         /// </summary>
         /// <param name="pagedDto"></param>
         /// <returns></returns>
-        Task<PagedDto<KnowledgeDataDto>> GetKnowList(KnowPagedListDto pagedDto);
+        Task<(int, IList<KnowledgeDataDto>)> GetKnowList(KnowPagedListDto pagedDto, CancellationToken cancellationToken);
 
         /// <summary>
         /// 知识申请查询
@@ -28,5 +29,46 @@ namespace Hotline.Application.Knowledge
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
         Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyHandleList(KnowledgeHandlePagedDto pagedDto, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 批量导出知识详情
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        Task<Dictionary<string, Stream>> KnowledgeInfoListExportAsync(KnowledgeInfoExportInDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 知识标题分词
+        /// </summary>
+        /// <param name="keywords"></param>
+        /// <param name="title"></param>
+        /// <returns></returns>
+        Task<IList<KnowledgeWordOutDto>> TitleParticiple(string title);
+
+        /// <summary>
+        /// 知识库浏览历史
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="requestAborted"></param>
+        /// <returns></returns>
+        Task<(int, IList<PageViewOutDto>)> GetPageViewListAsync(PageViewInDto dto, CancellationToken requestAborted = default);
+
+        /// <summary>
+        /// 热词集合
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="requestAborted"></param>
+        /// <returns></returns>
+        Task<(int total, IList<KnowledgeHotWordOutDto> items)> GetKnowledgeHotWordListAsync(KnowledgeHotWordInDto dto, CancellationToken requestAborted);
+
+        /// <summary>
+        /// 修改热词
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="requestAborted"></param>
+        /// <returns></returns>
+        Task UpdateKnowledgeHotWordAsync(UpdateKnowledgeHotWordInDto dto, CancellationToken requestAborted = default);
+        Task AddKnowledgeHotWordAsync(AddKnowledgeHotWordInDto dto, CancellationToken requestAborted = default);
     }
 }

+ 245 - 153
src/Hotline.Application/Knowledge/KnowApplication.cs

@@ -1,169 +1,261 @@
-using Hotline.KnowledgeBase;
+using Hotline.Application.Tools;
+using Hotline.KnowledgeBase;
 using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Settings;
 using Hotline.Settings.Hotspots;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Knowledge;
+using Hotline.Share.Enums.Article;
 using Hotline.Share.Enums.KnowledgeBase;
+using Hotline.Share.Tools;
 using Hotline.Users;
+using Mapster;
 using MapsterMapper;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Org.BouncyCastle.Utilities.IO;
+using PanGu;
 using SqlSugar;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
 using XF.Domain.Repository;
 
 namespace Hotline.Application.Knowledge
 {
-	public class KnowApplication : IKnowApplication, IScopeDependency
-	{
-		private readonly IKnowledgeRepository _knowledgeRepository;
-		private readonly IRepository<KnowledgeApply> _knowledgeApplyRepository;
-		private readonly ISessionContext _sessionContext;
-		private readonly IMapper _mapper;
-
-		/// <summary>
-		/// 
-		/// </summary>
-		/// <param name="knowledgeRepository"></param>
-		/// <param name="knowledgeApplyRepository"></param>
-		/// <param name="sessionContext"></param>
-		/// <param name="knowledgeDomainService"></param>
-		/// <param name="mapper"></param>
-		public KnowApplication(IKnowledgeRepository knowledgeRepository, IRepository<KnowledgeApply> knowledgeApplyRepository, ISessionContext sessionContext, IMapper mapper)
-		{
-			_knowledgeRepository = knowledgeRepository;
-			_knowledgeApplyRepository = knowledgeApplyRepository;
-			_sessionContext = sessionContext;
-			_mapper = mapper;
-		}
-
-		/// <summary>
-		/// 知识库查询
-		/// </summary>
-		/// <param name="pagedDto"></param>
-		/// <returns></returns>
-		public async Task<PagedDto<KnowledgeDataDto>> GetKnowList(KnowPagedListDto pagedDto)
-		{
-			RefAsync<int> total = 0;
-			var temp = await _knowledgeRepository
-			   .Queryable()
-			   .InnerJoin<User>((o, cus) => o.CreatorId == cus.Id)
-			   .InnerJoin<SystemOrganize>((o, cus, sys) => o.CreatorOrgId == sys.Id)
-			   .LeftJoin<Hotspot>((o, cus, sys, hst) => o.HotspotId == hst.Id)
-			   .Where((o, cus, sys, hst) => o.IsDeleted == false && o.Status != EKnowledgeStatus.Revert && o.Status != EKnowledgeStatus.Drafts)
-			   //关键词查询标题、创建人、创建部门
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), (o, cus, sys,  hst) => o.Title.Contains(pagedDto.Keyword!) || cus.Name.Contains(pagedDto.Keyword!) || sys.Name.Contains(pagedDto.Keyword!))
-			   //分类
-			   //.WhereIF(!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId), (o, cus, sys,  hst) => SqlFunc.JsonListObjectAny(o.KnowledgeType,"Key", pagedDto.KnowledgeTypeId))
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId), (o, cus, sys, hst) => o.KnowledgeType.Any(t=>t.KnowledgeTypeId == pagedDto.KnowledgeTypeId))
-			   //热点
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.HotspotId), (o, cus, sys,  hst) => o.HotspotId == pagedDto.HotspotId)
-			   //部门
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.CreateOrgId), (o, cus, sys,  hst) => o.CreatorOrgId == pagedDto.CreateOrgId)
-			  //状态
-			  // .WhereIF(pagedDto.Status.HasValue, (o, cus, sys, kn, hst) => o.Status == pagedDto.Status)
-			  //创建时间
-			  //.WhereIF(pagedDto.CreationStartTime.HasValue, (o, cus, sys, kn, hst) => o.CreationTime >= pagedDto.CreationStartTime)
-			  //.WhereIF(pagedDto.CreationEndTime.HasValue, (o, cus, sys, kn, hst) => o.CreationTime <= pagedDto.CreationEndTime)
-			  // //上架时间
-			  // .WhereIF(pagedDto.StartOnShelfTime.HasValue, (o, cus, sys, kn, hst) => o.OnShelfTime >= pagedDto.StartOnShelfTime)
-			  //.WhereIF(pagedDto.EndOnShelfTime.HasValue, (o, cus, sys, kn, hst) => o.OnShelfTime <= pagedDto.EndOnShelfTime)
-			  // //下架时间
-			  // .WhereIF(pagedDto.StartOffShelfTime.HasValue, (o, cus, sys, kn, hst) => o.OffShelfTime >= pagedDto.StartOffShelfTime)
-			  //.WhereIF(pagedDto.EndOffShelfTime.HasValue, (o, cus, sys, kn, hst) => o.OffShelfTime <= pagedDto.EndOffShelfTime)
-			   //重新构建数据
-			   .Select((o, cus, sys,  hst) => new
-			   {
-				   index = SqlFunc.RowNumber($"{o.Version} desc ", $"{o.Code}"),
-				   CreationName = cus.Name,
-				   CreationBMName = sys.Name,
-				   HotspotName = hst.HotSpotFullName,
-				   o.Id,
-				   o.Code,
-				   o.Title,
-				   o.Version,
-				   //o.KnowledgeType,
-				   o.KnowledgeType,
-				   o.IsDeleted,
-				   o.HotspotId,
-				   o.OnShelfTime,
-				   o.CreationTime,
-				   o.PageView,
-				   o.Status,
-				   o.OffShelfTime,
-				   o.LastModificationTime,
-				   o.WorkflowId,
-				   o.ExpiredTime,
-				   CreatorOrgId = o.CreatorOrgId,
-
-			   })
-		   //将结果合并成一个表
-		   .MergeTable()
-		   //取第一条数据
-		   .Where(d => d.index == 1)
-			.WhereIF(pagedDto.Status.HasValue, o => o.Status == pagedDto.Status)
-		   .OrderByDescending(d => d.CreationTime)
-		  //转分页数据
-		  .ToPageListAsync(pagedDto.PageIndex, pagedDto.PageSize, total);
-			//返回数据
-			return new PagedDto<KnowledgeDataDto>(total.Value, _mapper.Map<IReadOnlyList<KnowledgeDataDto>>(temp));
-		}
-
-		/// <summary>
-		/// 知识申请查询
-		/// </summary>
-		/// <param name="pagedDto"></param>
-		/// <param name="cancellationToken"></param>
-		/// <returns></returns>
-		public async Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyList(KnowledgeApplyPagedDto pagedDto, CancellationToken cancellationToken)
-		{
-			var (total, items) = await _knowledgeApplyRepository
-			   .Queryable()
-			   .Includes(it => it.User)
-			   .Includes(it => it.SystemOrganize)
-			   .Where(d => d.CreatorId == _sessionContext.RequiredUserId)
-			   .WhereIF(pagedDto.Status.HasValue, d => d.Status == pagedDto.Status)
-			   .WhereIF(pagedDto.ApplyType.HasValue, d => d.KnowledgeApplyType == pagedDto.ApplyType)
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "0", d => d.IsOvertime == true)
-			   .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "1", d => d.IsOvertime == false)
-			   .WhereIF(pagedDto.StartTime.HasValue, d => d.CreationTime >= pagedDto.StartTime)
-			   .WhereIF(pagedDto.EndTime.HasValue, d => d.CreationTime <= pagedDto.EndTime)
-			   .OrderByDescending(p => p.CreationTime)
-			   .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
-
-			return new PagedDto<KnowledgeApplyHandlePageDto>(total, _mapper.Map<IReadOnlyList<KnowledgeApplyHandlePageDto>>(items));
-		}
-
-		/// <summary>
-		/// 申请处理查询
-		/// </summary>
-		/// <param name="pagedDto"></param>
-		/// <param name="cancellationToken"></param>
-		/// <returns></returns>
-		public async Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyHandleList(KnowledgeHandlePagedDto pagedDto, CancellationToken cancellationToken)
-		{
-			var (total, items) = await _knowledgeApplyRepository
-				 .Queryable()
-				 .Includes(it => it.User)
-				 .Includes(it => it.SystemOrganize)
-				 .Where(p => p.DepartmentId == _sessionContext.RequiredOrgId)
-				 .WhereIF(pagedDto.Status.HasValue, d => d.Status == pagedDto.Status)
-				 .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), d => d.User.Name.Contains(pagedDto.Keyword!) || d.SystemOrganize.Name.Contains(pagedDto.Keyword!))
-				 .WhereIF(pagedDto.ApplyType.HasValue, d => d.KnowledgeApplyType == pagedDto.ApplyType)
-				 .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "0", d => d.IsOvertime == true)
-				 .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "1", d => d.IsOvertime == false)
-
-				 .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status != EKnowledgeApplyStatus.Failed && pagedDto.Status != EKnowledgeApplyStatus.Succeed, d => d.CreationTime >= pagedDto.StartTime)
-				 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status != EKnowledgeApplyStatus.Failed && pagedDto.Status != EKnowledgeApplyStatus.Succeed, d => d.CreationTime <= pagedDto.EndTime)
-				 .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Failed, d => d.ReturnTime >= pagedDto.StartTime)
-				 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Failed, d => d.ReturnTime <= pagedDto.EndTime)
-				  .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Succeed, d => d.HandleTime >= pagedDto.StartTime)
-				 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Succeed, d => d.HandleTime <= pagedDto.EndTime)
-
-				 .OrderByDescending(p => p.CreationTime)
-				 .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
-
-			return new PagedDto<KnowledgeApplyHandlePageDto>(total, _mapper.Map<IReadOnlyList<KnowledgeApplyHandlePageDto>>(items));
-		}
-	}
+    public class KnowApplication : IKnowApplication, IScopeDependency
+    {
+        private readonly IKnowledgeRepository _knowledgeRepository;
+        private readonly IRepository<KnowledgePv> _knowledgePvepository;
+        private readonly IRepository<Hotspot> _hotspotTypeRepository;
+        private readonly IRepository<KnowledgeType> _knowledgeTypeRepository;
+        private readonly IRepository<KnowledgeApply> _knowledgeApplyRepository;
+        private readonly IRepository<KnowledgeWord> _knowledgeWordRepository;
+        private readonly IRepository<KnowledgeHotWord> _knowledgeHotWordRepository;
+        private readonly ISessionContext _sessionContext;
+        private readonly IMapper _mapper;
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="knowledgeRepository"></param>
+        /// <param name="knowledgeApplyRepository"></param>
+        /// <param name="sessionContext"></param>
+        /// <param name="knowledgeDomainService"></param>
+        /// <param name="mapper"></param>
+        public KnowApplication(IKnowledgeRepository knowledgeRepository, IRepository<KnowledgeApply> knowledgeApplyRepository, ISessionContext sessionContext, IMapper mapper, IRepository<KnowledgeType> knowledgeTypeRepository, IRepository<Hotspot> hotspotTypeRepository, IRepository<KnowledgeWord> knowledgeWordRepository, IRepository<KnowledgePv> knowledgePvepository, IRepository<KnowledgeHotWord> knowledgeHotWordRepository)
+        {
+            _knowledgeRepository = knowledgeRepository;
+            _knowledgeApplyRepository = knowledgeApplyRepository;
+            _sessionContext = sessionContext;
+            _mapper = mapper;
+            _knowledgeTypeRepository = knowledgeTypeRepository;
+            _hotspotTypeRepository = hotspotTypeRepository;
+            _knowledgeWordRepository = knowledgeWordRepository;
+            _knowledgePvepository = knowledgePvepository;
+            _knowledgeHotWordRepository = knowledgeHotWordRepository;
+        }
+
+        /// <summary>
+        /// 知识库查询
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <returns></returns>
+        public async Task<(int, IList<KnowledgeDataDto>)> GetKnowList(KnowPagedListDto pagedDto, CancellationToken cancellationToken)
+        {
+            var typeSpliceName = string.Empty;
+            var hotspotHotSpotFullName = string.Empty;
+            if (!string.IsNullOrEmpty(pagedDto.KnowledgeTypeId))
+            {
+                var type = await _knowledgeTypeRepository.GetAsync(x => x.Id == pagedDto.KnowledgeTypeId);
+                typeSpliceName = type?.SpliceName;
+            }
+            if (!string.IsNullOrEmpty(pagedDto.HotspotId))
+            {
+                var hotspot = await _hotspotTypeRepository.GetAsync(x => x.Id == pagedDto.HotspotId);
+                hotspotHotSpotFullName = hotspot?.HotSpotFullName;
+            }
+            var (total, temp) = await _knowledgeRepository.Queryable(false, false, false)
+                .Includes(x => x.User)
+                .Includes(x => x.SystemOrganize)
+                .Includes(x => x.SourceOrganize)
+                .Includes(x => x.HotspotType)
+                .Includes(x => x.Workflow)
+                .Includes(x => x.KnowledgeType)
+                .Where(x => x.IsDeleted == false)
+                .Where(x => x.KnowledgeType.Any(t => t.KnowledgeType.KnowledgeTypeOrgs.Any(to => to.OrgId == _sessionContext.RequiredOrgId) || t.KnowledgeType.KnowledgeTypeOrgs.Any() == false))
+                .Where(x => (x.Status == EKnowledgeStatus.Drafts && x.CreatorId == _sessionContext.UserId) || (x.Status != EKnowledgeStatus.Drafts))
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.Title), x => x.Title.Contains(pagedDto.Title!))
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), x => x.Title.Contains(pagedDto.Keyword!) || x.CreatorName.Contains(pagedDto.Keyword!) || x.CreatorOrgName.Contains(pagedDto.Keyword!) || x.SourceOrganize.Name.Contains(pagedDto.Keyword!))
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status != EKnowledgeStatus.OffShelf, x => x.Status == pagedDto.Status && ((x.ExpiredTime != null && x.ExpiredTime > DateTime.Now) || x.ExpiredTime == null))
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == EKnowledgeStatus.OffShelf, x => x.Status == pagedDto.Status || (x.ExpiredTime != null && x.ExpiredTime < DateTime.Now && x.Status != EKnowledgeStatus.Drafts))
+                .WhereIF(pagedDto.IsPublic.HasValue, x => x.IsPublic == pagedDto.IsPublic)
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.Summary), x => x.Summary != null && x.Summary.Contains(pagedDto.Summary!))
+                .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
+                .WhereIF(!string.IsNullOrEmpty(hotspotHotSpotFullName), x => x.HotspotType.HotSpotFullName.EndsWith(hotspotHotSpotFullName!))
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.CreateOrgId), x => x.SourceOrganizeId != null && x.SourceOrganizeId.EndsWith(pagedDto.CreateOrgId!))
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.ModuleCode), x => x.Workflow.ModuleCode == pagedDto.ModuleCode)
+                .OrderByDescending(d => d.CreationTime)
+                .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
+            //返回数据
+            return (total, _mapper.Map<IList<KnowledgeDataDto>>(temp));
+        }
+
+        /// <summary>
+        /// 知识申请查询
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyList(KnowledgeApplyPagedDto pagedDto, CancellationToken cancellationToken)
+        {
+            var (total, items) = await _knowledgeApplyRepository
+               .Queryable()
+               .Includes(it => it.User)
+               .Includes(it => it.SystemOrganize)
+               .Where(d => d.CreatorId == _sessionContext.RequiredUserId)
+               .WhereIF(pagedDto.Status.HasValue, d => d.Status == pagedDto.Status)
+               .WhereIF(pagedDto.ApplyType.HasValue, d => d.KnowledgeApplyType == pagedDto.ApplyType)
+               .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "0", d => d.IsOvertime == true)
+               .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "1", d => d.IsOvertime == false)
+               .WhereIF(pagedDto.StartTime.HasValue, d => d.CreationTime >= pagedDto.StartTime)
+               .WhereIF(pagedDto.EndTime.HasValue, d => d.CreationTime <= pagedDto.EndTime)
+               .OrderByDescending(p => p.CreationTime)
+               .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
+
+            return new PagedDto<KnowledgeApplyHandlePageDto>(total, _mapper.Map<IReadOnlyList<KnowledgeApplyHandlePageDto>>(items));
+        }
+
+        /// <summary>
+        /// 申请处理查询
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<PagedDto<KnowledgeApplyHandlePageDto>> GetApplyHandleList(KnowledgeHandlePagedDto pagedDto, CancellationToken cancellationToken)
+        {
+            var (total, items) = await _knowledgeApplyRepository
+                 .Queryable()
+                 .Includes(it => it.User)
+                 .Includes(it => it.SystemOrganize)
+                 .Where(p => p.DepartmentId == _sessionContext.RequiredOrgId)
+                 .WhereIF(pagedDto.Status.HasValue, d => d.Status == pagedDto.Status)
+                 .WhereIF(!string.IsNullOrEmpty(pagedDto.Keyword), d => d.User.Name.Contains(pagedDto.Keyword!) || d.SystemOrganize.Name.Contains(pagedDto.Keyword!))
+                 .WhereIF(pagedDto.ApplyType.HasValue, d => d.KnowledgeApplyType == pagedDto.ApplyType)
+                 .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "0", d => d.IsOvertime == true)
+                 .WhereIF(!string.IsNullOrEmpty(pagedDto.IsOvertime) && pagedDto.IsOvertime == "1", d => d.IsOvertime == false)
+
+                 .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status != EKnowledgeApplyStatus.Failed && pagedDto.Status != EKnowledgeApplyStatus.Succeed, d => d.CreationTime >= pagedDto.StartTime)
+                 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status != EKnowledgeApplyStatus.Failed && pagedDto.Status != EKnowledgeApplyStatus.Succeed, d => d.CreationTime <= pagedDto.EndTime)
+                 .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Failed, d => d.ReturnTime >= pagedDto.StartTime)
+                 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Failed, d => d.ReturnTime <= pagedDto.EndTime)
+                  .WhereIF(pagedDto.StartTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Succeed, d => d.HandleTime >= pagedDto.StartTime)
+                 .WhereIF(pagedDto.EndTime.HasValue && pagedDto.Status == EKnowledgeApplyStatus.Succeed, d => d.HandleTime <= pagedDto.EndTime)
+
+                 .OrderByDescending(p => p.CreationTime)
+                 .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
+
+            return new PagedDto<KnowledgeApplyHandlePageDto>(total, _mapper.Map<IReadOnlyList<KnowledgeApplyHandlePageDto>>(items));
+        }
+
+        public async Task<Dictionary<string, Stream>> KnowledgeInfoListExportAsync(KnowledgeInfoExportInDto dto, CancellationToken cancellationToken)
+        {
+            var streamList = new Dictionary<string, Stream>();
+            var knowList = await _knowledgeRepository.Queryable()
+                .Where(m => dto.Ids.Contains(m.Id))
+                .Select(m => new { m.Title, m.Content })
+                .ToListAsync(cancellationToken);
+            foreach (var item in knowList)
+            {
+                Stream stream = item.Content.HtmlToStream(dto.FileType);
+                streamList.Add(item.Title + dto.FileType.GetFileExtension(), stream);
+            }
+            return streamList;
+        }
+
+        public async Task<IList<KnowledgeWordOutDto>> TitleParticiple(string title)
+        {
+            var keywords = new List<KnowledgeWordOutDto>();
+            var splitWords = new Segment().DoSegment(title);
+            for (int i = 0;i < splitWords.Count;i++)
+            {
+                var word = splitWords.ElementAt(i);
+                if (word is not { WordType: WordType.SimplifiedChinese, Word.Length: > 1 }) continue;
+
+                var tag = splitWords.ElementAt(i).Word;
+                var entity = await _knowledgeWordRepository.Queryable().Where(m => m.Tag == tag).FirstAsync();
+                if (entity is not null)
+                {
+                    keywords.Add(new KnowledgeWordOutDto(entity.Id, entity.Tag));
+                    continue;
+                }
+
+                var wordEntity = new KnowledgeWord
+                {
+                    Tag = tag,
+                    Classify = "普通标签",
+                    Remark = "系统自动从知识标题分词",
+                    Synonym = "",
+                    IsEnable = 1
+                };
+                var keyId = await _knowledgeWordRepository.AddAsync(wordEntity);
+                keywords.Add(new KnowledgeWordOutDto(keyId, tag));
+            }
+            return keywords;
+        }
+
+        public async Task<(int, IList<PageViewOutDto>)> GetPageViewListAsync(PageViewInDto dto, CancellationToken requestAborted = default)
+        {
+            var query = _knowledgePvepository.Queryable(includeDeleted: true)
+                .LeftJoin<KnowledgeBase.Knowledge>((p, k) => p.KnowledgeCode == k.Code)
+                .LeftJoin<KnowledgeRelationType>((p, k, r) => r.KnowledgeId == k.Id)
+                .WhereIF(dto.Title.NotNullOrEmpty(), (p, k, r) => k.Title.Contains(dto.Title))
+                .WhereIF(dto.CreatorName.NotNullOrEmpty(), (p, k, r) => p.CreatorName.Contains(dto.CreatorName))
+                .WhereIF(dto.KnowledgeTypeId.NotNullOrEmpty(), (p, k, r) => r.KnowledgeTypeId == dto.KnowledgeTypeId)
+                .WhereIF(dto.StartTime.HasValue && dto.EndTime.HasValue, (p, k, r) => p.CreationTime >= dto.StartTime && p.CreationTime <= dto.EndTime)
+                .OrderByDescending((p, k, r) => p.CreationTime);
+
+            if (_sessionContext.OrgIsCenter == false)
+                query = query.Where((p, k, r) => p.CreatorOrgId.StartsWith(_sessionContext.OrgId));
+
+            return await query.Select<PageViewOutDto>().ToPagedListAsync(dto.PageIndex, dto.PageSize, requestAborted);
+        }
+
+        public async Task<(int total, IList<KnowledgeHotWordOutDto> items)> GetKnowledgeHotWordListAsync(KnowledgeHotWordInDto dto, CancellationToken requestAborted)
+        {
+            var query = _knowledgeHotWordRepository.Queryable()
+                .WhereIF(dto.IsEnable != null, m => m.IsEnable == dto.IsEnable)
+                .WhereIF(dto.KeyWord.NotNullOrEmpty(), m => m.KeyWord.Contains(dto.KeyWord))
+                .WhereIF(dto.Type != null, m => m.Type == dto.Type)
+                // .WhereIF(dto.StartTime.HasValue && dto.EndTime.HasValue, m => m.CreationTime >= dto.StartTime && m.CreationTime <= dto.EndTime)
+                ;
+
+            if (dto.SortField.NotNullOrEmpty())
+                query = query.OrderByPropertyName(dto.SortField, (OrderByType)dto.OrderByType);
+            else
+            {
+                query = query.OrderByDescending(m => m.Sort);
+                query = query.OrderByDescending(m => m.CreationTime);
+            }
+
+            return await query.Select<KnowledgeHotWordOutDto>().ToPagedListAsync(dto.PageIndex, dto.PageSize, requestAborted);
+        }
+
+        public async Task UpdateKnowledgeHotWordAsync(UpdateKnowledgeHotWordInDto dto, CancellationToken requestAborted)
+        {
+            var entity = await _knowledgeHotWordRepository.GetAsync(dto.Id)
+                ?? throw UserFriendlyException.SameMessage("数据不存在");
+            dto.Adapt(entity);
+            await _knowledgeHotWordRepository.UpdateAsync(entity);
+        }
+
+        public async Task AddKnowledgeHotWordAsync(AddKnowledgeHotWordInDto dto, CancellationToken requestAborted)
+        {
+            var isExist = await _knowledgeHotWordRepository.Queryable()
+                .Where(m => m.KeyWord == dto.KeyWord).AnyAsync();
+            if (isExist) throw UserFriendlyException.SameMessage("热词已存在");
+            var entity = dto.Adapt<KnowledgeHotWord>();
+            await _knowledgeHotWordRepository.AddAsync(entity, requestAborted);
+        }
+    }
 }

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

@@ -306,6 +306,12 @@ namespace Hotline.Application.Orders
         /// <returns></returns>
         ISugarQueryable<OrderObserve> OrderObserveList(OrderObserveListDto dto);
 
+        /// <summary>
+        /// 终止列表
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<OrderTerminate> OrderTerminateList(OrderTerminateListDto dto);
 
 	}
 }

+ 13 - 0
src/Hotline.Application/Orders/IOrderVisitApplication.cs

@@ -0,0 +1,13 @@
+using Hotline.Share.Dtos.Order;
+
+namespace Hotline.Application.Orders;
+
+public interface IOrderVisitApplication
+{
+    /// <summary>
+    /// 回访量统计
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    Task<List<OrderVisitQuantityOutDto>> QueryOrderVisitQuantityAsync(QueryOrderVisitQuantity dto, CancellationToken cancellationToken = default);
+}

+ 32 - 12
src/Hotline.Application/Orders/OrderApplication.cs

@@ -65,6 +65,7 @@ using Hotline.Share.Tools;
 using Hotline.EventBus;
 using Hotline.Orders.Notifications;
 using Hotline.OrderTranspond;
+using Newtonsoft.Json;
 
 namespace Hotline.Application.Orders;
 
@@ -104,11 +105,10 @@ public class OrderApplication : IOrderApplication, IScopeDependency
     private readonly IRepository<OrderScreen> _orderScreenRepository;
     private readonly IRepository<OrderSendBackAudit> _orderSendBackAuditRepository;
     private readonly ICalcExpireTime _expireTime;
-    private readonly IOptions<CityBaseConfiguration> _cityBaseConfiguration;
     private readonly IRepository<OrderObserve> _orderObserveRepository;
+    private readonly IOrderTerminateRepository _orderTerminateRepository;
 
-
-    public OrderApplication(
+	public OrderApplication(
         IOrderDomainService orderDomainService,
         IOrderRepository orderRepository,
         IWorkflowDomainService workflowDomainService,
@@ -138,12 +138,12 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         IMediator mediator,
         IRepository<OrderVisitDetail> orderVisitedDetailRepository,
         IOptionsSnapshot<AppConfiguration> appOptions,
-        IOptions<CityBaseConfiguration> cityBaseConfiguration,
         ISystemDicDataCacheManager sysDicDataCacheManager,
         Publisher publisher,
         ISessionContextProvider sessionContextProvider,
         IRepository<TranspondCityRawData> transpondCityRawDataRepository,
-        IRepository<OrderObserve> orderObserveRepository)
+        IRepository<OrderObserve> orderObserveRepository,
+        IOrderTerminateRepository orderTerminateRepository)
     {
         _orderDomainService = orderDomainService;
         _workflowDomainService = workflowDomainService;
@@ -177,9 +177,9 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         _publisher = publisher;
         _sessionContextProvider = sessionContextProvider;
         _transpondCityRawDataRepository = transpondCityRawDataRepository;
-        _cityBaseConfiguration = cityBaseConfiguration;
         _orderObserveRepository = orderObserveRepository;
-    }
+        _orderTerminateRepository = orderTerminateRepository;
+	}
 
     /// <summary>
     /// 更新工单办理期满时间(延期调用,其他不调用)
@@ -2268,7 +2268,6 @@ public class OrderApplication : IOrderApplication, IScopeDependency
 
 
     #region private
-
     /// <summary>
     /// 接受外部工单(除省平台)
     /// </summary>
@@ -2366,13 +2365,11 @@ public class OrderApplication : IOrderApplication, IScopeDependency
 
             //特提(撤回至发起)
             if (!string.IsNullOrEmpty(order.WorkflowId))
-            {
                 //await _workflowDomainService.RecallToStartStepAsync(order.WorkflowId, "省工单重派", current, order.Status >= EOrderStatus.Filed, cancellationToken);
                 await _workflowDomainService.RecallToCenterFirstToSendAsync(order.WorkflowId, "省工单重派", order.Status >= EOrderStatus.Filed,
                     order.ExpiredTime, cancellationToken);
-            }
+            //await _workflowDomainService.RecallToStartStepAsync(order.WorkflowId, "省工单重派", current, cancellationToken);
         }
-
         return _mapper.Map<AddOrderResponse>(order);
     }
 
@@ -2536,6 +2533,29 @@ public class OrderApplication : IOrderApplication, IScopeDependency
             .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.Order.No.Contains(dto.No!))
             .OrderByDescending(d => d.CreationTime);
     }
+    /// <summary>
+    /// 终止列表
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    public ISugarQueryable<OrderTerminate> OrderTerminateList(OrderTerminateListDto dto) 
+    {
+	    var handler = dto.AuditStatus is 2;
+		var isAdmin = _orderDomainService.IsCheckAdmin();
+		return _orderTerminateRepository.Queryable(hasHandled: handler, isAdmin: isAdmin)
+            .Includes(d => d.Order)
+            .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.Order.No!.Contains(dto.No!))
+            .WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Order.Title!.Contains(dto.Title!))
+            .WhereIF(dto.ApplyStartTime.HasValue && dto.ApplyEndTime.HasValue,
+                d => d.CreationTime >= dto.ApplyStartTime && d.CreationTime <= dto.ApplyEndTime)
+            //.WhereIF(dto.AuditStatus is 1 , d=>d.Status == ETerminateStatus.Approval || d.Status == ETerminateStatus.SendBack )
+            //.WhereIF(dto.AuditStatus is 2, d => d.Status == ETerminateStatus.End || dto.Status == ETerminateStatus.Refuse)
+			.WhereIF(dto.QueryType is 1, d => d.CreatorId == _sessionContextProvider.SessionContext.UserId)
+            .WhereIF(dto.Status.HasValue, d => d.Status == dto.Status)
+            .WhereIF(dto.StartTime.HasValue && dto.EndTime.HasValue, d => d.Order.StartTime >= dto.StartTime && d.Order.StartTime <= dto.EndTime)
+            .OrderByDescending(d => d.CreationTime);
+
+	}
+	#endregion
 
-    #endregion
 }

+ 42 - 0
src/Hotline.Application/Orders/OrderVisitApplication.cs

@@ -0,0 +1,42 @@
+using Hotline.Orders;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Tools;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace Hotline.Application.Orders;
+
+/// <summary>
+/// 回访服务
+/// </summary>
+public class OrderVisitApplication : IOrderVisitApplication, IScopeDependency
+{
+    private readonly IOrderVisitRepository _orderVisitRepository;
+
+    public OrderVisitApplication(IOrderVisitRepository orderVisitRepository)
+    {
+        _orderVisitRepository = orderVisitRepository;
+    }
+
+    public async Task<List<OrderVisitQuantityOutDto>> QueryOrderVisitQuantityAsync(QueryOrderVisitQuantity dto, CancellationToken cancellationToken)
+    {
+        var query = _orderVisitRepository
+            .Queryable()
+            .WhereIF(dto.EmployeeName.NotNullOrEmpty(), m => m.Employee.Name.Contains(dto.EmployeeName))
+            .Where(m => m.CreationTime >= dto.StartTime && m.CreationTime <= dto.EndTime && m.VisitType != null)
+            .GroupBy(m => m.EmployeeId)
+            .Select(m => new OrderVisitQuantityOutDto
+            {
+                EmployeeId = m.EmployeeId,
+                EmployeeName = m.Employee.Name,
+                CallVisitCount = SqlFunc.AggregateSum(SqlFunc.IIF(m.VisitType == EVisitType.CallVisit, 1, 0)), // 电话回访量
+                DefaultVisitCount = SqlFunc.AggregateSum(SqlFunc.IIF(m.VisitType == EVisitType.ArtificialVisit, 1, 0)), // 默认回访量(人工回访)
+                SmsVisitCount = SqlFunc.AggregateSum(SqlFunc.IIF(m.VisitType == EVisitType.SmsVisit, 1, 0)), // 短信回访量
+                // TotalVisitCount = SqlFunc.AggregateSum(SqlFunc.IIF(m.VisitType != null, 1, 0)) // 总回访量
+            });
+
+
+        return await query.ToListAsync(cancellationToken);
+    }
+}

+ 79 - 8
src/Hotline.Application/StatisticalReport/CallReport/CallReportApplicationBase.cs

@@ -15,6 +15,8 @@ using Hotline.Users;
 using XF.Domain.Repository;
 using Hotline.CallCenter.Tels;
 using Hotline.Share.Dtos;
+using Hotline.Share.Tools;
+using JiebaNet.Segmenter.Common;
 
 namespace Hotline.Application.StatisticalReport.CallReport;
 
@@ -35,11 +37,6 @@ public abstract class CallReportApplicationBase : ICallReportApplication
         _telRestRepository = telRestRepository;
     }
 
-    //public virtual async Task<PagedDto<TrCallDto>> GetCallDetailListAsync(GetCallListDto dto, CancellationToken cancellationToken)
-    //{
-    //    throw new NotImplementedException();
-    //}
-
     public virtual async Task<List<CallHotLineDto>> GetCallHotLineListAsync(BiQueryGateWayDto dto, CancellationToken requestAborted)
     {
         int noConnectByeTimes = _systemSettingCacheManager.NoConnectByeTimes;
@@ -78,10 +75,9 @@ public abstract class CallReportApplicationBase : ICallReportApplication
     public virtual async Task<List<QueryCallsDetailDto>> QueryCallsDetailAsync(BiQueryCallsDto dto)
     {
         //超时接通量
-        int CallInOverConnRingTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.CallInOverConnRingTime)?.SettingValue[0]);
+        int CallInOverConnRingTime = _systemSettingCacheManager.CallInOverConnRingTime;
         //坐席超时挂断时间
-        int SeatChaoTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.SeatChaoTime)?.SettingValue[0]);
-
+        int SeatChaoTime = _systemSettingCacheManager.SeatChaoTime;
         //未接秒挂时间
         int noConnectByeTimes = _systemSettingCacheManager.NoConnectByeTimes;
         //呼入有效时间
@@ -151,6 +147,39 @@ public abstract class CallReportApplicationBase : ICallReportApplication
         return await query.ToPagedListAsync(dto.PageIndex, dto.PageSize);
     }
 
+    public virtual async Task<List<QueryCallsDetailStatistics>> QueryCallsDetailStatisticsAsync(StartEndTimeDto dto, CancellationToken cancellationToken)
+    {
+        //超时接通量
+        int CallInOverConnRingTime = _systemSettingCacheManager.CallInOverConnRingTime;
+        //坐席超时挂断时间
+        int SeatChaoTime = _systemSettingCacheManager.SeatChaoTime;
+        //未接秒挂时间
+        int noConnectByeTimes = _systemSettingCacheManager.NoConnectByeTimes;
+        //呼入有效时间
+        int effectiveTimes = _systemSettingCacheManager.EffectiveTimes;
+        //接通秒挂时间
+        int connectByeTimes = _systemSettingCacheManager.ConnectByeTimes;
+
+        var callData = await _callNativeRepository.Queryable()
+                .Where(p => p.CreationTime >= dto.StartTime && p.CreationTime <= dto.EndTime)
+                 .GroupBy(p => p.CreationTime.ToString("yyyy-MM-dd"))
+                .Select(p => new QueryCallsDetailStatistics
+                {
+                    Date = p.CreationTime.ToString("yyyy-MM-dd"),
+                    InTotal = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.In, 1, 0)),//呼入总量
+                    InConnectionQuantity = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.In && p.AnsweredTime != null, 1, 0)),//呼入接通量
+                    NotAcceptedHang = SqlFunc.AggregateSum(SqlFunc.IIF(p.Duration == 0 && p.RingDuration <= noConnectByeTimes && p.RingDuration > 0 && p.Direction == ECallDirection.In, 1, 0)), //呼入队列挂断
+                    InNotAnswered = SqlFunc.AggregateSum(SqlFunc.IIF(p.Duration == 0 && p.TelNo != "0" && p.Direction == ECallDirection.In, 1, 0)), // 挂机量
+                    IvrByeCount = SqlFunc.AggregateSum(SqlFunc.IIF(p.Direction == ECallDirection.In && p.BeginIvrTime.HasValue && !p.BeginQueueTime.HasValue && !p.BeginRingTime.HasValue && p.AnsweredTime == null, 1, 0)), //IVR挂断
+                    OutConnectionQuantity = SqlFunc.AggregateSum(SqlFunc.IIF(p.AnsweredTime != null && p.Direction == ECallDirection.Out, 1, 0)), // 呼出接通量
+                    OutNotAnswered = SqlFunc.AggregateSum(SqlFunc.IIF(p.AnsweredTime == null && p.Direction == ECallDirection.Out, 1, 0)), // 呼出未接量
+                })
+                .OrderBy(p => p.Date)
+                .ToListAsync(cancellationToken);
+
+        return callData;
+    }
+
     public virtual async Task<List<QueryCallsDetailDto>> QueryCallsHourDetailAsync(BiQueryCallsDto dto, CancellationToken cancellationToken)
     {
         //超时接通量
@@ -166,6 +195,48 @@ public abstract class CallReportApplicationBase : ICallReportApplication
         return await _callNativeRepository.QueryCallsHourDetail(dto.StartTime.Value, dto.EndTime.Value, noConnectByeTimes, effectiveTimes, connectByeTimes, CallInOverConnRingTime, SeatChaoTime, dto.Line);
     }
 
+    public virtual async Task<(int, List<QueryCallsStatisticsDetailOutDto>)> QueryCallsStatisticsDetailAsync(QueryCallsStatisticsDetailInDto dto, CancellationToken cancellationToken)
+    {
+        dto.FieldName ??= string.Empty;
+        dto.FieldName = dto.FieldName.ToLower();
+        //超时接通量
+        int CallInOverConnRingTime = _systemSettingCacheManager.CallInOverConnRingTime;
+        //坐席超时挂断时间
+        int SeatChaoTime = _systemSettingCacheManager.SeatChaoTime;
+        //未接秒挂时间
+        int noConnectByeTimes = _systemSettingCacheManager.NoConnectByeTimes;
+        //呼入有效时间
+        int effectiveTimes = _systemSettingCacheManager.EffectiveTimes;
+        //接通秒挂时间
+        int connectByeTimes = _systemSettingCacheManager.ConnectByeTimes;
+        var query = _callNativeRepository.Queryable(includeDeleted: true)
+            .LeftJoin<Order>((c, o) => c.Id == o.CallId)
+            .WhereIF(dto.OrderNo.NotNullOrEmpty(), (c, o) => o.No == dto.OrderNo)
+            .WhereIF(dto.FromNo.NotNullOrEmpty(), (c, o) => c.FromNo == dto.FromNo)
+            .WhereIF(dto.ToNo.NotNullOrEmpty(), (c, o) => c.ToNo == dto.ToNo)
+            .WhereIF(dto.TelNo.NotNullOrEmpty(), (c, o) => c.TelNo == dto.TelNo)
+            .WhereIF(dto.EndBy.IsNotNull(), (c, o) => c.EndBy == dto.EndBy)
+            .Where((c, o) => c.CreationTime >= dto.StartTime && c.CreationTime <= dto.EndTime);
+
+        if (dto.FieldName == "intotal") // 呼入总量
+            query = query.Where((c, o) => c.Direction == ECallDirection.In);
+        if (dto.FieldName == "inconnectionquantity") // 呼入接通
+            query = query.Where((c, o) => c.Direction == ECallDirection.In && c.AnsweredTime != null);
+        if (dto.FieldName == "innotanswered") // 挂机量
+            query = query.Where((c, o) => c.Direction == ECallDirection.In && c.Duration ==0 && c.TelNo != "0");
+        if (dto.FieldName == "notacceptedhang") // 呼入队列挂断
+            query = query.Where((c, o) => c.Duration == 0 && c.RingDuration <= noConnectByeTimes && c.RingDuration > 0 && c.Direction == ECallDirection.In);
+        if (dto.FieldName == "ivrbyecount") // 呼入IVR挂断
+            query = query.Where((c, o) => c.Direction == ECallDirection.In && c.BeginIvrTime.HasValue && !c.BeginQueueTime.HasValue && !c.BeginRingTime.HasValue && c.AnsweredTime == null);
+        if (dto.FieldName == "outconnectionquantity") // 呼出接通量
+            query = query.Where((c, o) => c.AnsweredTime != null && c.Direction == ECallDirection.Out);
+        if (dto.FieldName == "outnotanswered") // 呼出未接通
+            query = query.Where((c, o) => c.AnsweredTime == null && c.Direction == ECallDirection.Out);
+        return await query
+            .Select<QueryCallsStatisticsDetailOutDto>()
+            .ToPagedListAsync(dto.PageIndex, dto.PageSize, cancellationToken);
+    }
+
     /// <summary>
     /// 坐席话务统计分析
     /// </summary>

+ 9 - 0
src/Hotline.Application/StatisticalReport/ICallReportApplication.cs

@@ -37,6 +37,15 @@ namespace Hotline.Application.StatisticalReport
         Task<List<TrCallHourDto>> GetCallHourListAsync(BiQueryHourCallDto dto, CancellationToken cancellationToken);
         Task<TotalData<BiSeatSwitchDto>> GetCallListAsync(QueryCallListDto dto, CancellationToken cancellationToken);
         Task<List<CallHotLineDto>> GetCallHotLineListAsync(BiQueryGateWayDto dto, CancellationToken cancellationToken);
+        Task<List<QueryCallsDetailStatistics>> QueryCallsDetailStatisticsAsync(StartEndTimeDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 话务日期统计详情
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        Task<(int, List<QueryCallsStatisticsDetailOutDto>)> QueryCallsStatisticsDetailAsync(QueryCallsStatisticsDetailInDto dto, CancellationToken cancellationToken);
         // Task<PagedDto<TrCallDto>> GetCallDetailListAsync(GetCallListDto dto, CancellationToken cancellationToken);
     }
 }

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

@@ -30,6 +30,7 @@ using Mapster;
 using MapsterMapper;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
 using XF.Domain.Exceptions;
@@ -265,6 +266,8 @@ namespace Hotline.Application.Subscribers
                 }
             };
             await _orderRevokeRepository.AddAsync(orderRevoke, cancellationToken);
+            //var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            //CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
 
             //计算order 期满时间
             ExpiredTimeWithConfig expiredTimeConfig;
@@ -294,7 +297,7 @@ namespace Hotline.Application.Subscribers
             }
             else
             {
-                await _workflowDomainService.RecallToCenterFirstToSendAsync(order.WorkflowId, dto.Opinion, 
+                await _workflowDomainService.RecallToCenterFirstToSendAsync(order.WorkflowId, dto.Opinion,
                     order.Status >= EOrderStatus.Filed, order.ExpiredTime, cancellationToken);
             }
         }
@@ -456,6 +459,8 @@ namespace Hotline.Application.Subscribers
                             x.Status == EScreenStatus.Approval)
                 .FirstAsync(cancellationToken);
 
+            //var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            //CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
             SessionContextCreator.CreateSessionContext(_sessionContextProvider, dto.Source);
             await _workflowApplication.HandleToEndAsync(orderScreen.WorkflowId, "省上推送甄别结果", null,
                 dto.ProvinceScreenResult.AuditResult
@@ -702,6 +707,9 @@ namespace Hotline.Application.Subscribers
                         orderDelay.FileJson = await _fileRepository.AddFileAsync(dto.Files, orderDelay.Id, orderDelay.WorkflowId, cancellationToken);
                     await _orderDelayRepository.UpdateAsync(orderDelay, cancellationToken);
 
+                    //var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+                    //CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+
                     SessionContextCreator.CreateSessionContext(_sessionContextProvider, dto.Source);
                     await _workflowApplication.HandleToEndAsync(orderDelay.WorkflowId, dto.Opinion, dto.Files,
                         dto.IsPass ? EReviewResult.Approval : EReviewResult.Failed, cancellationToken);
@@ -728,7 +736,9 @@ namespace Hotline.Application.Subscribers
             //        cancellationToken);
             //    await _orderRepository.FileAsync(order, cancellationToken);
             //}
-            
+            //var setting = _systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            //CityBaseConfiguration cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+
             switch (dto.FinishType)
             {
                 case "0":

+ 80 - 3
src/Hotline.Application/Systems/BaseDataApplication.cs

@@ -1,29 +1,42 @@
-using Hotline.Caching.Interfaces;
+using DocumentFormat.OpenXml.Wordprocessing;
+using Hotline.Caching.Interfaces;
+using Hotline.Repository.SqlSugar.System;
 using Hotline.Settings;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Article;
+using Hotline.Share.Enums.CallCenter;
+using Hotline.Share.Enums.KnowledgeBase;
 using Hotline.Share.Enums.Order;
+using J2N.Collections.ObjectModel;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Globalization;
 using System.Linq;
 using System.Reflection;
 using System.Text;
 using System.Threading.Tasks;
+using XF.Domain.Authentications;
 using XF.Domain.Dependency;
 using XF.Utility.EnumExtensions;
+using static Lucene.Net.Util.Fst.Util;
 
 namespace Hotline.Application.Systems;
 public class BaseDataApplication : IScopeDependency
 {
     private readonly ISystemDicDataCacheManager _sysDicDataCacheManager;
+    private readonly ISystemOrganizeRepository _systemOrganizeRepository;
 
-    public BaseDataApplication(ISystemDicDataCacheManager sysDicDataCacheManager)
+    public BaseDataApplication(ISystemDicDataCacheManager sysDicDataCacheManager, ISystemOrganizeRepository systemOrganizeRepository)
     {
         _sysDicDataCacheManager = sysDicDataCacheManager;
+        _systemOrganizeRepository = systemOrganizeRepository;
     }
 
     private ConcurrentDictionary<string, dynamic> _baseData = new ConcurrentDictionary<string, dynamic>();
+
+
     //private ConcurrentDictionary<string, int> _baseType = new ConcurrentDictionary<string, int>();
     //private ConcurrentDictionary<string, int> _enumType = new ConcurrentDictionary<string, int>();
     #region 内部方法
@@ -61,7 +74,12 @@ public class BaseDataApplication : IScopeDependency
 
     public Dictionary<string, dynamic> Build()
     {
-        return new Dictionary<string, dynamic>(_baseData);
+        var newDict = _baseData.ToDictionary(
+            entry => char.ToLower(entry.Key[0], CultureInfo.InvariantCulture) + entry.Key.Substring(1),
+            entry => entry.Value
+        );
+
+        return newDict;
     }
 
     public BaseDataApplication VoiceEvaluate()
@@ -111,4 +129,63 @@ public class BaseDataApplication : IScopeDependency
         Add(SysDicTypeConsts.CallForwardingType);
         return this;
     }
+    public BaseDataApplication EndBy()
+    {
+        Add(typeof(EEndBy));
+        return this;
+    }
+
+    public BaseDataApplication OrgsOptions(ISessionContext sessionContext)
+    {
+        IReadOnlyList<SystemOrganize> items;
+        if (sessionContext.OrgIsCenter)
+            items = _systemOrganizeRepository.GetOrgJson().GetAwaiter().GetResult();
+        else
+            items = _systemOrganizeRepository.GetOrgJsonForUser(sessionContext.RequiredOrgId).GetAwaiter().GetResult();
+        _baseData.TryAdd("OrgsOptions", items);
+        return this;
+    }
+
+    public BaseDataApplication AttitudeType()
+    {
+        Add(typeof(EAttitudeType));
+        return this;
+    }
+
+    public BaseDataApplication FileType(EFileType ignoreFileType = EFileType.None)
+    {
+        var items = EnumExts.GetDescriptions<EFileType>();
+        var filteredDictionary = items
+             .Where(kvp => (ignoreFileType & (EFileType)kvp.Key) == 0)
+             .ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
+             .ToList();
+        _baseData.TryAdd("FileType", filteredDictionary);
+
+        return this;
+    }
+
+    public BaseDataApplication Add(string name, List<KeyValuePair<int, string>> items)
+    {
+        _baseData.TryAdd(name, items);
+        return this;
+    }
+
+    public BaseDataApplication SourceChannel()
+    {
+        Add(SysDicTypeConsts.SourceChannel);
+        return this;
+    }
+
+    public BaseDataApplication KnowledgeRetrievalType(int[] ignoreFileType)
+    {
+        var items = EnumExts.GetDescriptions<EKnowledgeRetrievalType>();
+        _baseData.TryAdd("KnowledgeRetrievalType", items.Where(m => !ignoreFileType.Contains(m.Key)).ToList());
+        return this;
+    }
+
+    public BaseDataApplication KnowledgeHotWordType()
+    {
+        Add(typeof(EKnowledgeHotWordType));
+        return this;
+    }
 }

+ 144 - 0
src/Hotline.Application/Tools/RSA.cs

@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Exceptions;
+
+namespace Hotline.Application.Tools
+{
+    /// <summary>
+    /// 加密、解密
+    /// </summary>
+    public class RSA
+    {
+        /// <summary>
+        /// RSA加密
+        /// </summary>
+        /// <param name="Data">原文</param>
+        /// <param name="PublicKeyString">公钥</param>
+        /// <param name="KeyType">密钥类型XML/PEM</param>
+        /// <returns></returns>
+        public static string RSAEncrypt(string Data, string PublicKeyString, string KeyType)
+        {
+            byte[] data = Encoding.GetEncoding("UTF-8").GetBytes(Data);
+            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
+            switch (KeyType)
+            {
+                case "XML":
+                    rsa.FromXmlString(PublicKeyString);
+                    break;
+                case "PEM":
+                    rsa = RSA_PEM.FromPEM(PublicKeyString);
+                    break;
+                default:
+                    throw UserFriendlyException.SameMessage("不支持的密钥类型");
+            }
+            //加密块最大长度限制,如果加密数据的长度超过 秘钥长度/8-11,会引发长度不正确的异常,所以进行数据的分块加密
+            int MaxBlockSize = rsa.KeySize / 8 - 11;
+            //正常长度
+            if (data.Length <= MaxBlockSize)
+            {
+                byte[] hashvalueEcy = rsa.Encrypt(data, false); //加密
+                return System.Convert.ToBase64String(hashvalueEcy);
+            }
+            //长度超过正常值
+            else
+            {
+                using (MemoryStream PlaiStream = new MemoryStream(data))
+                using (MemoryStream CrypStream = new MemoryStream())
+                {
+                    Byte[] Buffer = new Byte[MaxBlockSize];
+                    int BlockSize = PlaiStream.Read(Buffer, 0, MaxBlockSize);
+                    while (BlockSize > 0)
+                    {
+                        Byte[] ToEncrypt = new Byte[BlockSize];
+                        Array.Copy(Buffer, 0, ToEncrypt, 0, BlockSize);
+
+                        Byte[] Cryptograph = rsa.Encrypt(ToEncrypt, false);
+                        CrypStream.Write(Cryptograph, 0, Cryptograph.Length);
+                        BlockSize = PlaiStream.Read(Buffer, 0, MaxBlockSize);
+                    }
+                    return System.Convert.ToBase64String(CrypStream.ToArray(), Base64FormattingOptions.None);
+                }
+            }
+        }
+
+        /// <summary>
+        /// RSA解密
+        /// </summary>
+        /// <param name="Data">密文</param>
+        /// <param name="PrivateKeyString">私钥</param>
+        /// <param name="KeyType">密钥类型XML/PEM</param>
+        /// <returns></returns>
+        public static string RSADecrypt(string Data, string PrivateKeyString, string KeyType)
+        {
+            // 把+号,替换回来
+            Data = Data.Replace("%2B", "+");
+
+            // 补全密文
+            Data = CorrectionCiphertext(Data);
+
+            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
+            switch (KeyType)
+            {
+                case "XML":
+                    rsa.FromXmlString(PrivateKeyString);
+                    break;
+                case "PEM":
+                    rsa = RSA_PEM.FromPEM(PrivateKeyString);
+                    break;
+                default:
+                    throw UserFriendlyException.SameMessage("不支持的密钥类型");
+            }
+            int MaxBlockSize = rsa.KeySize / 8;    //解密块最大长度限制
+            //正常解密
+            if (Data.Length <= MaxBlockSize)
+            {
+                byte[] hashvalueDcy = rsa.Decrypt(System.Convert.FromBase64String(Data), false);//解密
+                return Encoding.GetEncoding("UTF-8").GetString(hashvalueDcy);
+            }
+            //分段解密
+            else
+            {
+                using (MemoryStream CrypStream = new MemoryStream(System.Convert.FromBase64String(Data)))
+                using (MemoryStream PlaiStream = new MemoryStream())
+                {
+                    Byte[] Buffer = new Byte[MaxBlockSize];
+                    int BlockSize = CrypStream.Read(Buffer, 0, MaxBlockSize);
+
+                    while (BlockSize > 0)
+                    {
+                        Byte[] ToDecrypt = new Byte[BlockSize];
+                        Array.Copy(Buffer, 0, ToDecrypt, 0, BlockSize);
+
+                        Byte[] Plaintext = rsa.Decrypt(ToDecrypt, false);
+                        PlaiStream.Write(Plaintext, 0, Plaintext.Length);
+                        BlockSize = CrypStream.Read(Buffer, 0, MaxBlockSize);
+                    }
+                    string output = Encoding.GetEncoding("UTF-8").GetString(PlaiStream.ToArray());
+                    return output;
+                }
+            }
+        }
+
+        /// <summary>
+        /// 补全密文
+        /// </summary>
+        /// <param name="strCiphertext">密文</param>
+        /// <param name="keySize">秘钥长度</param>
+        /// <returns>补全后的密文</returns>
+        private static string CorrectionCiphertext(string strCiphertext, int keySize = 1024)
+        {
+            int ciphertextLength = keySize / 8;
+            byte[] data = Convert.FromBase64String(strCiphertext);
+            var newData = new List<byte>(data);
+            while (newData.Count < ciphertextLength)
+            {
+                newData.Insert(0, 0x00);
+            }
+            return Convert.ToBase64String(newData.ToArray());
+        }
+    }
+}

+ 71 - 0
src/Hotline.Application/Tools/RSA_Create.cs

@@ -0,0 +1,71 @@
+
+using System.Security.Cryptography;
+
+namespace Hotline.Application.Tools
+{
+    /// <summary>
+    /// 公钥、私钥创建
+    /// </summary>
+    public class RSA_Create
+    {
+        /// <summary>
+        /// 登陆公钥
+        /// </summary>
+        public const string RSA_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGC7ZYMOLX8+Ze4AA0VrMcy/SbMhSGuTfOk5AWWekdSOZfb/OZ8G+oup5b/FCAxDmRolK0cK6nGgQ6QglMBB1qrHmfvyLR6Ock1MhUT8++T2lODRgM3xEtdJnFvcKIyte5eiSfJMby/7avHX0cho4OupRo1WK9veADyX45zmaMpQIDAQAB-----END PUBLIC KEY-----";
+
+        /// <summary>
+        /// 登陆私钥
+        /// </summary>
+        public const string RSA_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----MIICXAIBAAKBgQDGC7ZYMOLX8+Ze4AA0VrMcy/SbMhSGuTfOk5AWWekdSOZfb/OZ8G+oup5b/FCAxDmRolK0cK6nGgQ6QglMBB1qrHmfvyLR6Ock1MhUT8++T2lODRgM3xEtdJnFvcKIyte5eiSfJMby/7avHX0cho4OupRo1WK9veADyX45zmaMpQIDAQABAoGAELhaa7Ws4SiRUX895S1Qr+S0QQoIW5t1j7Y7mvd6S+Okt3f/LAb1llwzFHcH4OcwKRB4RQ2iYS88HiMHH+jVBs9uKtaVUGoTyly1TSBgNg7mtSh2JrGWPMAmVYYJKrIiwqrZh0783fQ0oYjSHoOGFaevVSiWdTgFCXkyJzXACKECQQDxaP8yO/YJ0k3oyYso0rzqrkt6NC17wl34VqGQ95QkJl40h2f0CXh458wPp1BgBQz9gcykri0V0MsdNK6a8TStAkEA0gPMbIHbNJ8FcbmToqWKBGlkDWSg/BZk9Xrov/wyc3QDtHZ7aV0m0x9/X0hXVzpAKiBnuehBVM/PxeQDaMs+2QJAT/hmQW/Xi11dbbYGIVneNyfR3QOX5Z13n6KjFvDM5ETJhEEu0Iqqm/K9iJn2aDiEGx56ejswuD9Yaa5gAPfSaQJBAIKuMmnic9BOk0isvR0b/MG6ArQCxdmW5cOKc4HjIFGPAy8r+qcZe6srkkpr7U0txfdwqpqX1u87UIJG24g90CECQBZTY2pGT6NVKFJ0cK7Qjwg1ekYMMPFGHjcf0jwI6nwZav7t46qSvSLSAXFtFsY7zTo3komvifKz8noLnA2qpBQ=-----END PRIVATE KEY-----";
+
+        /// <summary>
+        /// 取得私钥和公钥 XML 格式,返回数组第一个是私钥,第二个是公钥.
+        /// </summary>
+        /// <param name="size">密钥长度,默认1024,可以为2048</param>
+        /// <returns></returns>
+        public static string[] CreateXmlKey(int size = 1024)
+        {
+            //密钥格式要生成pkcs#1格式的  而不是pkcs#8格式的
+            RSACryptoServiceProvider sp = new RSACryptoServiceProvider(size);
+            string privateKey = sp.ToXmlString(true);//private key
+            string publicKey = sp.ToXmlString(false);//public  key
+            return new string[] { privateKey, publicKey };
+        }
+
+        /// <summary>
+        /// 取得私钥和公钥 CspBlob 格式,返回数组第一个是私钥,第二个是公钥.
+        /// </summary>
+        /// <param name="size"></param>
+        /// <returns></returns>
+        public static string[] CreateCspBlobKey(int size = 1024)
+        {
+            //密钥格式要生成pkcs#1格式的  而不是pkcs#8格式的
+            RSACryptoServiceProvider sp = new RSACryptoServiceProvider(size);
+            string privateKey = System.Convert.ToBase64String(sp.ExportCspBlob(true));//private key
+            string publicKey = System.Convert.ToBase64String(sp.ExportCspBlob(false));//public  key 
+
+            return new string[] { privateKey, publicKey };
+        }
+        /// <summary>
+        /// 导出PEM PKCS#1格式密钥对,返回数组第一个是私钥,第二个是公钥.
+        /// </summary>
+        public static string[] CreateKey_PEM_PKCS1(int size = 1024)
+        {
+            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(size);
+            string privateKey = RSA_PEM.ToPEM(rsa, false, false);
+            string publicKey = RSA_PEM.ToPEM(rsa, true, false);
+            return new string[] { privateKey, publicKey };
+        }
+
+        /// <summary>
+        /// 导出PEM PKCS#8格式密钥对,返回数组第一个是私钥,第二个是公钥.
+        /// </summary>
+        public static string[] CreateKey_PEM_PKCS8(int size = 1024, bool convertToPublic = false)
+        {
+            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(size);
+            string privateKey = RSA_PEM.ToPEM(rsa, false, true);
+            string publicKey = RSA_PEM.ToPEM(rsa, true, true);
+            return new string[] { privateKey, publicKey };
+        }
+    }
+}

+ 318 - 0
src/Hotline.Application/Tools/RSA_PEM.cs

@@ -0,0 +1,318 @@
+using System.Security.Cryptography;
+using System.Text.RegularExpressions;
+
+
+namespace Hotline.Application.Tools
+{
+    public class RSA_PEM
+    {
+        public static RSACryptoServiceProvider FromPEM(string pem)
+        {
+            var rsaParams = new CspParameters();
+            rsaParams.Flags = CspProviderFlags.UseMachineKeyStore;
+            var rsa = new RSACryptoServiceProvider();
+
+            var param = new RSAParameters();
+
+            var base64 = _PEMCode.Replace(pem, "");
+            var data = RSA_Unit.Base64DecodeBytes(base64);
+            if (data == null)
+            {
+                throw new Exception("PEM内容无效");
+            }
+            var idx = 0;
+
+            //读取长度
+            Func<byte, int> readLen = (first) =>
+            {
+                if (data[idx] == first)
+                {
+                    idx++;
+                    if (data[idx] == 0x81)
+                    {
+                        idx++;
+                        return data[idx++];
+                    }
+                    else if (data[idx] == 0x82)
+                    {
+                        idx++;
+                        return (((int)data[idx++]) << 8) + data[idx++];
+                    }
+                    else if (data[idx] < 0x80)
+                    {
+                        return data[idx++];
+                    }
+                }
+                throw new Exception("PEM未能提取到数据");
+            };
+            //读取块数据
+            Func<byte[]> readBlock = () =>
+            {
+                var len = readLen(0x02);
+                if (data[idx] == 0x00)
+                {
+                    idx++;
+                    len--;
+                }
+                var val = data.sub(idx, len);
+                idx += len;
+                return val;
+            };
+            //比较data从idx位置开始是否是byts内容
+            Func<byte[], bool> eq = (byts) =>
+            {
+                for (var i = 0; i < byts.Length; i++, idx++)
+                {
+                    if (idx >= data.Length)
+                    {
+                        return false;
+                    }
+                    if (byts[i] != data[idx])
+                    {
+                        return false;
+                    }
+                }
+                return true;
+            };
+
+
+
+
+            if (pem.Contains("PUBLIC KEY"))
+            {
+                /****使用公钥****/
+                //读取数据总长度
+                readLen(0x30);
+                if (!eq(_SeqOID))
+                {
+                    throw new Exception("PEM未知格式");
+                }
+                //读取1长度
+                readLen(0x03);
+                idx++;//跳过0x00
+                      //读取2长度
+                readLen(0x30);
+
+                //Modulus
+                param.Modulus = readBlock();
+
+                //Exponent
+                param.Exponent = readBlock();
+            }
+            else if (pem.Contains("PRIVATE KEY"))
+            {
+                /****使用私钥****/
+                //读取数据总长度
+                readLen(0x30);
+
+                //读取版本号
+                if (!eq(_Ver))
+                {
+                    throw new Exception("PEM未知版本");
+                }
+
+                //检测PKCS8
+                var idx2 = idx;
+                if (eq(_SeqOID))
+                {
+                    //读取1长度
+                    readLen(0x04);
+                    //读取2长度
+                    readLen(0x30);
+
+                    //读取版本号
+                    if (!eq(_Ver))
+                    {
+                        throw new Exception("PEM版本无效");
+                    }
+                }
+                else
+                {
+                    idx = idx2;
+                }
+
+                //读取数据
+                param.Modulus = readBlock();
+                param.Exponent = readBlock();
+                param.D = readBlock();
+                param.P = readBlock();
+                param.Q = readBlock();
+                param.DP = readBlock();
+                param.DQ = readBlock();
+                param.InverseQ = readBlock();
+            }
+            else
+            {
+                throw new Exception("pem需要BEGIN END标头");
+            }
+
+            rsa.ImportParameters(param);
+            return rsa;
+        }
+        static private Regex _PEMCode = new Regex(@"--+.+?--+|\s+");
+        static private byte[] _SeqOID = new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
+        static private byte[] _Ver = new byte[] { 0x02, 0x01, 0x00 };
+
+
+        /// <summary>
+        /// 将RSA中的密钥对转换成PEM格式,usePKCS8=false时返回PKCS#1格式,否则返回PKCS#8格式,如果convertToPublic含私钥的RSA将只返回公钥,仅含公钥的RSA不受影响
+        /// </summary>
+        public static string ToPEM(RSACryptoServiceProvider rsa, bool convertToPublic, bool usePKCS8)
+        {
+            //https://www.jianshu.com/p/25803dd9527d
+            //https://www.cnblogs.com/ylz8401/p/8443819.html
+            //https://blog.csdn.net/jiayanhui2877/article/details/47187077
+            //https://blog.csdn.net/xuanshao_/article/details/51679824
+            //https://blog.csdn.net/xuanshao_/article/details/51672547
+
+            var ms = new MemoryStream();
+            //写入一个长度字节码
+            Action<int> writeLenByte = (len) =>
+            {
+                if (len < 0x80)
+                {
+                    ms.WriteByte((byte)len);
+                }
+                else if (len <= 0xff)
+                {
+                    ms.WriteByte(0x81);
+                    ms.WriteByte((byte)len);
+                }
+                else
+                {
+                    ms.WriteByte(0x82);
+                    ms.WriteByte((byte)(len >> 8 & 0xff));
+                    ms.WriteByte((byte)(len & 0xff));
+                }
+            };
+            //写入一块数据
+            Action<byte[]> writeBlock = (byts) =>
+            {
+                var addZero = (byts[0] >> 4) >= 0x8;
+                ms.WriteByte(0x02);
+                var len = byts.Length + (addZero ? 1 : 0);
+                writeLenByte(len);
+
+                if (addZero)
+                {
+                    ms.WriteByte(0x00);
+                }
+                ms.Write(byts, 0, byts.Length);
+            };
+            //根据后续内容长度写入长度数据
+            Func<int, byte[], byte[]> writeLen = (index, byts) =>
+            {
+                var len = byts.Length - index;
+
+                ms.SetLength(0);
+                ms.Write(byts, 0, index);
+                writeLenByte(len);
+                ms.Write(byts, index, len);
+
+                return ms.ToArray();
+            };
+
+
+            if (rsa.PublicOnly || convertToPublic)
+            {
+                /****生成公钥****/
+                var param = rsa.ExportParameters(false);
+
+
+                //写入总字节数,不含本段长度,额外需要24字节的头,后续计算好填入
+                ms.WriteByte(0x30);
+                var index1 = (int)ms.Length;
+
+                //固定内容
+                // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
+                ms.writeAll(_SeqOID);
+
+                //从0x00开始的后续长度
+                ms.WriteByte(0x03);
+                var index2 = (int)ms.Length;
+                ms.WriteByte(0x00);
+
+                //后续内容长度
+                ms.WriteByte(0x30);
+                var index3 = (int)ms.Length;
+
+                //写入Modulus
+                writeBlock(param.Modulus);
+
+                //写入Exponent
+                writeBlock(param.Exponent);
+
+
+                //计算空缺的长度
+                var byts = ms.ToArray();
+
+                byts = writeLen(index3, byts);
+                byts = writeLen(index2, byts);
+                byts = writeLen(index1, byts);
+
+
+                return "-----BEGIN PUBLIC KEY-----\n" + RSA_Unit.TextBreak(RSA_Unit.Base64EncodeBytes(byts), 64) + "\n-----END PUBLIC KEY-----";
+            }
+            else
+            {
+                /****生成私钥****/
+                var param = rsa.ExportParameters(true);
+
+                //写入总字节数,后续写入
+                ms.WriteByte(0x30);
+                int index1 = (int)ms.Length;
+
+                //写入版本号
+                ms.writeAll(_Ver);
+
+                //PKCS8 多一段数据
+                int index2 = -1, index3 = -1;
+                if (usePKCS8)
+                {
+                    //固定内容
+                    ms.writeAll(_SeqOID);
+
+                    //后续内容长度
+                    ms.WriteByte(0x04);
+                    index2 = (int)ms.Length;
+
+                    //后续内容长度
+                    ms.WriteByte(0x30);
+                    index3 = (int)ms.Length;
+
+                    //写入版本号
+                    ms.writeAll(_Ver);
+                }
+
+                //写入数据
+                writeBlock(param.Modulus);
+                writeBlock(param.Exponent);
+                writeBlock(param.D);
+                writeBlock(param.P);
+                writeBlock(param.Q);
+                writeBlock(param.DP);
+                writeBlock(param.DQ);
+                writeBlock(param.InverseQ);
+
+
+                //计算空缺的长度
+                var byts = ms.ToArray();
+
+                if (index2 != -1)
+                {
+                    byts = writeLen(index3, byts);
+                    byts = writeLen(index2, byts);
+                }
+                byts = writeLen(index1, byts);
+
+
+                var flag = " PRIVATE KEY";
+                if (!usePKCS8)
+                {
+                    flag = " RSA" + flag;
+                }
+                return "-----BEGIN" + flag + "-----\n" + RSA_Unit.TextBreak(RSA_Unit.Base64EncodeBytes(byts), 64) + "\n-----END" + flag + "-----";
+            }
+        }
+    }
+}

+ 74 - 0
src/Hotline.Application/Tools/RSA_Unit.cs

@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Application.Tools
+{
+    public class RSA_Unit
+    {
+        public static string Base64EncodeBytes(byte[] byts)
+        {
+            return System.Convert.ToBase64String(byts);
+        }
+        public static byte[] Base64DecodeBytes(string str)
+        {
+            try
+            {
+                return System.Convert.FromBase64String(str);
+            }
+            catch
+            {
+                return null;
+            }
+        }
+        /// <summary>
+        /// 把字符串按每行多少个字断行
+        /// </summary>
+        public static string TextBreak(string text, int line)
+        {
+            var idx = 0;
+            var len = text.Length;
+            var str = new StringBuilder();
+            while (idx < len)
+            {
+                if (idx > 0)
+                {
+                    str.Append('\n');
+                }
+                if (idx + line >= len)
+                {
+                    str.Append(text.Substring(idx));
+                }
+                else
+                {
+                    str.Append(text.Substring(idx, line));
+                }
+                idx += line;
+            }
+            return str.ToString();
+        }
+
+    }
+
+    public static class Extensions
+    {
+        /// <summary>
+        /// 从数组start开始到指定长度复制一份
+        /// </summary>
+        public static T[] sub<T>(this T[] arr, int start, int count)
+        {
+            T[] val = new T[count];
+            for (var i = 0; i < count; i++)
+            {
+                val[i] = arr[start + i];
+            }
+            return val;
+        }
+        static public void writeAll(this Stream stream, byte[] byts)
+        {
+            stream.Write(byts, 0, byts.Length);
+        }
+    }
+}

+ 33 - 0
src/Hotline.Application/Tools/StreamExtensions.cs

@@ -0,0 +1,33 @@
+using Hotline.Share.Enums.Article;
+using Hotline.Share.Tools;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Hotline.Application.Tools;
+public static class StreamExtensions
+{
+    public static FileStreamResult GetPDFFile(this Stream stream, string fileName = "")
+    {
+        return stream.GetFileStreamResult(EFileType.pdf, fileName);
+    }
+
+    public static FileStreamResult GetExcelFile(this Stream stream, string fileName = "")
+    {
+        return stream.GetFileStreamResult(EFileType.excel, fileName);
+    }
+
+    public static FileStreamResult GetWordFile(this Stream stream,  string fileName = "")
+    {
+        return stream.GetFileStreamResult(EFileType.word, fileName);
+    }
+
+    public static FileStreamResult GetFileStreamResult(this Stream stream, EFileType fileType, string fileName = "")
+    {
+        var tail = DateTime.Now.ToString("yyyyMMddhhmmss") + fileType.GetFileExtension();
+        fileName = fileName.IsNullOrEmpty() ? tail : $"{fileName}_{tail}";
+        return new FileStreamResult(stream, fileType.GetContentType())
+        {
+            FileDownloadName = fileName
+        };
+    }
+
+}

+ 23 - 0
src/Hotline.Application/Tools/StringExtensions.cs

@@ -0,0 +1,23 @@
+using Hotline.Application.ExportWord;
+using Hotline.Share.Enums.Article;
+
+namespace Hotline.Application.Tools;
+public static class StringExtensions
+{
+    public static Stream HtmlToWord(this string value)
+    {
+        return WordHelper.ConvertHtmlToWord(value);
+    }
+
+    public static Stream HtmlToPDF(this string value)
+    {
+        return WordHelper.ConvertHtmlToPdf(value);
+    }
+
+    public static Stream HtmlToStream(this string value, EFileType fileType)
+    {
+        if (fileType == EFileType.pdf) return value.HtmlToPDF();
+        if (fileType == EFileType.word) return value.HtmlToWord();
+        throw new NotImplementedException($"无效的 fileType 入参: {fileType}");
+    }
+}

+ 16 - 0
src/Hotline.Application/Users/IUserApplication.cs

@@ -0,0 +1,16 @@
+using Hotline.Share.Dtos.Users;
+using Hotline.Users;
+using SqlSugar;
+
+namespace Hotline.Application.Users
+{
+    public interface IUserApplication
+    {
+        /// <summary>
+        /// 查询用户数据
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<User> QueryPaged(UserPagedDto dto);
+    }
+}

+ 49 - 0
src/Hotline.Application/Users/UserApplication.cs

@@ -0,0 +1,49 @@
+using Hotline.SeedData;
+using Hotline.Share.Dtos.Users;
+using Hotline.Share.Enums.Identity;
+using Hotline.Users;
+using SqlSugar;
+using XF.Domain.Authentications;
+using XF.Domain.Dependency;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Users
+{
+    public class UserApplication : IUserApplication, IScopeDependency
+    {
+        private readonly IRepository<User> _userRepository;
+        private readonly ISessionContext _sessionContext;
+
+        public UserApplication(IRepository<User> userRepository, ISessionContext sessionContext)
+        {
+            _userRepository = userRepository;
+            _sessionContext = sessionContext;
+        }
+
+        /// <summary>
+        /// 查询用户数据
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        public ISugarQueryable<User> QueryPaged(UserPagedDto dto)
+        {
+            return _userRepository.Queryable(includeDeleted: true)
+             .Includes(d => d.Account)
+             .Includes(d => d.Roles)
+             .Includes(d => d.Organization)
+             .Where(d => d.Account.AccountType == EAccountType.Personal && d.Id != SysAccountSeedData.Id)
+             .WhereIF(_sessionContext.OrgIsCenter == false, d => d.OrgId.StartsWith(_sessionContext.RequiredOrgId))
+             .WhereIF(!string.IsNullOrEmpty(dto.Keyword),
+                 d => d.Name.Contains(dto.Keyword!) || d.PhoneNo.Contains(dto.Keyword!) ||
+                      d.Account.UserName.Contains(dto.Keyword))
+             .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => d.OrgId == dto.OrgCode)
+             .WhereIF(!string.IsNullOrEmpty(dto.Role), d => d.Roles.Any(x => x.Id == dto.Role))
+             .WhereIF(!string.IsNullOrEmpty(dto.Name), d => d.Name.Contains(dto.Name))
+             .WhereIF(!string.IsNullOrEmpty(dto.PhoneNo), d => d.PhoneNo.Contains(dto.PhoneNo))
+             .WhereIF(!string.IsNullOrEmpty(dto.OrgName), d => d.Organization.Name.Contains(dto.OrgName))
+             .OrderBy(d => d.Account.Status)
+             .OrderBy(d => d.Organization.OrgType)
+             .OrderByDescending(d => d.CreationTime);
+        }
+    }
+}

+ 6 - 0
src/Hotline.Repository.SqlSugar/File/FileRepository.cs

@@ -10,6 +10,7 @@ using SqlSugar;
 using System.Threading;
 using XF.Domain.Authentications;
 using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
 using XF.Domain.Repository;
 
 namespace Hotline.Repository.SqlSugar.File
@@ -34,6 +35,11 @@ namespace Hotline.Repository.SqlSugar.File
 			await Db.Deleteable<Hotline.File.File>().In(x=>x.Id , deleteFilesId).ExecuteCommandAsync(cancellationToken);
 			foreach (FileDto file in files)
 			{
+				if (string.IsNullOrEmpty(file.Path) || string.IsNullOrEmpty(file.AllPath))
+					throw UserFriendlyException.SameMessage("附件信息错误,请检查后重新上传附件!");
+				var names = file.FileName.Split(".");
+				file.Name = names[0];
+				file.Type = names[1];
 				file.OrgName = _sessionContext.OrgName;
 				file.OrgId = _sessionContext.OrgId;
 				file.UserId = _sessionContext.UserId;

+ 11 - 3
src/Hotline.Repository.SqlSugar/Orders/OrderRepository.cs

@@ -1249,7 +1249,7 @@ namespace Hotline.Repository.SqlSugar.Orders
                 .Where(x => x.OrderVisit.VisitState == EVisitState.Visited && x.VisitTarget == EVisitTarget.Org)
                 .WhereIF(IsCenter == false, x => x.VisitOrgCode.StartsWith(_sessionContext.RequiredOrgId))
                 .WhereIF(dto.OrgVisitStatisticsType.HasValue, x => x.OrderVisit.Order.ProcessType == (EProcessType)((int)dto.OrgVisitStatisticsType))
-                .WhereIF(!string.IsNullOrEmpty(dto.OrgProcessingResults), x => SqlFunc.JsonField(x.OrgProcessingResults, "Key") == dto.OrgProcessingResults)
+                .WhereIF(!string.IsNullOrEmpty(dto.OrgProcessingResults), dto.AttitudeType == EAttitudeType.ProcessingResult ? x => SqlFunc.JsonField(x.OrgProcessingResults, "Key") == dto.OrgProcessingResults : x => SqlFunc.JsonField(x.OrgHandledAttitude, "Key") == dto.OrgProcessingResults)
                 .WhereIF(!string.IsNullOrEmpty(dto.VisitUser), x => x.OrderVisit.Employee.Name.Contains(dto.VisitUser))
                 .WhereIF(!string.IsNullOrEmpty(dto.No), x => x.OrderVisit.Order.No == dto.No)
                 .WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.OrderVisit.Order.Title.Contains(dto.Title))
@@ -1279,7 +1279,8 @@ namespace Hotline.Repository.SqlSugar.Orders
                     VisitUser = x.OrderVisit.Employee.Name,
                     VisitType = x.OrderVisit.VisitType,
                     VisitTime = x.OrderVisit.VisitTime,
-                    OrgProcessingResults = SqlFunc.JsonField(x.OrgProcessingResults, "Value"),
+                    OrgProcessingResults = dto.AttitudeType == EAttitudeType.ProcessingResult ? SqlFunc.JsonField(x.OrgProcessingResults, "Value")
+                    : SqlFunc.JsonField(x.OrgHandledAttitude, "Value"),
                     Content = x.OrderVisit.Order.Content,
                     FileOpinion = x.OrderVisit.Order.FileOpinion,
                     FiledTime = x.OrderVisit.Order.FiledTime,
@@ -1343,7 +1344,14 @@ namespace Hotline.Repository.SqlSugar.Orders
         }
     }
 
-    public class OrderDelayRepository : BaseRepositoryWorkflow<OrderDelay>, IOrderDelayRepository, IScopeDependency
+    public class OrderTerminateRepository : BaseRepositoryWorkflow<OrderTerminate>, IOrderTerminateRepository, IScopeDependency
+    {
+	    public OrderTerminateRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
+	    {
+	    }
+    }
+
+	public class OrderDelayRepository : BaseRepositoryWorkflow<OrderDelay>, IOrderDelayRepository, IScopeDependency
     {
         public OrderDelayRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
         {

+ 10 - 0
src/Hotline.Share/Attributes/ContentTypeAttribute.cs

@@ -0,0 +1,10 @@
+namespace Hotline.Share.Attributes;
+
+internal class ContentTypeAttribute : Attribute
+{
+    public string ContentType { get; }
+    public ContentTypeAttribute(string name)
+    {
+        ContentType = name;
+    }
+}

+ 10 - 0
src/Hotline.Share/Attributes/FileExtensionAttribute.cs

@@ -0,0 +1,10 @@
+namespace Hotline.Share.Attributes;
+
+internal class FileExtensionAttribute : Attribute
+{
+    public string FileExtension { get; }
+    public FileExtensionAttribute(string name)
+    {
+        FileExtension = name;
+    }
+}

+ 121 - 1
src/Hotline.Share/Dtos/CallCenter/BiQueryCallsDto.cs

@@ -1,5 +1,7 @@
-using Hotline.Share.Requests;
+using Hotline.Share.Enums.CallCenter;
+using Hotline.Share.Requests;
 using System.ComponentModel.DataAnnotations;
+using XF.Utility.EnumExtensions;
 
 namespace Hotline.Share.Dtos.CallCenter;
 
@@ -38,7 +40,19 @@ public class BiQueryGateWayDto : StartEndTimeDto
 
 public class StartEndTimeDto
 {
+    [Required]
     public DateTime StartTime { get; set; }
+
+    [Required]
+    public DateTime EndTime { get; set; }
+}
+
+public record PagedStartEndTimeDto : PagedRequest 
+{
+    [Required]
+    public DateTime StartTime { get; set; }
+
+    [Required]
     public DateTime EndTime { get; set; }
 }
 
@@ -72,4 +86,110 @@ public class TotalData<T>
 
     public int Total { get; set; }
     public List<T> Data { get; set; }
+}
+
+public record QueryCallsStatisticsDetailInDto : PagedStartEndTimeDto
+{
+    /// <summary>
+    /// 工单编号
+    /// </summary>
+    public string? OrderNo { get; set; }
+
+    /// <summary>
+    /// 主叫
+    /// </summary>
+    public string? FromNo { get; set; }
+
+    /// <summary>
+    /// 被叫
+    /// </summary>
+    public string? ToNo { get; set; }
+
+    /// <summary>
+    /// 分机号码
+    /// </summary>
+    public string? TelNo { get; set; }
+
+    /// <summary>
+    /// 挂断方
+    /// </summary>
+    public EEndBy? EndBy { get; set; }
+
+    /// <summary>
+    /// 上一个页面点击的字段名称
+    /// </summary>
+    public string? FieldName { get; set; }
+}
+
+public class QueryCallsStatisticsDetailOutDto
+{
+    public string Id { get; set; }
+    public string? OrderId { get; set; }
+
+    /// <summary>
+    /// 工单号
+    /// </summary>
+    public string? OrderNo { get; set; }
+
+    /// <summary>
+    /// 工单标题
+    /// </summary>
+    public string? OrderTitle { get; set; }
+
+    /// <summary>
+    /// 主叫
+    /// </summary>
+    public string FromNo { get; set; }
+
+    /// <summary>
+    /// 被叫
+    /// </summary>
+    public string ToNo { get; set; }
+
+    /// <summary>
+    /// 响应分机号
+    /// </summary>
+    public string TelNo { get; set; }
+
+    /// <summary>
+    /// 挂断方
+    /// </summary>
+    public EEndBy? EndBy { get; set; }
+
+    public string EndByTxt => EndBy.GetDescription();
+
+    /// <summary>
+    /// 话务员姓名
+    /// </summary>
+    public string UserName { get; set; }
+
+    /// <summary>
+    /// 分机组id(技能组Id)
+    /// </summary>
+    public string? GroupId { get; set; }
+
+    /// <summary>
+    /// IVR开始时间
+    /// </summary>
+    public DateTime? BeginIvrTime { get; set; }
+
+    /// <summary>
+    /// 接听时间
+    /// </summary>
+    public DateTime? AnsweredTime { get; set; }
+
+    /// <summary>
+    /// 挂机时间
+    /// </summary>
+    public DateTime EndTime { get; set; }
+
+    /// <summary>
+    /// 通话时长(秒)
+    /// </summary>
+    public int Duration { get; set; }
+
+    /// <summary>
+    /// 语音文件路径
+    /// </summary>
+    public string AudioFile { get; set; }
 }

+ 140 - 0
src/Hotline.Share/Dtos/CallCenter/CenterReportStatisticsDto.cs

@@ -8,6 +8,11 @@ namespace Hotline.Share.Dtos.CallCenter
 {
     public class CenterReportStatisticsDto
     {
+        /// <summary>
+        /// 电话
+        /// </summary>
+        public CenterReportCallInfoDto CenterReportCallInfoDto { get; set; }
+
         /// <summary>
         /// 电话
         /// </summary>
@@ -33,6 +38,11 @@ namespace Hotline.Share.Dtos.CallCenter
         /// </summary>
         public List<CenterReportOrderSourceChannelDto> CenterReportOrderAcceptTypes { get; set; }
 
+        /// <summary>
+        /// 专线统计
+        /// </summary>
+        public List<CenterReportOrderSourceChannelDto>  CenterReportOrderDedicatedLine{get;set ;}
+
         /// <summary>
         /// 市直部门
         /// </summary>
@@ -44,6 +54,62 @@ namespace Hotline.Share.Dtos.CallCenter
         public OrgStatisticsAll OrgStatisticsAreaAll { get; set; }
     }
 
+    /// <summary>
+    /// 话务情况
+    /// </summary>
+    public class CenterReportCallInfoDto
+    {
+        /// <summary>
+        /// 话务总量
+        /// </summary>
+        public int AllCallCount { get; set; }
+
+        /// <summary>
+        /// 呼入总量
+        /// </summary>
+        public int InTotal { get; set; }
+
+        /// <summary>
+        /// 呼出总量
+        /// </summary>
+        public int OutTotal { get; set; }
+
+        /// <summary>
+        /// 呼入接通量
+        /// </summary>
+        public int InConnectionQuantity { get; set; }
+
+        /// <summary>
+        /// 呼出接通量
+        /// </summary>
+        public int OutConnectionQuantity { get; set; }
+
+        /// <summary>
+        /// 呼入未接通
+        /// </summary>
+        public int InHanguped { get; set; }
+
+        /// <summary>
+        /// 呼出未接通
+        /// </summary>
+        public int OutHanguped { get; set; }
+
+        /// <summary>
+        /// 队列挂断
+        /// </summary>
+        public int QueueByeCount { get; set; }
+
+        /// <summary>
+        /// IVR挂断
+        /// </summary>
+        public int IvrByeCount { get; set; }
+
+        /// <summary>
+        /// 呼入未接通
+        /// </summary>
+        public int CallInHanguped => InHanguped - QueueByeCount - IvrByeCount;
+    }
+
     /// <summary>
     /// 话务量
     /// </summary>
@@ -100,10 +166,53 @@ namespace Hotline.Share.Dtos.CallCenter
         /// </summary>
         public int CompletedCount { get; set; }
 
+        /// <summary>
+        /// 按时办结
+        /// </summary>
+        public int OnTimeCompletedCount { get; set; }
+
+        /// <summary>
+        /// 按时办结率
+        /// </summary>
+        public double OnTimeCompletedCountClc => CalcRate();
+
+        /// <summary>
+        /// 计算办结率
+        /// </summary>
+        /// <returns></returns>
+        public double CalcRate()
+        {
+            if (OnTimeCompletedCount > 0 && CompletedCount > 0)
+                return Math.Round(((double)OnTimeCompletedCount / CompletedCount) * 100, 2);
+            return 0;
+        }
+
+        /// <summary>
+        /// 中心已办结
+        /// </summary>
+        public int CenterCompletedCount { get; set; }
+
+        /// <summary>
+        /// 部门已办结
+        /// </summary>
+        public int OrgCompletedCount { get; set; }
+
         /// <summary>
         /// 在办
         /// </summary>
         public int InProgressCount { get; set; }
+
+        /// <summary>
+        /// 中心在办
+        /// </summary>
+        public int CenterInProgressCount { get; set; }
+
+        /// <summary>
+        /// 部门在办
+        /// </summary>
+        public int OrgInProgressCount { get; set; }
+
+
     }
 
     /// <summary>
@@ -161,6 +270,21 @@ namespace Hotline.Share.Dtos.CallCenter
         /// </summary>
         public int Visitd { get; set; }
 
+        /// <summary>
+        /// 已回访--电话
+        /// </summary>
+        public int CallVisitd { get; set; }
+
+        /// <summary>
+        /// 已回访--短信
+        /// </summary>
+        public int SmsVisitd { get; set; }
+
+        /// <summary>
+        /// 其他回访
+        /// </summary>
+        public int OtherVisitd { get; set; }
+
         /// <summary>
         /// 待回访
         /// </summary>
@@ -219,8 +343,24 @@ namespace Hotline.Share.Dtos.CallCenter
 
     public class OrgStatistics
     {
+        /// <summary>
+        /// 部门编码
+        /// </summary>
+        public string OrgCode { get; set; }
+
+        /// <summary>
+        /// 部门名称
+        /// </summary>
         public string OrgName { get; set; }
 
+        /// <summary>
+        /// 工单数量
+        /// </summary>
         public int CountNum { get; set; }
+
+        /// <summary>
+        /// 办件次数
+        /// </summary>
+        public int HandleCountNum => CountNum;
     }
 }

+ 64 - 0
src/Hotline.Share/Dtos/CallCenter/QueryCallsDetailDto.cs

@@ -125,4 +125,68 @@
             return Math.Round((Quantity / (double)Count) * 100, 3) + "%";
         }
     }
+
+    public class QueryCallsDetailStatistics
+    {
+
+        /// <summary>
+        /// 日期
+        /// </summary>
+        public string Date { get; set; }
+
+        /// <summary>
+        /// 呼入总量 (呼入接通 + 挂机量 + 呼入队列挂断)
+        /// </summary>
+        public int InTotal { get; set; }
+
+        /// <summary>
+        /// 呼入队列挂断
+        /// </summary>
+        public int NotAcceptedHang { get; set; }
+
+        /// <summary>
+        /// 呼入接通量
+        /// </summary>
+        public int InConnectionQuantity { get; set; }
+
+        /// <summary>
+        /// 挂机量(呼入 + telno 非空 + 通话时间是0)
+        /// </summary>
+        public int InNotAnswered { get; set; }
+
+        /// <summary>
+        /// 呼入接通率
+        /// </summary>
+        public string InConnectionRate => CalcSatisfiedRate(InTotal, InConnectionQuantity + InNotAnswered);
+
+        /// <summary>
+        /// 呼入IVR挂断
+        /// </summary>
+        public int IvrByeCount { get; set; }
+
+        /// <summary>
+        /// 呼出接通量
+        /// </summary>
+        public int OutConnectionQuantity { get; set; }
+
+        /// <summary>
+        /// 呼出未接通
+        /// </summary>
+        public int OutNotAnswered { get; set; }
+
+        /// <summary>
+        /// 计算满意度
+        /// </summary>
+        /// <param name="Count">总数</param>
+        /// <param name="Quantity"></param>
+        /// <returns></returns>
+        public string CalcSatisfiedRate(int Count, int Quantity)
+        {
+            if (Count <= 0 || Quantity <= 0)
+                return 0 + "%";
+
+            return Math.Round((Quantity / (double)Count) * 100, 3) + "%";
+        }
+
+    }
 }

+ 12 - 2
src/Hotline.Share/Dtos/File/FileDto.cs

@@ -9,7 +9,12 @@ namespace Hotline.Share.Dtos.File
 {
 	public class FileDto
 	{
-        /// <summary>
+		/// <summary>
+		/// 附件全称
+		/// </summary>
+		public string FileName { get; set; }
+
+		/// <summary>
 		/// 附件名称
 		/// </summary>
 		public string? Name { get; set; }
@@ -73,7 +78,12 @@ namespace Hotline.Share.Dtos.File
 		/// </summary>
 		public string? Path { get; set; }
 
-}
+		/// <summary>
+		/// 完整附件路径
+		/// </summary>
+		public string? AllPath { get; set; }
+
+	}
 	public class UpdateFileDto: FileDto
 	{
 		public string Id { get; set; }

+ 5 - 0
src/Hotline.Share/Dtos/FlowEngine/NextStepOption.cs

@@ -34,6 +34,11 @@ public class NextStepOption : Kv
     /// </summary>
     public EHandlerType HandlerType { get; set; }
 
+    /// <summary>
+    /// 节点部门等级(非部门等级办理时无效)
+    /// </summary>
+    public int? OrgLevel { get; set; }
+
     /// <summary>
     /// 节点下可选办理对象
     /// </summary>

+ 5 - 0
src/Hotline.Share/Dtos/FlowEngine/NextStepsDto.cs

@@ -34,6 +34,11 @@ public class NextStepsDto
     /// </summary>
     public EBusinessType CurrentStepBusinessType { get; set; }
 
+    /// <summary>
+    /// 当前办理节点部门等级(非部门等级办理时无效)
+    /// </summary>
+    public int? CurrentOrgLevel { get; set; }
+
     public IReadOnlyList<KeyValuePair<int, string>> TimeTypeOptions { get; set; }
 }
 

+ 5 - 0
src/Hotline.Share/Dtos/Identity/LoginDto.cs

@@ -13,6 +13,11 @@ namespace Hotline.Share.Dtos.Identity
         public string? MsgCode { get; set; }
     }
 
+    public class HotlineLoginOldToNewDto
+    {
+        public string UserName { get; set; }
+    }
+
     public class LoginPageInfoDto
     {
 

+ 9 - 0
src/Hotline.Share/Dtos/Knowledge/KnowledgeCollectDto.cs

@@ -2,6 +2,7 @@
 using Hotline.Share.Requests;
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -53,6 +54,10 @@ namespace Hotline.Share.Dtos.Knowledge
 		/// </summary>
 		public bool? Collect { get; set; }
 
+		/// <summary>
+		/// 分组Id
+		/// </summary>
+		public string? KnowledgeCollectGroupId { get; set; }
 	}
 
 	public class KnowledgeCollectDeleteDto
@@ -97,5 +102,9 @@ namespace Hotline.Share.Dtos.Knowledge
 
 	public record KnowledgeCollectListDto : PagedKeywordRequest
 	{
+		/// <summary>
+		/// 分组Id
+		/// </summary>
+		public string? KnowledgeCollectGroupId { get; set; }
 	}
 }

+ 191 - 1
src/Hotline.Share/Dtos/Knowledge/KnowledgeDto.cs

@@ -1,8 +1,13 @@
-using Hotline.Share.Dtos.File;
+using Hotline.Share.Dtos.CallCenter;
+using Hotline.Share.Dtos.File;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Org;
 using Hotline.Share.Dtos.Quality;
+using Hotline.Share.Enums.Article;
 using Hotline.Share.Enums.KnowledgeBase;
+using Hotline.Share.Notifications.NewRockCallCenter;
+using Hotline.Share.Requests;
+using System.ComponentModel.DataAnnotations;
 using XF.Utility.EnumExtensions;
 
 namespace Hotline.Share.Dtos.Knowledge
@@ -333,4 +338,189 @@ namespace Hotline.Share.Dtos.Knowledge
     {
 
     }
+
+    public class KnowledgeCollectGroupOutDto
+    {
+        /// <summary>
+        /// Id
+        /// </summary>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// 名字
+        /// </summary>
+        public string Name { get; set; }
+    }
+
+    public class KnowledgeCollectGroupInDto
+    { 
+        /// <summary>
+        /// 名字
+        /// </summary>
+        [Required]
+        public string Name { get; set; }
+    }
+
+    public record KnowledgeCollectGroupListInDto : PagedKeywordRequest
+    { }
+
+    public class KnowledgeInfoExportInDto
+    {
+        /// <summary>
+        /// 导入的知识Id集合
+        /// </summary>
+        public string[] Ids { get; set; }
+
+        /// <summary>
+        /// 导出格式
+        /// </summary>
+        public EFileType FileType { get; set; }
+    }
+
+    public record PageViewInDto : PagedRequest
+    {
+        /// <summary>
+        /// 标题
+        /// </summary>
+        public string? Title { get; set; }
+
+        /// <summary>
+        /// 浏览人
+        /// </summary>
+        public string? CreatorName { get; set; }
+
+        /// <summary>
+        /// 知识分类Id
+        /// </summary>
+        public string? KnowledgeTypeId { get; set; }
+
+        /// <summary>
+        /// 开始时间
+        /// </summary>
+        public DateTime? StartTime { get; set; }
+
+        /// <summary>
+        /// 结束时间
+        /// </summary>
+        public DateTime? EndTime { get; set; }
+    }
+
+    public class PageViewOutDto
+    {
+        /// <summary>
+        /// 标题
+        /// </summary>
+        public string KnowledgeTitle { get; set; }
+
+        /// <summary>
+        /// 浏览人
+        /// </summary>
+        public string CreatorName { get; set; }
+
+        /// <summary>
+        /// 浏览时间
+        /// </summary>
+        public DateTime CreationTime { get; set; }
+    }
+
+    public class KnowledgeWordOutDto
+    {
+        public KnowledgeWordOutDto()
+        {
+            
+        }
+        public KnowledgeWordOutDto(string keyId, string tag)
+        {
+            Id = keyId;
+            Tag = tag;
+        }
+
+        /// <summary>
+        /// Id
+        /// </summary>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// 标签
+        /// </summary>
+        public string Tag { get; set; }
+    }
+
+    public record KnowledgeHotWordInDto : PagedRequest
+    {
+        /// <summary>
+        /// 热词
+        /// </summary>
+        public string? KeyWord { get; set; }
+
+        /// <summary>
+        /// 是否启用
+        /// </summary>
+        public bool? IsEnable { get; set; }
+
+        /// <summary>
+        /// 排序(需要被排序的字段名)
+        /// </summary>
+        public string? SortField { get; set; }
+
+        /// <summary>
+        /// 排序方向 (0: 升序Asc; 1: 降序Desc)
+        /// </summary>
+        public int OrderByType { get; set; } = 1;
+
+        /// <summary>
+        /// 热词类型
+        /// </summary>
+        public EKnowledgeHotWordType? Type { get; set; }
+    }
+
+    public class UpdateKnowledgeHotWordInDto : AddKnowledgeHotWordInDto
+    { 
+        /// <summary>
+        /// Id
+        /// </summary>
+        [Required]
+        public string Id { get; set; }
+
+        /// <summary>
+        /// 点击次数
+        /// </summary>
+        public int? SearchCount { get; set; }
+
+        /// <summary>
+        /// 是否启用
+        /// </summary>
+        public bool? IsEnable { get; set; }
+    }
+
+    public class AddKnowledgeHotWordInDto
+    {
+        /// <summary>
+        /// 热词
+        /// </summary>
+        public string? KeyWord { get; set; }
+
+        /// <summary>
+        /// 排序
+        /// </summary>
+        public int? Sort { get; set; }
+
+        /// <summary>
+        /// 热词类型
+        /// </summary>
+        public EKnowledgeHotWordType? Type { get; set; }
+    }
+
+    public class KnowledgeHotWordOutDto : UpdateKnowledgeHotWordInDto
+    {
+        /// <summary>
+        /// 创建时间
+        /// </summary>
+        public DateTime CreationTime { get; set; }
+
+        /// <summary>
+        /// 热词类型
+        /// </summary>
+        public string TypeTxt => Type?.GetDescription();
+    }
 }

+ 6 - 4
src/Hotline.Share/Dtos/Order/OrderBiDto.cs

@@ -350,10 +350,12 @@ namespace Hotline.Share.Dtos.Order
 		/// </summary>
 		public DateTime? VisitTime { get; set; }
 
-		/// <summary>
-		/// 满意度
-		/// </summary>
-		public string OrgProcessingResults { get; set; }
+        /// <summary>
+        /// 满意度
+        /// 如果入参 EAttitudeType = 1 是 办件结果
+        /// 如果入参 EAttitudeType = 2 是 办事态度
+        /// </summary>
+        public string OrgProcessingResults { get; set; }
 
 		/// <summary>
 		/// 受理内容

+ 11 - 2
src/Hotline.Share/Dtos/Order/OrderDto.cs

@@ -677,11 +677,11 @@ namespace Hotline.Share.Dtos.Order
             this.FromName = maskString;
             this.FromGender = EGender.Unknown;
             this.FromPhone = maskString;
-
             this.FullAddress = maskString;
             this.Address = maskString;
             this.City = maskString;
             this.Street = maskString;
+            this.ContactMask = maskString;
             return this;
         }
 
@@ -745,8 +745,17 @@ namespace Hotline.Share.Dtos.Order
         /// </summary>
         public bool? ProvinceSendBack { get; set; }
 
+		/// <summary>
+		/// 终止
+		/// </summary>
+		public List<OrderTerminateDto> OrderTerminates { get; set; }
 
-    }
+        /// <summary>
+        /// 终止状态
+        /// </summary>
+        public string? OrderTerminateStatus { get; set; }
+
+	}
 
     public class UpdateOrderDto : AddOrderDto
     {

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

@@ -22,7 +22,19 @@ namespace Hotline.Share.Dtos.Order
 
     }
 
-    public class ScreenNextFlowDto
+    public class TerminateStartFlowDto : StartWorkflowDto<OrderTerminateDto>
+    {
+
+    }
+
+    public class TerminateNextFlowDto
+	{
+	    public OrderTerminateDto Data { get; set; }
+
+	    public NextWorkflowDto NextWorkflow { get; set; }
+    }
+
+	public class ScreenNextFlowDto
     {
         public OrderScreenDto Data { get; set; }
 

+ 222 - 0
src/Hotline.Share/Dtos/Order/OrderTerminateDto.cs

@@ -0,0 +1,222 @@
+using Hotline.Share.Dtos.File;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.Share.Requests;
+using System.ComponentModel;
+using XF.Utility.EnumExtensions;
+
+namespace Hotline.Share.Dtos.Order
+{
+	public class OrderTerminateDto
+	{
+
+		/// <summary>
+		/// 终止Id
+		/// </summary>
+		public string Id { get; set; }
+
+		/// <summary>
+		/// 工单id
+		/// </summary>
+		public string OrderId { get; set; }
+
+
+		/// <summary>
+		/// 终止状态
+		/// </summary>
+		public ETerminateStatus? Status { get; set; }
+
+		/// <summary>
+		/// 工单编号
+		/// </summary>
+		public string No { get; set; }
+
+		/// <summary>
+		/// 终止理由
+		/// </summary>
+		public string? Content { get; set; }
+
+		/// <summary>
+		/// 附件
+		/// </summary>
+		public List<FileJson>? FileJson { get; set; }
+
+		/// <summary>
+		/// 附件列表
+		/// </summary>
+		public List<FileDto> Files { get; set; } = new();
+	}
+
+	public class OrderTerminateBaseDto
+	{
+		public DateTime? LastModificationTime { get; set; }
+
+		public bool IsDeleted { get; set; }
+
+		/// <summary>
+		/// 删除时间
+		/// </summary>
+		public DateTime? DeletionTime { get; set; }
+
+
+		/// <summary>
+		/// 创建时间
+		/// </summary>
+		public DateTime CreationTime { get; set; }
+
+		public string Id { get; set; }
+
+		/// <summary>
+		/// 组织Id
+		/// </summary>
+		public string? CreatorOrgId { get; set; }
+
+
+		public string? CreatorOrgName { get; set; }
+
+		/// <summary>
+		/// 创建人
+		/// </summary>
+		public string? CreatorId { get; set; }
+
+		public string? CreatorName { get; set; }
+
+		public string? WorkflowId { get; set; }
+	}
+
+	public class OrderTerminateEntityDto : OrderTerminateBaseDto 
+	{
+		/// <summary>
+		/// 工单
+		/// </summary>
+		public OrderDto Order { get; set; }
+
+		/// <summary>
+		/// 是否可办理
+		/// </summary>
+		public bool IsCanHandle { get; set; }
+
+		/// <summary>
+		/// 审批状态 
+		/// </summary>
+		public ETerminateStatus? Status { get; set; }
+
+
+		public string StatusText => Status.GetDescription();
+
+		/// <summary>
+		/// 终止理由
+		/// </summary>
+		public string? Content { get; set; }
+
+
+		/// <summary>
+		/// 办理 true  审批 false 
+		/// </summary>
+		public bool Handle { get; set; }
+
+		/// <summary>
+		/// 附件列表
+		/// </summary>
+		public List<FileDto> Files { get; set; } = new();
+
+		public List<FileJson>? FileJson { get; set; }
+	}
+
+	public record OrderTerminateListDto : PagedRequest
+	{
+		/// <summary>
+		/// 工单编号
+		/// </summary>
+		public string? No { get; set; }
+
+		/// <summary>
+		/// 工单标题
+		/// </summary>
+		public string? Title { get; set; }
+
+		/// <summary>
+		/// 审批状态 
+		/// </summary>
+		public ETerminateStatus? Status { get; set; }
+
+		/// <summary>
+		/// 申请开始时间
+		/// </summary>
+		public DateTime? ApplyStartTime { get; set; }
+
+		/// <summary>
+		/// 申请结束时间
+		/// </summary>
+		public DateTime? ApplyEndTime { get; set; }
+
+		/// <summary>
+		/// 工单受理开始时间
+		/// </summary>
+		public DateTime? StartTime { get; set; }
+
+		/// <summary>
+		/// 工单受理结束时间
+		/// </summary>
+		public DateTime? EndTime { get; set; }
+
+		/// <summary>
+		/// 查询类型  0 全部 1自己
+		/// </summary>
+		public int?  QueryType { get; set; }
+
+		/// <summary>
+		/// 审批状态  0 全部  1待审批 2 已审批
+		/// </summary>
+		public int? AuditStatus { get; set; }
+	}
+
+	public class OrderTerminateContentDto
+	{
+		/// <summary>
+		/// 终止id
+		/// </summary>
+		public string Id { get; set; }
+
+		/// <summary>
+		/// 终止理由
+		/// </summary>
+		public string? Content { get; set; }
+	}
+
+	public enum ETerminateStatus
+	{
+		/// <summary>
+		/// 审批中
+		/// </summary>
+		[Description("审批中")]
+		Approval = 1,
+
+		/// <summary>
+		/// 终止完成
+		/// </summary>
+		[Description("终止同意")]
+		End = 2,
+
+		/// <summary>
+		/// 终止拒绝
+		/// </summary>
+		[Description("终止不同意")]
+		Refuse = 3,
+
+		/// <summary>
+		/// 退回
+		/// </summary>
+		[Description("终止退回")]
+		SendBack = 4,
+
+		/// <summary>
+		/// 退回待重提
+		/// </summary>
+		[Description("退回待重提")]
+		SendBackStart = 5,
+	}
+}

+ 41 - 1
src/Hotline.Share/Dtos/Order/OrderVisitDto.cs

@@ -1,4 +1,5 @@
-using Hotline.Share.Dtos.Users;
+using Hotline.Share.Dtos.CallCenter;
+using Hotline.Share.Dtos.Users;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Requests;
 using System;
@@ -127,6 +128,13 @@ namespace Hotline.Share.Dtos.Order
         public EProcessType? ProcessType { get; set; }
     }
 
+    public class QueryOrderVisitQuantity : StartEndTimeDto
+    {
+        /// <summary>
+        /// 访问员
+        /// </summary>
+        public string? EmployeeName { get; set; }
+    }
 
     public record QueryOrderVisitSourceChannelDto
     {
@@ -622,6 +630,38 @@ namespace Hotline.Share.Dtos.Order
         SMSUnsatisfied = 41,
     }
 
+    public class OrderVisitQuantityOutDto
+    {
+        /// <summary>
+        /// 回访员
+        /// </summary>
+        public string EmployeeName { get; set; }
+
+        /// <summary>
+        /// 回访员
+        /// </summary>
+        public string EmployeeId { get; set; }
+
+        /// <summary>
+        /// 电话回访量
+        /// </summary>
+        public int CallVisitCount { get; set; }
+
+        /// <summary>
+        /// 默认回访量
+        /// </summary>
+        public int DefaultVisitCount { get; set; }
+
+        /// <summary>
+        /// 短信回访量
+        /// </summary>
+        public int SmsVisitCount { get; set; }
+
+        /// <summary>
+        /// 总回访量
+        /// </summary>
+        public int TotalVisitCount => SmsVisitCount + DefaultVisitCount + CallVisitCount;
+    }
     public class OrderVisitSourceChannelDto
     {
         public int Count { get; set; }

+ 5 - 0
src/Hotline.Share/Dtos/Order/OrderWaitedDto.cs

@@ -41,6 +41,11 @@ namespace Hotline.Share.Dtos.Order
         /// 区域
         /// </summary>
         public string? AreaCode { get; set; }
+
+        /// <summary>
+        /// 工单状态
+        /// </summary>
+        public EOrderStatus? Status { get; set; }
 	}
 
     /// <summary>

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

@@ -71,6 +71,12 @@ namespace Hotline.Share.Dtos.Order
 
 		public string? AuditUser { get; set; }
 
+
+		/// <summary>
+		/// 审批时间
+		/// </summary>
+		public DateTime? AuditTime { get; set; }
+
 		/// <summary>
 		/// 申请部门ID
 		/// </summary>

+ 10 - 0
src/Hotline.Share/Dtos/Settings/TimeConfig.cs

@@ -22,6 +22,16 @@ namespace Hotline.Share.Dtos.Settings
     {
         public const string BusCodeName = "AcceptTypeCode";
 
+        public OrderTimeClacInfo()
+        {
+            
+        }
+
+        public OrderTimeClacInfo(string acceptTypeCode)
+        {
+            AcceptTypeCode = acceptTypeCode;
+        }
+
         /// <summary>
         /// 受理类型代码
         /// </summary>

+ 10 - 0
src/Hotline.Share/Dtos/Users/UserDto.cs

@@ -43,6 +43,11 @@ public record AddUserDto
     /// </summary>
     public string? PhoneNo { get; set; }
 
+    /// <summary>
+    /// 座机号码
+    /// </summary>
+    public string? LandlineNumber { get; set; }
+
     /// <summary>
     /// 展示名称(Identity.DisplayName)
     /// </summary>
@@ -99,6 +104,11 @@ public record UpdateUserDto
     /// </summary>
     public string? PhoneNo { get; set; }
 
+    /// <summary>
+    /// 座机号码
+    /// </summary>
+    public string? LandlineNumber { get; set; }
+
     /// <summary>
     /// 展示名称(Identity.DisplayName)
     /// </summary>

+ 1 - 1
src/Hotline.Share/Dtos/Users/UserPagedDto.cs

@@ -3,7 +3,7 @@ using Hotline.Share.Requests;
 
 namespace Hotline.Share.Dtos.Users
 {
-    public record UserPagedDto(string? OrgCode, string? Role) : PagedKeywordRequest;
+    public record UserPagedDto(string? OrgCode, string? Role,string? Name,string?OrgName,string? PhoneNo) : PagedKeywordRequest;
 
     public record RestPagedDto(string? KeyWords,DateTime? BeginTime,DateTime? EndTime, ETelRestApplyStatus? Status,string? Reason): PagedRequest;
 }

+ 34 - 0
src/Hotline.Share/Enums/Article/EFileType.cs

@@ -0,0 +1,34 @@
+using Hotline.Share.Attributes;
+using System.ComponentModel;
+
+namespace Hotline.Share.Enums.Article;
+
+[Flags]
+public enum EFileType
+{
+    None = 0, 
+
+    /// <summary>
+    /// Excel文件
+    /// </summary>
+    [Description("导出Excel")]
+    [FileExtension(".xlsx")]
+    [ContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")]
+    excel = 1,
+
+    /// <summary>
+    /// Word文件
+    /// </summary>
+    [Description("导出Word")]
+    [FileExtension(".docx")]
+    [ContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")]
+    word = 2,
+
+    /// <summary>
+    /// PDF文件
+    /// </summary>
+    [Description("导出PDF")]
+    [FileExtension(".pdf")]
+    [ContentType("application/pdf")]
+    pdf = 4,
+}

+ 25 - 0
src/Hotline.Share/Enums/KnowledgeBase/EKnowledgeHotWordType.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Enums.KnowledgeBase;
+public enum EKnowledgeHotWordType
+{
+    /// <summary>
+    /// 知识库
+    /// </summary>
+    [Description("知识库")]
+    KnowledgeBase = 0,
+
+    /// <summary>
+    /// 预案库
+    /// </summary>
+    [Description("预案库")]
+    PrePlanBase = 1,
+
+    [Description("案例库")]
+    CaseBase = 2,
+}

+ 7 - 1
src/Hotline.Share/Enums/KnowledgeBase/EKnowledgeRetrievalType.cs

@@ -22,7 +22,7 @@ public enum EKnowledgeRetrievalType
     /// <summary>
     /// 正文
     /// </summary>
-    [Description("正文")]
+    [Description("知识内容")]
     Content = 2,
 
     /// <summary>
@@ -36,4 +36,10 @@ public enum EKnowledgeRetrievalType
     /// </summary>
     [Description("所属部门")]
     Org = 4,
+
+    /// <summary>
+    /// 关键词
+    /// </summary>
+    [Description("关键词")]
+    KeyWord = 5,
 }

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

@@ -0,0 +1,18 @@
+using System.ComponentModel;
+
+namespace Hotline.Share.Enums.Order;
+public enum EAttitudeType
+{
+    /// <summary>
+    /// 办件结果
+    /// </summary>
+    [Description("办件结果")]
+    ProcessingResult = 1,
+
+    /// <summary>
+    /// 办事态度
+    /// </summary>
+    [Description("办事态度")]
+    WorkAttitude = 2
+}
+

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

@@ -296,7 +296,7 @@ public record QueryOrderDelayDataListRequest : ReportPagedRequest
     public string? OrgName { get; set; }
 }
 
-public record QueryOrderDelayDataDetailRequest : ReportPagedRequest
+public record QueryOrderDelayDataDetailRequest : ReportRequiredPagedRequest
 {
     public string? OrgCode { get; set; }
 
@@ -433,7 +433,8 @@ public record OrgVisitDetailListReq: PagedKeywordRequest
     public EOrgVisitStatisticsType? OrgVisitStatisticsType { get; set; }
 
     /// <summary>
-    /// 办件结果
+    /// AttitudeType = 1 是 办件结果
+    /// AttitudeType = 2 是 办事态度
     /// </summary>
     public string? OrgProcessingResults { get; set; }
 
@@ -485,6 +486,12 @@ public record OrgVisitDetailListReq: PagedKeywordRequest
     public DateTime? VisitTimeStart { get; set; }
 
     public DateTime? VisitTimeEnd { get; set; }
+
+    /// <summary>
+    /// 办件态度类型(1:办件结果; 2:办事态度)
+    /// 默认 办件结果;
+    /// </summary>
+    public EAttitudeType AttitudeType { get; set; } = EAttitudeType.ProcessingResult;
 }
 
 

+ 45 - 0
src/Hotline.Share/Tools/EnumExtensions.cs

@@ -0,0 +1,45 @@
+using Hotline.Share.Attributes;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Tools;
+public static class EnumExtensions
+{
+    /// <summary>
+    /// 获取枚举项上的<see cref="FileExtensionAttribute"/>特性的文字描述
+    /// </summary>
+    /// <param name="value">枚举</param>
+    /// <returns>属性值</returns>
+    public static string GetFileExtension(this Enum value)
+    {
+        var type = value.GetType();
+        var member = type.GetMember(value.ToString()).FirstOrDefault();
+        var desc = member?.GetAttribute<FileExtensionAttribute>();
+        if (desc != null)
+        {
+            return desc.FileExtension;
+        }
+        return string.Empty;
+    }
+
+    /// <summary>
+    /// 获取枚举项上的<see cref="ContentTypeAttribute"/>特性的文字描述
+    /// </summary>
+    /// <param name="value">枚举</param>
+    /// <returns>属性值</returns>
+    public static string GetContentType(this Enum value)
+    {
+        var type = value.GetType();
+        var member = type.GetMember(value.ToString()).FirstOrDefault();
+        var desc = member?.GetAttribute<ContentTypeAttribute>();
+        if (desc != null)
+        {
+            return desc.ContentType;
+        }
+        return string.Empty;
+    }
+}

+ 13 - 1
src/Hotline.Share/Tools/StringExtensions.cs

@@ -1,4 +1,6 @@
-namespace Hotline.Share.Tools;
+using System.Text.RegularExpressions;
+
+namespace Hotline.Share.Tools;
 public static class StringExtensions
 {
     public static bool IsNullOrEmpty(this string? str)
@@ -27,4 +29,14 @@ public static class StringExtensions
         // 尝试解析字符串为枚举
         return (TEnum)Enum.Parse(typeof(TEnum), value);
     }
+
+    public static IList<string> SplitKeywords(this string value)
+    {
+        var regex = new Regex(@"[ ,,]+");
+
+        return regex.Split(value)
+                    .Where(s => !string.IsNullOrWhiteSpace(s))
+                    .Select(s => s.Trim())
+                    .ToList();
+    }
 }

+ 15 - 0
src/Hotline.Share/Tools/TupleExtensions.cs

@@ -0,0 +1,15 @@
+using Hotline.Share.Dtos;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Tools;
+public static class TupleExtensions
+{
+    public static PagedDto<T> ToPaged<T>(this (int, IList<T>) tuple)
+    {
+        return new PagedDto<T>(tuple.Item1, tuple.Item2.ToList());
+    }
+}

+ 13 - 0
src/Hotline.Share/Tools/TypeExtensions.cs

@@ -14,4 +14,17 @@ public static class TypeExtensions
     {
         return memberInfo.IsDefined(typeof(T), inherit);
     }
+
+    /// <summary>
+    /// 从类型成员获取指定Attribute特性
+    /// </summary>
+    /// <typeparam name="T">Attribute特性类型</typeparam>
+    /// <param name="memberInfo">类型类型成员</param>
+    /// <param name="inherit">是否从继承中查找</param>
+    /// <returns>存在返回第一个,不存在返回null</returns>
+    public static T GetAttribute<T>(this MemberInfo memberInfo, bool inherit = false) where T : Attribute
+    {
+        var descripts = memberInfo.GetCustomAttributes(typeof(T), inherit);
+        return descripts.FirstOrDefault() as T;
+    }
 }

+ 9 - 2
src/Hotline/Authentications/Police110SessionContext.cs

@@ -1,4 +1,7 @@
-using Hotline.Configurations;
+using Hotline.Caching.Interfaces;
+using Hotline.Configurations;
+using Hotline.Settings;
+using Newtonsoft.Json;
 using XF.Domain.Authentications;
 
 namespace Hotline.Authentications
@@ -6,8 +9,12 @@ namespace Hotline.Authentications
     public class Police110SessionContext : ISessionContext
     {
         public const string Key = "Police110SessionContext";
-        public Police110SessionContext(PublicSecurityConfiguration config)
+        public Police110SessionContext(ISystemSettingCacheManager systemSettingCacheManager)
         {
+            var setting = systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            var cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+            var config = cityBase.PublicSecurity;
+
             UserId = config.UserId;
             UserName = config.UserName;
             OrgId = config.OrgId;

+ 13 - 6
src/Hotline/Authentications/ProvinceSessionContext.cs

@@ -1,5 +1,8 @@
-using Hotline.Configurations;
+using Hotline.Caching.Interfaces;
+using Hotline.Configurations;
+using Hotline.Settings;
 using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
 using XF.Domain.Authentications;
 
 namespace Hotline.Authentications
@@ -8,12 +11,16 @@ namespace Hotline.Authentications
     {
         public const string Key = "ProvinceSessionContext";
 
-        public ProvinceSessionContext(IOptions<CityBaseConfiguration> options)
+        public ProvinceSessionContext(ISystemSettingCacheManager systemSettingCacheManager)
         {
-            UserId = options.Value.CityProvince.UserId;
-            UserName = options.Value.CityProvince.UserName;
-            OrgId = options.Value.CityProvince.OrgId;
-            OrgName = options.Value.CityProvince.OrgName;
+            var setting = systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            var cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+            var config = cityBase.CityProvince;
+
+            UserId = config.UserId;
+            UserName = config.UserName;
+            OrgId = config.OrgId;
+            OrgName = config.OrgName;
             OrgLevel = 1;
         }
 

+ 2 - 10
src/Hotline/Authentications/SessionContextCreator.cs

@@ -1,13 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Hotline.Configurations;
-using Hotline.Share.Enums.Order;
-using Hotline.Users;
-using XF.Domain.Authentications;
-using static Org.BouncyCastle.Math.EC.ECCurve;
+using XF.Domain.Authentications;
+
 
 namespace Hotline.Authentications
 {

+ 9 - 2
src/Hotline/Authentications/YbEnterpriseSessionContext.cs

@@ -1,4 +1,7 @@
-using Hotline.Configurations;
+using Hotline.Caching.Interfaces;
+using Hotline.Configurations;
+using Hotline.Settings;
+using Newtonsoft.Json;
 using XF.Domain.Authentications;
 
 namespace Hotline.Authentications
@@ -6,8 +9,12 @@ namespace Hotline.Authentications
     public class YbEnterpriseSessionContext : ISessionContext
     {
         public const string Key = "YbEnterpriseSessionContext";
-        public YbEnterpriseSessionContext(CityEnterpriseConfiguration config)
+        public YbEnterpriseSessionContext(ISystemSettingCacheManager systemSettingCacheManager)
         {
+            var setting = systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            var cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+            var config = cityBase.CityEnterprise;
+
             UserId = config.UserId;
             UserName = config.UserName;
             OrgId = config.OrgId;

+ 9 - 2
src/Hotline/Authentications/ZzptSessionContext.cs

@@ -1,4 +1,7 @@
-using Hotline.Configurations;
+using Hotline.Caching.Interfaces;
+using Hotline.Configurations;
+using Hotline.Settings;
+using Newtonsoft.Json;
 using XF.Domain.Authentications;
 
 namespace Hotline.Authentications
@@ -6,8 +9,12 @@ namespace Hotline.Authentications
     public class ZzptSessionContext : ISessionContext
     {
         public const string Key = "ZzptSessionContext";
-        public ZzptSessionContext(ComprehensiveTreatmentConfiguration config)
+        public ZzptSessionContext(ISystemSettingCacheManager systemSettingCacheManager)
         {
+            var setting = systemSettingCacheManager.GetSetting(SettingConstants.CityBaseConfiguration)?.SettingValue[0];
+            var cityBase = JsonConvert.DeserializeObject<CityBaseConfiguration>(setting);
+            var config = cityBase.ComprehensiveTreatment;
+
             UserId = config.UserId;
             UserName = config.UserName;
             OrgId = config.OrgId;

+ 1 - 0
src/Hotline/Caching/Interfaces/ISystemSettingCacheManager.cs

@@ -13,5 +13,6 @@ namespace Hotline.Caching.Interfaces
         int SeatChaoTime { get; }
         int RingTimes { get; }
         string RecordPrefix { get; }
+        string DefaultVisitEmployeeId { get; }
     }
 }

+ 2 - 0
src/Hotline/Caching/Services/SystemSettingCacheManager.cs

@@ -51,5 +51,7 @@ namespace Hotline.Caching.Services
         public int RingTimes => int.Parse(GetSetting(SettingConstants.RingTimes)?.SettingValue[0]);
 
         public string RecordPrefix => GetSetting(SettingConstants.RecordPrefix)?.SettingValue[0].Trim().ToString();
+
+        public string DefaultVisitEmployeeId => GetSetting(SettingConstants.DefaultVisitEmployeeId)?.SettingValue[0].Trim().ToString();
     }
 }

+ 12 - 0
src/Hotline/File/File.cs

@@ -7,6 +7,12 @@ namespace Hotline.File
 	[Description("附件")]
 	public class File : FullStateEntity
 	{
+		/// <summary>
+		/// 附件全称
+		/// </summary>
+		[SugarColumn(ColumnDescription = "附件全称")]
+		public string? FileName { get; set; }
+
 		/// <summary>
 		/// 附件名称
 		/// </summary>
@@ -81,5 +87,11 @@ namespace Hotline.File
 		/// </summary>
 		[SugarColumn(ColumnDescription = "附件路径")]
 		public string? Path { get; set; }
+
+		/// <summary>
+		/// 完整附件路径
+		/// </summary>
+		[SugarColumn(ColumnDescription = "完整附件路径")]
+		public string? AllPath { get; set; }
 	}
 }

+ 13 - 1
src/Hotline/FlowEngine/WorkflowModules/WorkflowModuleConsts.cs

@@ -44,7 +44,18 @@ public class WorkflowModuleConsts
     /// </summary>
     public const string TelRestApply = "TelRestApply";
 
-    public static List<WorkflowModule> AllModules =>
+    /// <summary>
+    /// 工单终止
+    /// </summary>
+    public const string OrderTerminate = "OrderTerminate";
+
+    /// <summary>
+    /// 二次办理
+    /// </summary>
+    public const string OrderSecondaryHandling = "OrderSecondaryHandling";
+
+
+	public static List<WorkflowModule> AllModules =>
         new()
         {
             new(OrderHandle, "工单办理"),
@@ -55,5 +66,6 @@ public class WorkflowModuleConsts
             new(OrderDelay,"工单延期"),
             new(OrderPrevious,"工单退回"),
             new(OrderScreen,"工单甄别"),
+            new(OrderTerminate,"工单终止"),
         };
 }

+ 5 - 2
src/Hotline/KnowledgeBase/Knowledge.cs

@@ -211,6 +211,9 @@ public class Knowledge : WorkflowEntity//   WorkflowEntity  FullStateEntity
 	[SugarColumn(ColumnDataType = "json", IsJson = true, IsNullable = true)]
 	public List<FileJson>? FileJson { get; set; }
 
-
-
+	/// <summary>
+	/// 收藏数量
+	/// </summary>
+	[SugarColumn(ColumnDescription = "收藏数量")]
+	public int? CollectCount { get; set; } = 0;
 }

+ 6 - 1
src/Hotline/KnowledgeBase/KnowledgeCollect.cs

@@ -36,5 +36,10 @@ namespace Hotline.KnowledgeBase
 		[SugarColumn(ColumnDescription = "是否收藏")]
 		public bool? Collect { get; set; }
 
-	}
+        /// <summary>
+        /// 分组Id
+        /// </summary>
+        [SugarColumn(ColumnDescription = "分组Id")]
+        public string? KnowledgeCollectGroupId { get; set; }
+    }
 }

+ 17 - 0
src/Hotline/KnowledgeBase/KnowledgeCollectGroup.cs

@@ -0,0 +1,17 @@
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.KnowledgeBase;
+
+[Description("知识库收藏分组")]
+public class KnowledgeCollectGroup : FullStateEntity
+{
+    [SugarColumn(ColumnDescription = "分组名称")]
+    public string Name { get; set; }
+}

+ 42 - 0
src/Hotline/KnowledgeBase/KnowledgeHotWord.cs

@@ -0,0 +1,42 @@
+using Hotline.Share.Enums.KnowledgeBase;
+using SqlSugar;
+using System.ComponentModel;
+using XF.Domain.Repository;
+
+namespace Hotline.KnowledgeBase;
+
+[Description("知识热词")]
+public class KnowledgeHotWord : CreationEntity
+{
+    /// <summary>
+    /// 关键字
+    /// </summary>
+    [SugarColumn(ColumnDescription = "关键字")]
+    public string KeyWord { get; set; }
+
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [SugarColumn(ColumnDescription = "排序")]
+    public int Sort { get; set; }
+
+    /// <summary>
+    /// 搜索次数
+    /// </summary>
+    [SugarColumn(ColumnDescription = "搜索次数")]
+    public int SearchCount { get; set; }
+
+    
+    /// <summary>
+    /// 类型
+    /// </summary>
+    [SugarColumn(ColumnDescription = "类型")]
+    public EKnowledgeHotWordType? Type { get; set; }
+
+    /// <summary>
+    /// 是否启用
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否启用")]
+    public bool IsEnable { get; set; }
+}
+

+ 6 - 1
src/Hotline/Orders/IOrderRepository.cs

@@ -185,7 +185,12 @@ namespace Hotline.Orders
 
     }
 
-    public interface IOrderDelayRepository : IRepositoryWorkflow<OrderDelay>
+    public interface IOrderTerminateRepository : IRepositoryWorkflow<OrderTerminate>
+    {
+
+    }
+
+	public interface IOrderDelayRepository : IRepositoryWorkflow<OrderDelay>
     {
 
     }

+ 7 - 1
src/Hotline/Orders/Order.cs

@@ -1080,8 +1080,14 @@ namespace Hotline.Orders
         public List<OrderScreen> OrderScreens { get; set; }
 
         /// <summary>
-        /// 
+        /// 终止
         /// </summary>
+        [Navigate(NavigateType.OneToMany, nameof(OrderTerminate.OrderId))]
+        public List<OrderTerminate> OrderTerminates { get; set; }
+
+		/// <summary>
+		/// 
+		/// </summary>
 		[Navigate(NavigateType.OneToMany, nameof(OrderSpecial.OrderId))]
         public List<OrderSpecial> OrderSpecials { get; set; }
 

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

@@ -205,6 +205,8 @@ public class OrderDomainService : IOrderDomainService, IScopeDependency
             return false;
     }
 
+   
+
     #region 平均派单
     /// <summary>
     /// 平均派单

+ 67 - 0
src/Hotline/Orders/OrderTerminate.cs

@@ -0,0 +1,67 @@
+using Hotline.FlowEngine.Workflows;
+using Hotline.Share.Dtos.File;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.Share.Dtos.Order;
+using XF.Domain.Repository;
+
+namespace Hotline.Orders
+{
+	[Description("工单终止")]
+	public class OrderTerminate : WorkflowEntity
+	{
+		/// <summary>
+		/// 工单id
+		/// </summary>
+		[SugarColumn(ColumnDescription = "工单id")]
+		public string OrderId { get; set; }
+
+		/// <summary>
+		/// 工单编号
+		/// </summary>
+		[SugarColumn(ColumnDescription = "工单编号")]
+		public string No { get; set; }
+
+		/// <summary>
+		/// 终止状态
+		/// </summary>
+		[SugarColumn(ColumnDescription = "终止状态")]
+		public ETerminateStatus? Status { get; set; }
+
+		/// <summary>
+		/// 重提终止
+		/// </summary>
+		[SugarColumn(ColumnDescription = "重提终止",DefaultValue ="f")]
+		public bool IsRecommit { get; set; }
+
+		/// <summary>
+		/// 终止理由
+		/// </summary>
+		[SugarColumn(ColumnDescription = "终止理由", ColumnDataType = "varchar(2000)")]
+		public string? Content { get; set; }
+
+		/// <summary>
+		/// 附件
+		/// </summary>
+		[SugarColumn(ColumnDescription = "附件", ColumnDataType = "json", IsJson = true, IsNullable = true)]
+		public List<FileJson>? FileJson { get; set; }
+
+		/// <summary>
+		/// 工单
+		/// </summary>
+		[Navigate(NavigateType.OneToOne, nameof(OrderId))]
+		public Order Order { get; set; }
+
+		/// <summary>
+		/// 流程
+		/// </summary>
+		[Navigate(NavigateType.OneToOne, nameof(WorkflowId))]
+		public Workflow? Workflow { get; set; }
+	}
+
+}

+ 5 - 0
src/Hotline/Orders/OrderVisit.cs

@@ -161,6 +161,11 @@ public class OrderVisit : CreationEntity
     /// </summary>
     public bool? IsEffectiveAiVisit { get; set; }
 
+    /// <summary>
+    /// 是否批量回访
+    /// </summary>
+    public bool? IsBatchVisit { get; set; }
+
     public void AiVisitTime()
     {
         LastVisitTime = DateTime.Now;

+ 147 - 48
src/Hotline/Permissions/EPermission.cs

@@ -60,6 +60,12 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName ="工单坐席待办",Name ="交办单导出",Description ="交办单导出")]
         JbdExport = 100104,
+        /// <summary>
+        /// 分配工单
+        /// </summary>
+        [Display(GroupName ="工单坐席待办",Name ="分配工单",Description ="分配工单")]
+        AssignOrders = 100105,
+
         #endregion
 
         #region 工单待办
@@ -229,6 +235,11 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName ="工单列表",Name ="工单待办",Description ="工单待办")]
         OrderAgent = 200117,
+        /// <summary>
+        /// 设置撤销
+        /// </summary>
+        [Display(GroupName ="工单列表",Name ="设置撤销",Description ="设置撤销")]
+        SetRevoke = 200118,
         #endregion
 
         #region 延期管理
@@ -765,43 +776,6 @@ namespace Hotline.Permissions
 
         #endregion
 
-        #region 二次回访申请
-        [Display(GroupName = "二次回访申请",Name ="二次回访申请")]
-        OrderVisitAgain = 201000,
-
-        /// <summary>
-        /// 可申请二次回访列表
-        /// </summary>
-        [Display(GroupName = "二次回访申请", Name = "可申请二次回访列表", Description = "可申请二次回访列表")]
-        OrderVisitAgainList = 201001,
-
-        /// <summary>
-        /// 申请二次回访
-        /// </summary>
-        [Display(GroupName = "二次回访申请", Name = "申请二次回访", Description = "申请二次回访")]
-        ApplyOrderVisit = 201002,
-        #endregion
-
-        #region 二次回访审核
-        /// <summary>
-        /// 二次回访审核
-        /// </summary>
-        [Display(GroupName = "二次回访审核",Name = "二次回访审核",Description ="二次回访审核")]
-        VisitApply  = 201100,
-
-        /// <summary>
-        /// 二次回访申请列表
-        /// </summary>
-        [Display(GroupName = "二次回访审核", Name = "二次回访申请列表", Description = "二次回访申请列表")]
-        VisitApplyList = 201101,
-
-        /// <summary>
-        /// 审核二次回访
-        /// </summary>
-        [Display(GroupName = "ExaminOrderVisit", Name = "二次回访审核", Description = "二次回访审核")]
-        ExaminOrderVisit = 201102,
-        #endregion
-
         #region 二次办理
         /// <summary>
         /// 二次办理
@@ -855,6 +829,61 @@ namespace Hotline.Permissions
         [Display(GroupName = "二次办理", Name = "二次办理查询", Description = "二次办理查询")]
         SecondHandleQuery = 201507,
 
+		#endregion
+		#endregion
+
+		#region 终止管理
+
+		/// <summary>
+		/// 终止管理
+		/// </summary>
+		[Display(GroupName = "业务管理", Name = "终止管理", Description = "终止管理")]
+		OrderTerminateManage = 201700,
+
+        #region 终止申请
+        /// <summary>
+        /// 终止申请
+        /// </summary>
+        [Display(GroupName ="业务管理",Name ="终止申请",Description ="终止申请")]
+        TerminateApply = 201701,
+        /// <summary>
+        /// 终止申请
+        /// </summary>
+        [Display(GroupName ="业务管理",Name ="终止申请",Description ="终止申请")]
+        TerminateApplyButton = 201702,
+        #endregion
+
+        #region 终止审批
+        /// <summary>
+        /// 终止审批
+        /// </summary>
+        [Display(GroupName ="业务管理",Name ="终止审批",Description ="终止审批")]
+        ExamineTerminate = 2017203,
+        /// <summary>
+        /// 理由修改
+        /// </summary>
+        [Display(GroupName ="业务管理",Name ="理由修改",Description ="理由修改")]
+        ModifyReason = 201704,
+        /// <summary>
+        /// 终止审批
+        /// </summary>
+        [Display(GroupName ="业务管理",Name ="终止审批",Description ="终止审批")]
+        ExamineTerminateButton = 201705,
+        /// <summary>
+        /// 终止退回
+        /// </summary>
+        [Display(GroupName ="业务管理",Name ="终止退回",Description ="终止退回")]
+        ExamineReturn = 201706,
+
+        #endregion
+
+        #region 终止查询
+        /// <summary>
+        /// 终止查询
+        /// </summary>
+        [Display(GroupName ="业务管理",Name ="终止查询",Description ="终止查询")]
+        QueryTerminate = 201707,
+
         #endregion
         #endregion
 
@@ -875,14 +904,15 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName ="工单修改",Name ="工单修改",Description ="工单修改")]
         ModifyOrder = 201602,
-        #endregion
-        #endregion
+		#endregion
 
-        #region 质检管理(40,00,00)
-        /// <summary>
-        /// 质检管理
-        /// </summary>
-        [Display(GroupName = "质检管理",Name ="质检管理",Description ="质检管理")]
+		#endregion
+
+		#region 质检管理(40,00,00)
+		/// <summary>
+		/// 质检管理
+		/// </summary>
+		[Display(GroupName = "质检管理",Name ="质检管理",Description ="质检管理")]
         QualityManage = 400000,
 
         #region 质检中心
@@ -1432,6 +1462,11 @@ namespace Hotline.Permissions
 
         #region 知识库
         /// <summary>
+        /// 知识审批
+        /// </summary>
+        [Display(GroupName ="知识库",Name ="知识审批",Description ="知识审批")]
+        ExamineKnowledge = 600301,
+        /// <summary>
         /// 新增审核按钮
         /// </summary>
         [Display(GroupName = "知识库", Name = "新增审核按钮", Description = "提交审核按钮")]
@@ -1462,7 +1497,16 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName = "知识库管理", Name = "知识上架", Description = "知识上架")]
         KnowledgeOnTheShelf = 600307,
-
+        /// <summary>
+        /// 导出知识库
+        /// </summary>
+        [Display(GroupName ="知识库管理",Name = "导出知识详情",Description = "导出知识详情")]
+        ExprotKnowledgeInfo = 600308,
+        /// <summary>
+        /// 批量审核
+        /// </summary>
+        [Display(GroupName ="知识库管理",Name ="批量审核",Description ="批量审核")]
+        BatchExamineKnowledge = 600309,
         #endregion
 
         #endregion
@@ -1619,6 +1663,35 @@ namespace Hotline.Permissions
 
         #endregion
 
+        #region 知识浏览
+        [Display(GroupName = "知识库管理", Name = "知识库浏览", Description = "知识库浏览")]
+        BrowseKnowledge = 600900,
+
+        #endregion
+
+        #region 热词管理
+        /// <summary>
+        /// 热词管理
+        /// </summary>
+        [Display(GroupName ="知识库管理",Name ="热词管理",Description ="热词管理")]
+        HotWordsManager = 601000,
+        /// <summary>
+        /// 新增热词
+        /// </summary>
+        [Display(GroupName = "知识库管理", Name = "新增热词", Description = "新增热词")]
+        AddHotWords = 601001,
+        /// <summary>
+        /// 删除热词
+        /// </summary>
+        [Display(GroupName = "知识库管理", Name = "删除热词", Description = "删除热词")]
+        DelHotWords = 601002,
+        /// <summary>
+        /// 修改热词
+        /// </summary>
+        [Display(GroupName = "知识库管理", Name = "修改热词", Description = "修改热词")]
+        ModifyHotWords = 601003,
+        #endregion
+
         #endregion
 
         #region 辅助功能(70,00,00)
@@ -1882,6 +1955,11 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName = "辅助功能",Name ="重新发送",Description ="重新发送")]
         SendAgain = 700802,
+        /// <summary>
+        /// 新增短信
+        /// </summary>
+        [Display(GroupName = "辅助功能", Name = "新增短信", Description = "新增短信")]
+        AddMessage = 700803,
         #endregion
 
         #region 事件分类管理
@@ -2080,7 +2158,15 @@ namespace Hotline.Permissions
         #endregion
         #endregion
 
-        
+        #region 部门通讯录
+        /// <summary>
+        /// 部门通讯录
+        /// </summary>
+        [Display(GroupName = "辅助功能", Name = "部门通讯录", Description = "部门通讯录")]
+        OrgAddressBook = 701600,
+
+        #endregion
+
         #endregion
 
         #region 话务管理(80,00,00)
@@ -2272,6 +2358,11 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName ="数据统计",Name ="坐席动作类型统计",Description ="坐席动作类型统计")]
         SeatActionTypeStatistics = 110108,
+        /// <summary>
+        /// 话务日期统计简单
+        /// </summary>
+        [Display(GroupName = "数据统计", Name = "话务日期统计简单", Description = "话务日期统计简单")]
+        DateCallStatisticsSimple = 110109,
         #endregion
 
         #region 业务统计(11,02,00)
@@ -2541,8 +2632,16 @@ namespace Hotline.Permissions
         /// </summary>
         [Display(GroupName ="数据统计",Name ="部门未签收统计",Description ="部门未签收统计")]
         OrgUnSignedSatisfied = 110607,
-
-
+        /// <summary>
+        /// 部门延期明细
+        /// </summary>
+        [Display(GroupName = "数据统计", Name = "部门延期明细", Description = "部门延期明细")]
+        OrgDelayDetail = 110608,
+        /// <summary>
+        /// 部门满意度明细
+        /// </summary>
+        [Display(GroupName = "数据统计", Name = "部门满意度明细", Description = "部门满意度明细")]
+        OrgSatisfactionDetail = 110609,
         #endregion
 
 

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.