Pārlūkot izejas kodu

Merge branch 'test' into release

xf 3 mēneši atpakaļ
vecāks
revīzija
71cd879518
100 mainītis faili ar 8084 papildinājumiem un 490 dzēšanām
  1. 14 0
      Hotline.sln
  2. 3 1
      src/Hotline.Ai.Jths/AiQualityService.cs
  3. 104 0
      src/Hotline.Ai.XingTang/AiQualityService.cs
  4. 15 0
      src/Hotline.Ai.XingTang/AiXingTangStartupExtensions.cs
  5. 20 0
      src/Hotline.Ai.XingTang/Hotline.Ai.XingTang.csproj
  6. 102 2
      src/Hotline.Api/Controllers/Bi/BiCallController.cs
  7. 666 45
      src/Hotline.Api/Controllers/Bi/BiOrderController.cs
  8. 130 0
      src/Hotline.Api/Controllers/Bi/BiQualityController.cs
  9. 82 2
      src/Hotline.Api/Controllers/Bigscreen/DataScreenController.cs
  10. 70 40
      src/Hotline.Api/Controllers/CallController.cs
  11. 590 0
      src/Hotline.Api/Controllers/CaseController.cs
  12. 10 10
      src/Hotline.Api/Controllers/CommonPController.cs
  13. 143 0
      src/Hotline.Api/Controllers/ExportData/ExportDataController.cs
  14. 84 4
      src/Hotline.Api/Controllers/ExportWordController.cs
  15. 134 6
      src/Hotline.Api/Controllers/FileController.cs
  16. 1 0
      src/Hotline.Api/Controllers/HomeController.cs
  17. 12 1
      src/Hotline.Api/Controllers/IdentityController.cs
  18. 64 19
      src/Hotline.Api/Controllers/JudicialManagementOrdersController.cs
  19. 456 42
      src/Hotline.Api/Controllers/OrderController.cs
  20. 75 0
      src/Hotline.Api/Controllers/OrderModuleControllers/OrderCarbonCopyController.cs
  21. 1 1
      src/Hotline.Api/Controllers/OrderModuleControllers/OrderComplementController.cs
  22. 1 0
      src/Hotline.Api/Controllers/OrderTerminateController.cs
  23. 561 0
      src/Hotline.Api/Controllers/PlanController.cs
  24. 69 9
      src/Hotline.Api/Controllers/QualityController.cs
  25. 290 2
      src/Hotline.Api/Controllers/Snapshot/IndustryController.cs
  26. 102 0
      src/Hotline.Api/Controllers/Snapshot/InviteCodeController.cs
  27. 265 0
      src/Hotline.Api/Controllers/Snapshot/RedPackController.cs
  28. 250 0
      src/Hotline.Api/Controllers/Snapshot/SnapshotBulletinController.cs
  29. 301 15
      src/Hotline.Api/Controllers/Snapshot/SnapshotController.cs
  30. 259 0
      src/Hotline.Api/Controllers/Snapshot/SnapshotOrderController.cs
  31. 4 0
      src/Hotline.Api/Controllers/SysController.cs
  32. 165 7
      src/Hotline.Api/Controllers/TestController.cs
  33. 28 25
      src/Hotline.Api/Controllers/WebPortalController.cs
  34. 261 0
      src/Hotline.Api/Controllers/XthxController.cs
  35. 5 4
      src/Hotline.Api/Hotline.Api.csproj
  36. 1 0
      src/Hotline.Api/Realtimes/RealtimeMethods.cs
  37. 8 0
      src/Hotline.Api/Realtimes/RealtimeService.cs
  38. 18 9
      src/Hotline.Api/StartupExtensions.cs
  39. BIN
      src/Hotline.Api/Template/QualityCertificate.doc
  40. 6 1
      src/Hotline.Api/config/appsettings.Development.json
  41. 2 2
      src/Hotline.Application.Contracts/Validators/Order/AddOrderDtoValidator.cs
  42. 0 2
      src/Hotline.Application.Contracts/Validators/Snapshot/IndustryValidator.cs
  43. 2 1
      src/Hotline.Application.Tests/Application/DefaultCallApplicationTest.cs
  44. 106 0
      src/Hotline.Application.Tests/Application/IndustryApplicationTest.cs
  45. 50 0
      src/Hotline.Application.Tests/Application/InviteCodeApplicationTest.cs
  46. 2 1
      src/Hotline.Application.Tests/Application/KnowApplicationTest.cs
  47. 76 0
      src/Hotline.Application.Tests/Application/OrderSnapshotApplicationTest.cs
  48. 92 0
      src/Hotline.Application.Tests/Application/RedPackApplicationTest.cs
  49. 55 8
      src/Hotline.Application.Tests/Application/SnapshotApplicationMockTest.cs
  50. 355 51
      src/Hotline.Application.Tests/Application/SnapshotApplicationTest.cs
  51. 13 6
      src/Hotline.Application.Tests/Application/SystemSettingCacheManagerTest.cs
  52. 40 0
      src/Hotline.Application.Tests/Application/ThirdIdentifyApplicationTest.cs
  53. 9 10
      src/Hotline.Application.Tests/Controller/DefaultSessionContext.cs
  54. 37 0
      src/Hotline.Application.Tests/Controller/IndustryControllerTest.cs
  55. 2 1
      src/Hotline.Application.Tests/Controller/KnowledgeControllerTest.cs
  56. 72 3
      src/Hotline.Application.Tests/Controller/OrderControllerTest.cs
  57. 54 3
      src/Hotline.Application.Tests/Controller/SnapshotControllerTest.cs
  58. 2 1
      src/Hotline.Application.Tests/Domain/OrderVisitDomainServiceTest.cs
  59. 2 1
      src/Hotline.Application.Tests/Hotline.Application.Tests.csproj
  60. 3 0
      src/Hotline.Application.Tests/Infrastructure/TestSettingConstants.cs
  61. 50 0
      src/Hotline.Application.Tests/Infrastructure/TianQueTest.cs
  62. 24 0
      src/Hotline.Application.Tests/Infrastructure/WeiXinTest.cs
  63. 19 0
      src/Hotline.Application.Tests/Mock/Interfaces/IOrderServiceStartWorkflow.cs
  64. 97 19
      src/Hotline.Application.Tests/Mock/OrderServiceMock.cs
  65. 145 0
      src/Hotline.Application.Tests/Mock/OrderServiceStartWorkflow.cs
  66. 6 2
      src/Hotline.Application.Tests/Mock/ThirdTestService.cs
  67. 0 10
      src/Hotline.Application.Tests/Repository/FWMQRepositoryTest.cs
  68. 14 6
      src/Hotline.Application.Tests/Startup.cs
  69. 39 18
      src/Hotline.Application.Tests/TestBase.cs
  70. 0 1
      src/Hotline.Application.Tests/appsettings.Development.json
  71. 19 4
      src/Hotline.Application/Bigscreen/DataScreenRefreshService.cs
  72. 16 7
      src/Hotline.Application/CallCenter/DefaultCallApplication.cs
  73. 6 6
      src/Hotline.Application/CallCenter/ICallApplication.cs
  74. 690 0
      src/Hotline.Application/Caselibrary/CaseApplication.cs
  75. 90 0
      src/Hotline.Application/Caselibrary/ICaseApplication.cs
  76. 20 0
      src/Hotline.Application/ExportExcel/ExportApplication.cs
  77. 2 0
      src/Hotline.Application/ExportExcel/IExportApplication.cs
  78. 13 1
      src/Hotline.Application/FlowEngine/WorkflowApplication.cs
  79. 2 1
      src/Hotline.Application/Handlers/FlowEngine/WorkflowEndHandler.cs
  80. 32 13
      src/Hotline.Application/Handlers/FlowEngine/WorkflowNextHandler.cs
  81. 45 21
      src/Hotline.Application/Handlers/FlowEngine/WorkflowPreviousHandler.cs
  82. 1 1
      src/Hotline.Application/Hotline.Application.csproj
  83. 7 0
      src/Hotline.Application/Identity/IIdentityAppService.cs
  84. 49 5
      src/Hotline.Application/Identity/IdentityAppService.cs
  85. 13 10
      src/Hotline.Application/JudicialManagement/EnforcementApplication.cs
  86. 1 0
      src/Hotline.Application/Knowledge/KnowApplication.cs
  87. 7 0
      src/Hotline.Application/Mappers/MapperConfigs.cs
  88. 0 2
      src/Hotline.Application/Mappers/OrderMapperConfigs.cs
  89. 74 1
      src/Hotline.Application/Mappers/SnapshotMapperConfigs.cs
  90. 1 1
      src/Hotline.Application/Orders/Handles/OrderHandler/AddOrderPushMessageNotifyHandler.cs
  91. 1 1
      src/Hotline.Application/Orders/Handles/OrderHandler/GetOrderDetailNotifyHandler.cs
  92. 1 1
      src/Hotline.Application/Orders/Handles/OrderHandler/OrderRelateCallHandler.cs
  93. 1 1
      src/Hotline.Application/Orders/Handles/OrderHandler/OrderVisitSmsHandler.cs
  94. 3 3
      src/Hotline.Application/Orders/Handles/OrderHandler/TranspondCityNotifyHandler.cs
  95. 1 1
      src/Hotline.Application/Orders/Handles/OrderHandler/UpdateOrderPushMessageNotifyHandler.cs
  96. 9 9
      src/Hotline.Application/Orders/Handles/OrderScreenHandler/OrderScreenEndWorkflowHandler.cs
  97. 101 0
      src/Hotline.Application/Orders/Handles/OrderScreenHandler/OrderScreenNextWorkflowHandler.cs
  98. 37 0
      src/Hotline.Application/Orders/Handles/OrderScreenHandler/OrderScreenStartWorkflowHandler.cs
  99. 63 0
      src/Hotline.Application/Orders/Handles/SnapshotHandler/GuiderSystemTimeoutHandler.cs
  100. 41 10
      src/Hotline.Application/Orders/IOrderApplication.cs

+ 14 - 0
Hotline.sln

@@ -59,6 +59,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hotline.Logger", "src\Hotli
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hotline.WeChat", "src\Hotline.WeChat\Hotline.WeChat.csproj", "{75215667-65AF-4B7B-85E7-3140239B30CC}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TianQue.Sdk", "src\TianQue.Sdk\TianQue.Sdk.csproj", "{6CF27647-D0E0-4D17-80FB-3EE57864A2B4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hotline.Ai.XingTang", "src\Hotline.Ai.XingTang\Hotline.Ai.XingTang.csproj", "{8E4F64EF-314A-45BA-8BB2-46FF5B06F7D5}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -153,6 +157,14 @@ Global
 		{75215667-65AF-4B7B-85E7-3140239B30CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{75215667-65AF-4B7B-85E7-3140239B30CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{75215667-65AF-4B7B-85E7-3140239B30CC}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6CF27647-D0E0-4D17-80FB-3EE57864A2B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6CF27647-D0E0-4D17-80FB-3EE57864A2B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6CF27647-D0E0-4D17-80FB-3EE57864A2B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6CF27647-D0E0-4D17-80FB-3EE57864A2B4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8E4F64EF-314A-45BA-8BB2-46FF5B06F7D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{8E4F64EF-314A-45BA-8BB2-46FF5B06F7D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{8E4F64EF-314A-45BA-8BB2-46FF5B06F7D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{8E4F64EF-314A-45BA-8BB2-46FF5B06F7D5}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -183,6 +195,8 @@ Global
 		{9F99C272-5BC2-452C-9D97-BC756AF04669} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
 		{37784861-ABC0-41F4-87B4-2E08A89A2C42} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
 		{75215667-65AF-4B7B-85E7-3140239B30CC} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
+		{6CF27647-D0E0-4D17-80FB-3EE57864A2B4} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
+		{8E4F64EF-314A-45BA-8BB2-46FF5B06F7D5} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {4B8EA790-BD13-4422-8D63-D6DBB77B823F}

+ 3 - 1
src/Hotline.Ai.Jths/AiQualityService.cs

@@ -21,8 +21,10 @@ namespace Hotline.Ai.Jths
             _client = new RestClient();
             _baseUrl = baseUrl;
         }
+        public async Task<string> CreateAiOrderQualityTask(string filename, CancellationToken cancellationToken) { return ""; }
 
-        public async Task CreateAiOrderQualityTask(
+
+		public async Task CreateAiOrderQualityTask(
              string id,
              string audioFile,
              string fromNo,

+ 104 - 0
src/Hotline.Ai.XingTang/AiQualityService.cs

@@ -0,0 +1,104 @@
+using RestSharp;
+using Fw.Utility.UnifyResponse;
+using System.Text;
+using System.Security.Cryptography;
+using Hotline.Share.Dtos.Quality;
+using Hotline.Ai.Quality;
+
+namespace Hotline.Ai.XingTang
+{
+    public class AiQualityService : IAiQualityService
+    {
+        private readonly RestClient _client;
+        private readonly string _baseUrl;
+
+        public AiQualityService(string baseUrl)
+        {
+            _client = new RestClient();
+            _baseUrl = baseUrl;
+        }
+
+        public async Task CreateAiOrderQualityTask(
+            string id,
+            string audioFile,
+            string fromNo,
+            DateTime? callStartTime,
+            string viteRecordPrefix,
+            string ywlxString,
+            CancellationToken cancellationToken)
+        { 
+        }
+
+
+		public async Task<string> CreateAiOrderQualityTask(string filename, CancellationToken cancellationToken)
+        {
+            var url = _baseUrl + "/offlinerecog?filename=" + filename;
+			var baseUrl = new Uri(url);
+            return  await ExecuteAsync(baseUrl.ToString(), Method.Get, "", cancellationToken);
+        }
+
+        public async Task<ApiResponse<TResponse>> ExecuteAsync<TRequest, TResponse>(string path, Method httpMethod,
+            TRequest request, CancellationToken cancellationToken)
+            where TRequest : class
+        {
+            var req = new RestRequest(path, httpMethod);
+            if (httpMethod is Method.Get)
+            {
+                req.AddObject(request);
+            }
+            else
+            {
+                req.AddJsonBody(request);
+            }
+
+            try
+            {
+                var response = await _client.ExecuteAsync<ApiResponse<TResponse>>(req, cancellationToken);
+                return response.Data;
+            }
+            catch (Exception e)
+            {
+                throw new HttpRequestException($"智能质检平台错误,Error: {e.Message}");
+            }
+        }
+
+        public async Task<string> ExecuteAsync<TRequest>(string path, Method httpMethod, TRequest request,
+            CancellationToken cancellationToken)
+            where TRequest : class
+        {
+            var req = new RestRequest(path, httpMethod);
+            req.Timeout = new TimeSpan(0,30,0);
+
+            try
+            {
+                var response = await _client.ExecuteAsync<ApiResponse>(req, cancellationToken);
+                return response.Content;
+            }
+            catch (Exception e)
+            {
+                throw new HttpRequestException($"智能质检平台错误,Error: {e.Message}");
+            }
+        }
+
+        /// <summary>
+        /// MD5加密
+        /// </summary>
+        /// <param name="input">需要加密的字符串</param>
+        /// <returns></returns>
+        private static string MD5Encrypt(string? input)
+        {
+            using var md5 = MD5.Create();
+            var t = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
+            var sb = new StringBuilder(32);
+            for (var i = 0; i < t.Length; i++)
+                sb.Append(t[i].ToString("x").PadLeft(2, '0'));
+            return sb.ToString();
+        }
+
+        private static string Base64En(string? model)
+        {
+            var bytes = Encoding.UTF8.GetBytes(model);
+            return Convert.ToBase64String(bytes);
+        }
+    }
+}

+ 15 - 0
src/Hotline.Ai.XingTang/AiXingTangStartupExtensions.cs

@@ -0,0 +1,15 @@
+using Hotline.Ai.Quality;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Hotline.Ai.XingTang
+{
+	public static class AiXingTangStartupExtensions
+	{
+		public  static IServiceCollection AddAiXingTang(this IServiceCollection services, string baseUrl)
+		{
+			services.AddSingleton<IAiQualityService, AiQualityService>(_ => new AiQualityService(baseUrl));
+
+			return services;
+		}
+	}
+}

+ 20 - 0
src/Hotline.Ai.XingTang/Hotline.Ai.XingTang.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Fw.Utility.UnifyResponse" Version="1.0.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
+    <PackageReference Include="RestSharp" Version="112.1.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Hotline.Share\Hotline.Share.csproj" />
+    <ProjectReference Include="..\Hotline\Hotline.csproj" />
+  </ItemGroup>
+
+</Project>

+ 102 - 2
src/Hotline.Api/Controllers/Bi/BiCallController.cs

@@ -22,6 +22,7 @@ using XF.Domain.Repository;
 using Hotline.Settings;
 using XF.Utility.EnumExtensions;
 using Hotline.Share.Enums.CallCenter;
+using Hotline.Repository.SqlSugar.Extensions;
 
 namespace Hotline.Api.Controllers.Bi;
 
@@ -575,7 +576,7 @@ public class BiCallController : BaseController
             Date = "合计",
             PersonCallInCount = list.Sum(x=>x.PersonCallInCount),
             PersonCallInPutthroughCount = list.Sum(x=>x.PersonCallInPutthroughCount),
-            PersonRingOffCount = list.Sum(x=>x.PersonRingOffCount),//个人服务挂断
+            //PersonRingOffCount = list.Sum(x=>x.PersonRingOffCount),//个人服务挂断
             PersonQueueOffCount = list.Sum(x=>x.PersonQueueOffCount),//个人服务队列挂断
             PersonWaitOffCount = list.Sum(x=>x.PersonWaitOffCount) //个人服务等待挂断
         };
@@ -608,7 +609,7 @@ public class BiCallController : BaseController
             Date = "合计",
             EnterpriseCallInCount = list.Sum(x=>x.EnterpriseCallInCount),
             EnterpriseCallInPutthroughCount = list.Sum(x=>x.EnterpriseCallInPutthroughCount),
-            EnterpriseRingOffCount = list.Sum(x=>x.EnterpriseRingOffCount), //企业挂断
+            //EnterpriseRingOffCount = list.Sum(x=>x.EnterpriseRingOffCount), //企业挂断
             EnterpriseQueueOffCount = list.Sum(x=>x.EnterpriseQueueOffCount),//个人服务队列挂断
             EnterpriseWaitOffCount = list.Sum(x=>x.EnterpriseWaitOffCount) //个人服务等待挂断
         };
@@ -671,5 +672,104 @@ public class BiCallController : BaseController
                 await _callReportApplication.QueryCallOutDateStatisticsDetail(dto.QueryDto,enterpriseTels)
             , "呼出话务统计明细", "Date");
     }
+
+    /// <summary>
+    /// 坐席月接通率统计
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("query-seat-monthcall")]
+    public async Task<object> QuerySeatMonthCall([FromQuery]QuerySeatMonthCallRequest dto)
+    {
+        var list =  await _callReportApplication.QuerySeatMonthCall(dto);
+        var total = new QuerySeatMonthCallResp()
+        {
+            Name = "合计",
+            InAnswered = list.Sum(x => x.InAnswered),
+            InAvailableAnswer = list.Sum(x => x.InAvailableAnswer),
+            InHangupImmediateWhenAnswered = list.Sum(x => x.InHangupImmediateWhenAnswered),
+            OverTimeImmediate = list.Sum(x => x.OverTimeImmediate),
+            InTimeImmediate = list.Sum(x => x.InTimeImmediate),
+            InHanguped = list.Sum(x => x.InHanguped),
+            InHangupImmediate = list.Sum(x => x.InHangupImmediate),
+            OverTimeInHanguped = list.Sum(x => x.OverTimeInHanguped),
+        };
+
+        return new { List = list, Total = total };
+    }
+
+    /// <summary>
+    /// 坐席月接通率统计基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("query-seat-monthcall-basedata")]
+    public async Task<object> QuerySeatMonthCallBaseData()
+    {
+        return new
+        {
+            SeatUser =await _userRepository.Queryable().Where(x => x.UserType == Share.Enums.User.EUserType.Seat).ToListAsync()            
+        };
+    }
+
+    /// <summary>
+    /// 坐席月接通率统计导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("query-seat-monthcall/export")]
+    public async Task<FileStreamResult> QuerySeatMonthCallExport([FromBody] ExportExcelDto<QuerySeatMonthCallRequest> dto)
+    =>  _exportApplication.GetExcelFile(
+            dto,              
+            await _callReportApplication.QuerySeatMonthCall(dto.QueryDto)
+            , "坐席月接通率统计", "Name");
+
+
+    /// <summary>
+    /// 坐席月接通明细
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("query-seat-monthcall-detail")]
+    public async Task<PagedDto<QuerySeatMonthCallDetailResp>> QuerySeatMonthCallDetail([FromQuery] QuerySeatMonthCallDetailRequest dto)
+    {
+        var query = _callReportApplication.QuerySeatMonthCallDetail(dto);
+        var(total, items) =  await query.ToPagedListAsync(dto.PageIndex, dto.PageSize);
+        return new PagedDto<QuerySeatMonthCallDetailResp>(total, items);
+    }
+
+    /// <summary>
+    /// 坐席月接通明细导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("query-seat-monthcall-detail/export")]
+    public async Task<FileStreamResult> QuerySeatMonthCallDetailExport([FromBody] ExportExcelDto<QuerySeatMonthCallDetailRequest> dto)
+    {
+        var query = _callReportApplication.QuerySeatMonthCallDetail(dto.QueryDto);
+        List<QuerySeatMonthCallDetailResp> 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<QuerySeatMonthCallDetailResp>>(data);
+
+        dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+        var dtos = dataDtos
+            .Select(stu => _mapper.Map(stu, typeof(QuerySeatMonthCallDetailResp), dynamicClass))
+            .Cast<object>()
+            .ToList();
+
+        var stream = ExcelHelper.CreateStream(dtos);
+
+        return ExcelStreamResult(stream, "坐席月接通明细");
+    }
+
     #endregion
 }

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 666 - 45
src/Hotline.Api/Controllers/Bi/BiOrderController.cs


+ 130 - 0
src/Hotline.Api/Controllers/Bi/BiQualityController.cs

@@ -0,0 +1,130 @@
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos;
+using Hotline.Share.Requests;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using Hotline.Application.Quality;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Share.Dtos.Quality;
+using Hotline.Tools;
+using Hotline.Quality;
+using Hotline.Share.Enums.Quality;
+
+namespace Hotline.Api.Controllers.Bi
+{
+	public class BiQualityController : BaseController
+	{
+		private readonly IMapper _mapper;
+		private readonly IQualityApplication _qualityApplication;
+		private readonly IQualityRepository _qualityRepository;
+
+
+		public BiQualityController(
+			IMapper mapper,
+			IQualityApplication qualityApplication,
+			IQualityRepository qualityRepository
+			)
+		{
+			_mapper = mapper;
+			_qualityApplication = qualityApplication;
+			_qualityRepository = qualityRepository;
+		}
+
+
+		/// <summary>
+		/// 坐席质检分析
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("seats_quality_analyse")]
+		public async Task<PagedDto<SeatsQualityAnalyseDto>> SeatsQualityAnalyse([FromQuery] PagedKeywordRequest dto)
+		{
+			var (total, items) = await _qualityApplication.SeatsQualityAnalyse(dto, HttpContext.RequestAborted).ToPagedListAsync(dto, HttpContext.RequestAborted);
+
+			return new PagedDto<SeatsQualityAnalyseDto>(total, _mapper.Map<IReadOnlyList<SeatsQualityAnalyseDto>>(items));
+		}
+
+		/// <summary>
+		/// 坐席质检分析导出
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpPost("seats_quality_analyse/export")]
+		public async Task<FileStreamResult> SeatsQualityAnalyseExport([FromBody] ExportExcelDto<PagedKeywordRequest> dto)
+		{
+			var query = _qualityApplication.SeatsQualityAnalyse(dto.QueryDto, HttpContext.RequestAborted);
+
+			List<Hotline.Quality.Quality> lists;
+			if (dto.IsExportAll)
+			{
+				lists = await query.ToListAsync(HttpContext.RequestAborted);
+			}
+			else
+			{
+				var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+				lists = items;
+			}
+
+			var listDtos = _mapper.Map<ICollection<SeatsQualityAnalyseDto>>(lists);
+
+			dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+			var dtos = listDtos
+				.Select(stu => _mapper.Map(stu, typeof(SeatsQualityAnalyseDto), dynamicClass))
+				.Cast<object>()
+				.ToList();
+
+			var stream = ExcelHelper.CreateStream(dtos);
+
+			return ExcelStreamResult(stream, "坐席质检分析");
+
+		}
+
+		#region 质检工作分析
+
+		/// <summary>
+		/// 质检工单概览
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpGet("quality_order_overview")]
+		public async Task<List<QualityOrderOverviewDto>> QualityOrderOverview([FromQuery] QualityWorkAnalysisRequest dto)
+		{
+			var allOrderNum =  await _qualityRepository.Queryable().Where(x => x.Source == EQualitySource.Accepted && x.QualityTime >= dto.StartTime && x.QualityTime <= dto.EndTime && x.State == EQualityState.End).CountAsync();
+			var items = await _qualityApplication.QualityOrderOverview(dto,allOrderNum, HttpContext.RequestAborted).ToListAsync(HttpContext.RequestAborted);
+			return items;
+		}
+
+		/// <summary>
+		/// 质检工单概览导出
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpPost("quality_order_overview/export")]
+		public async Task<FileStreamResult> QualityOrderOverviewExport([FromBody] ExportExcelDto<QualityWorkAnalysisRequest> dto)
+		{
+			var allOrderNum = await _qualityRepository.Queryable().Where(x => x.Source == EQualitySource.Accepted && x.QualityTime >= dto.QueryDto.StartTime && x.QualityTime <= dto.QueryDto.EndTime && x.State == EQualityState.End).CountAsync();
+			var query = _qualityApplication.QualityOrderOverview(dto.QueryDto, allOrderNum, HttpContext.RequestAborted);
+
+			List<QualityOrderOverviewDto> listDtos;
+
+			listDtos = await query.ToListAsync(HttpContext.RequestAborted);
+
+			//var listDtos = _mapper.Map<ICollection<SeatsQualityAnalyseDto>>(lists);
+
+			dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+			var dtos = listDtos
+				.Select(stu => _mapper.Map(stu, typeof(SeatsQualityAnalyseDto), dynamicClass))
+				.Cast<object>()
+				.ToList();
+
+			var stream = ExcelHelper.CreateStream(dtos);
+
+			return ExcelStreamResult(stream, "质检工单概览");
+
+		}
+
+		#endregion
+	}
+}

+ 82 - 2
src/Hotline.Api/Controllers/Bigscreen/DataScreenController.cs

@@ -1,18 +1,22 @@
-using Hotline.Configurations;
+using DocumentFormat.OpenXml.Drawing;
+using Hotline.Configurations;
 using Hotline.KnowledgeBase;
 using Hotline.Orders;
 using Hotline.Repository.SqlSugar.Orders;
 using Hotline.Settings;
 using Hotline.Settings.Hotspots;
+using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Bigscreen;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Enums.KnowledgeBase;
 using Hotline.Share.Enums.Order;
+using JiebaNet.Segmenter.Common;
 using MapsterMapper;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Options;
 using SqlSugar;
+using System;
 using XF.Domain.Repository;
 
 namespace Hotline.Api.Controllers.Bigscreen
@@ -28,11 +32,13 @@ namespace Hotline.Api.Controllers.Bigscreen
         private readonly IRepository<OrderVisitDetail> _orderVisitDetailRepository;
         private readonly IRepository<SystemArea> _systemAreaRepository;
         private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
+        private readonly IRepository<OrderSecondaryHandling> _orderSecondaryHandlingRepository;
 
         public DataScreenController(IOrderRepository orderRepository, IRepository<OrderVisit> orderVisitRepository,
             IRepository<OrderDelay> orderDelayRepository, IRepository<Knowledge> knowledgeRepository, IRepository<KnowledgePv> knowledgePvRepository,
             IMapper mapper, IRepository<OrderVisitDetail> orderVisitDetailRepository, IRepository<SystemArea> systemAreaRepository,
-            IOptionsSnapshot<AppConfiguration> appOptions)
+            IOptionsSnapshot<AppConfiguration> appOptions,
+            IRepository<OrderSecondaryHandling> orderSecondaryHandlingRepository)
         {
             _orderRepository = orderRepository;
             _orderVisitRepository = orderVisitRepository;
@@ -43,6 +49,7 @@ namespace Hotline.Api.Controllers.Bigscreen
             _orderVisitDetailRepository = orderVisitDetailRepository;
             _systemAreaRepository = systemAreaRepository;
             _appOptions = appOptions;
+            _orderSecondaryHandlingRepository = orderSecondaryHandlingRepository;
         }
 
         /// <summary>
@@ -590,5 +597,78 @@ namespace Hotline.Api.Controllers.Bigscreen
                 return list;
             }
         }
+
+        /// <summary>
+        /// 二次办理统计
+        /// </summary>
+        /// <param name="StartTime"></param>
+        /// <param name="EndTime"></param>
+        /// <returns></returns>
+        [AllowAnonymous]
+        [HttpGet("order-secondary-statistics")]
+        public async Task<SecondaryProcessingOrderStatisticsDto> OrderSecondaryStatistics(DateTime StartTime, DateTime EndTime)
+        {
+            DateTime? dateTime = DateTime.Now;
+            EndTime = EndTime.AddDays(1).AddSeconds(-1);
+            var data = new SecondaryProcessingOrderStatisticsDto
+            {
+                OrderCount = await _orderSecondaryHandlingRepository.Queryable()
+                 .Where(x => x.AuditTime >= StartTime && x.AuditTime <= EndTime && x.State != ESecondaryHandlingState.NotApply
+                 && x.State != ESecondaryHandlingState.Apply && x.State != ESecondaryHandlingState.Refuse).CountAsync(),
+
+                OrderOverdueCount = await _orderSecondaryHandlingRepository.Queryable()
+                .Includes(x => x.Order)
+                 //.Where(x => x.Order.Status < EOrderStatus.Filed)
+                 .Where(x => x.Order.ExpiredTime != null &&
+                        (((x.Order.Status == EOrderStatus.Filed || x.Order.Status == EOrderStatus.Published || x.Order.Status == EOrderStatus.Visited) &&
+                           x.Order.FiledTime >= x.Order.ExpiredTime) ||
+                         ((x.Order.Status != EOrderStatus.Filed && x.Order.Status != EOrderStatus.Published && x.Order.Status != EOrderStatus.Visited) &&
+                          dateTime >= x.Order.ExpiredTime.Value)))
+                 .Where(x => x.AuditTime >= StartTime && x.AuditTime <= EndTime
+                 && x.State != ESecondaryHandlingState.NotApply
+                 && x.State != ESecondaryHandlingState.Apply && x.State != ESecondaryHandlingState.Refuse)
+                 .CountAsync(),
+
+                OrderSoonOverdueCount = await _orderSecondaryHandlingRepository.Queryable()
+                .Includes(x => x.Order)
+                 .Where(x => x.Order.Status < EOrderStatus.Filed && dateTime > x.Order.NearlyExpiredTime && dateTime < x.Order.ExpiredTime)
+                 .Where(x => x.AuditTime >= StartTime && x.AuditTime <= EndTime
+                 && x.State != ESecondaryHandlingState.NotApply
+                 && x.State != ESecondaryHandlingState.Apply && x.State != ESecondaryHandlingState.Refuse)
+                 .CountAsync()
+            };
+            var da = await _orderSecondaryHandlingRepository.Queryable()
+                .LeftJoin<OrderVisit>((os, ov) => os.OrderId == ov.OrderId)
+                    .LeftJoin<OrderVisitDetail>((os, ov, od) => ov.Id == od.VisitId)
+                    .Where((os, ov, od) => ov.VisitState == EVisitState.Visited && od.VisitTarget == EVisitTarget.Org && ov.VisitTime >= StartTime && ov.VisitTime <= EndTime
+                    && os.State != ESecondaryHandlingState.NotApply
+                    && os.State != ESecondaryHandlingState.Apply && os.State != ESecondaryHandlingState.Refuse)
+                    .Select((os, ov, od) => new SecondarySatisfactionDto()
+                    {
+                        Count = SqlFunc.AggregateCount(os.Id),
+                        NoSatisfiedCount = SqlFunc.AggregateSum(SqlFunc.IIF(SqlFunc.JsonField(od.OrgProcessingResults, "Key") == "1"
+                        || SqlFunc.JsonField(od.OrgProcessingResults, "Key") == "2", 1, 0)),//不满意数
+                    }).FirstAsync();
+            if (da != null)
+                data.SatisfactionRate = da.SatisfiedRate;
+            return data;
+        }
+
+        /// <summary>
+        /// 二次办理中工单概览
+        /// </summary>
+        /// <returns></returns>
+        [AllowAnonymous]
+        [HttpGet("order-secondary-handling-query")]
+        public async Task<List<OrderSecondaryHandlingDto>> OrderSecondaryHandlingDetailQuery()
+        {
+            var quer = await _orderSecondaryHandlingRepository.Queryable()
+                 .Includes(x => x.Order)
+                  .Where(x => x.CreationTime.Date == DateTime.Now.Date)
+                  .OrderByDescending(x => x.CreationTime)
+                  .Take(50)
+                 .ToListAsync();
+            return _mapper.Map<List<OrderSecondaryHandlingDto>>(quer);
+        }
     }
 }

+ 70 - 40
src/Hotline.Api/Controllers/CallController.cs

@@ -13,11 +13,15 @@ using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.Quality;
 using Hotline.Share.Dtos.TrCallCenter;
 using Hotline.Share.Enums.CallCenter;
+using Hotline.Repository.SqlSugar.Extensions;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Options;
 using System.Threading;
 using XF.Domain.Exceptions;
 using XF.Utility.EnumExtensions;
+using XF.Domain.Repository;
+using MapsterMapper;
+using SqlSugar;
 
 namespace Hotline.Api.Controllers
 {
@@ -25,33 +29,39 @@ namespace Hotline.Api.Controllers
     {
         private readonly ICallApplication _callApplication;
         private readonly Publisher _publisher;
+        private readonly IMapper _mapper;
         private readonly IOptionsSnapshot<CallCenterConfiguration> _callcenterOptions;
         private readonly ISystemSettingCacheManager _systemSettingCacheManager;
+        private readonly IRepository<TelOperationXthx> _telOperationXthxRepository;
 
-		public CallController(
+        public CallController(
             ICallApplication callApplication,
             Publisher publisher,
+            IMapper mapper,
             IOptionsSnapshot<CallCenterConfiguration> callcenterOptions,
-            ISystemSettingCacheManager systemSettingCacheManager)
+            ISystemSettingCacheManager systemSettingCacheManager,
+            IRepository<TelOperationXthx> telOperationXthxRepository)
         {
             _callApplication = callApplication;
             _publisher = publisher;
+            _mapper = mapper;
             _callcenterOptions = callcenterOptions;
             _systemSettingCacheManager = systemSettingCacheManager;
-		}
+            _telOperationXthxRepository = telOperationXthxRepository;
+        }
 
         /// <summary>
         /// 查询分机
         /// </summary>
         [HttpGet("tels")]
         public async Task<IList<TelDto>> QueryTels()
-            => (await _callApplication.QueryTelsAsync(new QueryTelsInDto(null) { PageSize = 99999}, HttpContext.RequestAborted)).Item2;
+            => (await _callApplication.QueryTelsAsync(new QueryTelsInDto(null) { PageSize = 99999 }, HttpContext.RequestAborted)).Item2;
 
         /// <summary>
         /// 查询分机
         /// </summary>
         [HttpGet("tels-paged")]
-        public async Task<PagedDto<TelDto>> QueryTelsAsync([FromQuery]QueryTelsInDto dto)
+        public async Task<PagedDto<TelDto>> QueryTelsAsync([FromQuery] QueryTelsInDto dto)
             => (await _callApplication.QueryTelsAsync(dto, HttpContext.RequestAborted)).ToPaged();
 
         /// <summary>
@@ -112,7 +122,6 @@ namespace Hotline.Api.Controllers
         /// </summary>
         /// <param name="callId"></param>
         /// <returns></returns>
-        /// <exception cref="NotImplementedException"></exception>
         [HttpGet("{callId}")]
         public Task<List<CallNative>> GetCall(string callId)
         {
@@ -141,7 +150,7 @@ namespace Hotline.Api.Controllers
         /// <param name="dto"></param>
         /// <returns></returns>
         [HttpGet("tel-operations-fixed")]
-        public Task<IReadOnlyList<TelOperation>> QueryTelOperationsFixed([FromQuery] QueryTelOperationsFixedDto dto) => 
+        public Task<IReadOnlyList<TelOperation>> QueryTelOperationsFixed([FromQuery] QueryTelOperationsFixedDto dto) =>
             _callApplication.QueryTelOperationsAsync(dto, HttpContext.RequestAborted);
 
         /// <summary>
@@ -156,8 +165,6 @@ namespace Hotline.Api.Controllers
             };
         }
 
-
-
         /// <summary>
         /// 通话转写
         /// </summary>
@@ -166,36 +173,59 @@ namespace Hotline.Api.Controllers
         [HttpPost("calls/transliteration")]
         public async Task CallTransliteration([FromBody] CallTransliteration dto)
         {
-	        foreach (var id in dto.Ids)
-	        {
-				var call = await _callApplication.GetTianrunCallAsync(id, HttpContext.RequestAborted);
-				if (call is null)
-					throw UserFriendlyException.SameMessage("通话信息错误");
-				if (call.TransliterationState == ECallTransliterationState.Underway)
-					throw UserFriendlyException.SameMessage("正在转写中,请勿重复点击,请稍作等待");
-				if (call.TransliterationState == ECallTransliterationState.Succeed)
-					throw UserFriendlyException.SameMessage("转写成功,不能重新转写");
-				call.InitTransliterationId();
-				await _callApplication.EditTransliterationStateAsync(call.Id, ECallTransliterationState.Underway, call.TransliterationId, HttpContext.RequestAborted);
-				var audioFile = call.RecordingAbsolutePath;
-				var fromNo = call.CPN;
-				var callStartTime = call.CreatedTime;
-				var Id = call.TransliterationId;
-				var setting = _systemSettingCacheManager.GetSetting(SettingConstants.ViteRecordPrefix);
-
-
-
-				var handler = new AiQualityHandler()
-				{
-					Id = Id,
-					Source = "AiAnswered",
-					AudioFile = audioFile,
-					FromNo = fromNo,
-					CallStartTime = callStartTime,
-					ViteRecordPrefix = setting?.SettingValue[0],
-				};
-				await _publisher.PublishAsync(new AiOrderQualityNotify(handler), PublishStrategy.ParallelNoWait, HttpContext.RequestAborted);
-			}
+            foreach (var id in dto.Ids)
+            {
+                var call = await _callApplication.GetTianrunCallAsync(id, HttpContext.RequestAborted);
+                if (call is null)
+                    throw UserFriendlyException.SameMessage("通话信息错误");
+                if (call.TransliterationState == ECallTransliterationState.Underway)
+                    throw UserFriendlyException.SameMessage("正在转写中,请勿重复点击,请稍作等待");
+                if (call.TransliterationState == ECallTransliterationState.Succeed)
+                    throw UserFriendlyException.SameMessage("转写成功,不能重新转写");
+                call.InitTransliterationId();
+                await _callApplication.EditTransliterationStateAsync(call.Id, ECallTransliterationState.Underway, call.TransliterationId, HttpContext.RequestAborted);
+                var audioFile = call.RecordingAbsolutePath;
+                var fromNo = call.CPN;
+                var callStartTime = call.CreatedTime;
+                var Id = call.TransliterationId;
+                var setting = _systemSettingCacheManager.GetSetting(SettingConstants.ViteRecordPrefix);
+
+
+
+                var handler = new AiQualityHandler()
+                {
+                    Id = Id,
+                    Source = "AiAnswered",
+                    AudioFile = audioFile,
+                    FromNo = fromNo,
+                    CallStartTime = callStartTime,
+                    ViteRecordPrefix = setting?.SettingValue[0],
+                };
+                await _publisher.PublishAsync(new AiOrderQualityNotify(handler), PublishStrategy.ParallelNoWait, HttpContext.RequestAborted);
+            }
+        }
+
+        #region 坐席动作类型统计
+
+        /// <summary>
+        /// 坐席动作类型统计
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("telaction/list")]
+        public async Task<PagedDto<TelActionListXthx>> TelActionList([FromQuery] TelActionXthxDto dto)
+        {
+            var (total, items) = await _telOperationXthxRepository.Queryable()
+                .WhereIF(string.IsNullOrEmpty(dto.TelNo) == false, x => x.TelNo.Contains(dto.TelNo))
+                .WhereIF(dto.OperationStatus != null, x => x.OperationStatus == dto.OperationStatus)
+                .WhereIF(string.IsNullOrEmpty(dto.UserName) == false, x => x.UserName.Contains(dto.UserName))
+                .WhereIF(dto.StartTime.HasValue, x => x.StartTime >= dto.StartTime)
+                .WhereIF(dto.EndTime.HasValue, x => x.StartTime <= dto.EndTime)
+                .OrderByDescending(x => x.StartTime)
+                .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+            return new PagedDto<TelActionListXthx>(total, _mapper.Map<IReadOnlyList<TelActionListXthx>>(items));
         }
-	}
+
+        #endregion
+    }
 }

+ 590 - 0
src/Hotline.Api/Controllers/CaseController.cs

@@ -0,0 +1,590 @@
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Authentications;
+using XF.Domain.Repository;
+using Hotline.Application.Caselibrary;
+using Hotline.Share.Dtos.Caselibrary;
+using SqlSugar;
+using Hotline.CaseLibrary;
+using XF.Domain.Exceptions;
+using Hotline.Share.Dtos;
+using Hotline.Share.Tools;
+using Hotline.Share.Enums.Caselibrary;
+using Hotline.Application.ExportWord;
+using Hotline.Application.Tools;
+using Hotline.Share.Enums.Article;
+using XF.Utility.EnumExtensions;
+using Hotline.Share.Dtos.Order;
+using Hotline.Application.ExportExcel;
+
+namespace Hotline.Api.Controllers
+{
+    /// <summary>
+    /// 案例库
+    /// </summary>
+    public class CaseController : BaseController
+    {
+
+        #region 注入
+
+        private readonly IMapper _mapper;
+        private readonly ISessionContext _sessionContext;
+        private readonly ICaseApplication _caseApplication;
+        private readonly IRepository<CaseType> _caseTypeRepository;
+        private readonly IRepository<CaseList> _caseListRepository;
+        private readonly IRepository<CaseCollect> _caseCollectRepository;
+        private readonly IWordHelperService _wordHelperService;
+        private readonly IExportApplication _exportApplication;
+
+
+        public CaseController(
+           IMapper mapper,
+           ISessionContext sessionContext,
+           ICaseApplication CaseApplication,
+           IRepository<CaseType> CaseTypeRepository,
+           IRepository<CaseList> CaseListRepository,
+           IRepository<CaseCollect> CaseCollectRepository,
+           IWordHelperService wordHelperService,
+           IExportApplication exportApplication)
+        {
+            _mapper = mapper;
+            _sessionContext = sessionContext;
+            _caseApplication = CaseApplication;
+            _caseTypeRepository = CaseTypeRepository;
+            _caseListRepository = CaseListRepository;
+            _caseCollectRepository = CaseCollectRepository;
+            _wordHelperService = wordHelperService;
+            _exportApplication = exportApplication;
+        }
+
+        #endregion
+
+        #region 案例库类型管理
+
+        /// <summary>
+        /// 获取列表层级分类
+        /// </summary>
+        /// <param name="IsEnable">是否启用</param>
+        /// <returns></returns>
+        [HttpGet("type/treelist")]
+        public async Task<List<CaseTypeDto>> QueryAllTreeList(bool? IsEnable)
+        {
+            return await _caseTypeRepository.Queryable()
+                .WhereIF(IsEnable.HasValue, x => x.IsEnable == IsEnable)
+                .Where(x => SqlFunc.Subqueryable<CaseTypeOrg>().Where(to => to.TypeId == x.Id).Any() ||
+                SqlFunc.Subqueryable<CaseTypeOrg>().Where(to => to.TypeId == x.Id).NotAny()
+                )
+                .Select(x => new CaseTypeDto()
+                {
+                    Id = x.Id.SelectAll()
+                }
+                )
+                .OrderBy(x => x.Sort).ToTreeAsync(it => it.children, it => it.ParentId, null, it => it.Id);
+        }
+
+        /// <summary>
+        /// 新增
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("type/add")]
+        public async Task<string> AddType([FromBody] AddCaseTypeDto dto)
+        {
+            return await _caseApplication.AddTypeAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 编辑
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("type/update")]
+        public async Task UpdateType([FromBody] UpdateCaseTypeDto dto)
+        {
+            await _caseApplication.UpdateTypeAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 查询详情
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        [HttpGet("type/info/{Id}")]
+        public async Task<CaseType> GetType(string Id)
+        {
+            var types = await _caseTypeRepository.Queryable()
+                .Includes(x => x.CaseTypeOrgs)   // 填充子对象
+                .Where(x => x.Id == Id)
+                .FirstAsync(HttpContext.RequestAborted);
+            if (types is null)
+                throw UserFriendlyException.SameMessage("查询失败!");
+            return types;
+        }
+
+        /// <summary>
+        /// 删除
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        [HttpDelete("type/remove/{Id}")]
+        public async Task RemoveType(string Id)
+        {
+            await _caseApplication.RemoveTypeAsync(Id, HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+        #region 案例库管理
+
+        /// <summary>
+        /// 案例库分类列表
+        /// </summary>
+        /// <param name="IsEnable"></param>
+        /// <returns></returns>
+        [HttpGet("list/treelist")]
+        public async Task<List<CaseTypeDto>> QueryAllCaseTypeTreeList(bool? IsEnable)
+        {
+            return await _caseTypeRepository.Queryable()
+                .WhereIF(IsEnable.HasValue, x => x.IsEnable == IsEnable)
+                .Where(x => SqlFunc.Subqueryable<CaseTypeOrg>().Where(to => to.TypeId == x.Id).Any() ||
+                SqlFunc.Subqueryable<CaseTypeOrg>().Where(to => to.TypeId == x.Id).NotAny()
+                )
+                .Select(x => new CaseTypeDto()
+                {
+                    Id = x.Id.SelectAll(),
+                    CaseNum = SqlFunc.Subqueryable<CaseRelationType>().LeftJoin<CaseList>((kr, k) => kr.CaseId == k.Id)
+                         .Where((kr, k) => kr.CaseTypeId == x.Id)
+                         .DistinctCount(kr => kr.CaseId)
+                }
+                )
+                .OrderBy(x => x.Sort).ToTreeAsync(it => it.children, it => it.ParentId, null, it => it.Id);
+        }
+
+        /// <summary>
+        /// 案例库列表
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <returns></returns>
+        [HttpGet("list")]
+        public async Task<PagedDto<CaseDataDto>> QueryAllCaseList([FromQuery] CaseListDto pagedDto)
+        {
+            return (await _caseApplication.QueryAllCaseListAsync(pagedDto, HttpContext.RequestAborted)).ToPaged();
+        }
+
+        /// <summary>
+        /// 案例库草稿
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("list/draft")]
+        public async Task<string> CaseDraft([FromBody] AddCaseListDto dto)
+        {
+            dto.Status = ECaseStatus.NewDrafts;
+            return await _caseApplication.AddCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库草稿修改
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/draftupdate")]
+        public async Task UpdateCaseDraft([FromBody] UpdateCaseListDto dto)
+        {
+            dto.Status = ECaseStatus.NewDrafts;
+            await _caseApplication.UpdateCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库草稿上架到审核
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/draftadd")]
+        public async Task AddCaseDraft([FromBody] UpdateCaseListDto dto)
+        {
+            dto.ApplyStatus = ECaseApplyStatus.Add;
+            dto.Status = ECaseStatus.Auditing;
+            await _caseApplication.UpdateCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库草稿删除
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        [HttpDelete("list/draftremove/{Id}")]
+        public async Task RemovePlanDraft(string Id)
+        {
+            UpdateCaseListDto dto = new UpdateCaseListDto();
+            dto.ApplyStatus = ECaseApplyStatus.Delete;
+            dto.Status = ECaseStatus.Auditing;
+            dto.Id = Id;
+            await _caseApplication.RemoveCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库删除
+        /// </summary>
+        /// <param name="dtoDel"></param>
+        /// <returns></returns>
+        [HttpDelete("list/remove")]
+        public async Task RemovePlan([FromBody] DelCaseListDto dtoDel)
+        {
+            UpdateCaseListDto dto = new UpdateCaseListDto();
+            dto.ApplyStatus = ECaseApplyStatus.Delete;
+            dto.Status = ECaseStatus.Auditing;
+            dto.Id = dtoDel.Id;
+            dto.ApplyReason = dtoDel.ApplyReason;
+            await _caseApplication.UpdateCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库新增到审核
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("list/add")]
+        public async Task<string> AddCase([FromBody] AddCaseListDto dto)
+        {
+            dto.ApplyStatus = ECaseApplyStatus.Add;
+            dto.Status = ECaseStatus.Auditing;
+            return await _caseApplication.AddCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库编辑提交到审核
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/update")]
+        public async Task UpdateCase([FromBody] UpdateCaseListDto dto)
+        {
+            dto.ApplyStatus = ECaseApplyStatus.Update;
+            dto.Status = ECaseStatus.Auditing;
+            await _caseApplication.UpdateCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库上架
+        /// </summary>
+        /// <param name="Id">案例库ID</param>
+        /// <returns></returns>
+        [HttpGet("list/onshelf/{Id}")]
+        public async Task OnshelfPlan(string Id)
+        {
+            UpdateCaseListDto dto = new UpdateCaseListDto();
+            dto.Id = Id;
+            dto.ApplyStatus = ECaseApplyStatus.Add;
+            dto.Status = ECaseStatus.OnShelf;
+            dto.OnShelfTime = DateTime.Now;
+            await _caseApplication.AuditCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库下架
+        /// </summary>
+        /// <param name="Id">案例库ID</param>
+        /// <returns></returns>
+        [HttpGet("list/offshelf/{Id}")]
+        public async Task OffshelfCase(string Id)
+        {
+            UpdateCaseListDto dto = new UpdateCaseListDto();
+            dto.Id = Id;
+            dto.ApplyStatus = ECaseApplyStatus.Offshelf;
+            dto.Status = ECaseStatus.Auditing;
+            dto.OffShelfTime = DateTime.Now;
+
+            await _caseApplication.AuditCaseAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 设置热门
+        /// </summary>
+        /// <param name="Ids"></param>
+        /// <param name="Popular"></param>
+        /// <returns></returns>
+        [HttpPost("list/popular")]
+        public async Task PopularCase([FromBody] CaseInfoPopularDto dto)
+        {
+            if (dto.Ids.Length > 0)
+            {
+                for (int i = 0; i < dto.Ids.Length; i++)
+                {
+                    var Case = await _caseListRepository.GetAsync(dto.Ids[i]);
+
+                    //if (Case == null)
+                    //    throw UserFriendlyException.SameMessage("案例库查询失败");
+                    if (Case != null)
+                    {
+                        Case.IsPopular = dto.Popular;
+                        await _caseListRepository.UpdateNullAsync(Case, HttpContext.RequestAborted);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// 案例库审核(新增、修改、删除)
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/examin")]
+        public async Task ExaminCase([FromBody] AuditCaseListDto dto)
+        {
+            var Case = await _caseListRepository.GetAsync(dto.Id);
+            if (Case == null)
+                throw UserFriendlyException.SameMessage("案例库查询失败");
+
+            var CaseDto = _mapper.Map<UpdateCaseListDto>(Case);
+
+            if (dto.State == 0)
+            {//不同意
+                CaseDto.Status = ECaseStatus.Revert;
+            }
+            else if (dto.State == 1)
+            {//同意 
+                if (CaseDto.ApplyStatus == ECaseApplyStatus.Add)
+                {
+                    CaseDto.Status = ECaseStatus.OnShelf;
+                    CaseDto.OnShelfTime = DateTime.Now;
+                }
+                if (CaseDto.ApplyStatus == ECaseApplyStatus.Update)
+                {
+                    CaseDto.Status = ECaseStatus.OnShelf;
+                    CaseDto.OnShelfTime = DateTime.Now;
+                    CaseDto.UpdateTime = DateTime.Now;
+                }
+                if (CaseDto.ApplyStatus == ECaseApplyStatus.Offshelf)
+                {
+                    CaseDto.Status = ECaseStatus.OffShelf;
+                    CaseDto.OffShelfTime = DateTime.Now;
+                }
+                if (CaseDto.ApplyStatus == ECaseApplyStatus.Delete)
+                {
+                    CaseDto.Status = ECaseStatus.Drafts;
+                }
+            }
+            CaseDto.Id = dto.Id;
+            CaseDto.ExaminTime = DateTime.Now;
+            CaseDto.ExaminManId = _sessionContext.UserId;
+            CaseDto.ExaminOrganizeId = _sessionContext.OrgId;
+            CaseDto.ExaminOpinion = dto.ExaminOpinion;
+
+            await _caseApplication.AuditCaseAsync(CaseDto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库详情
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <param name="IsAddPv"></param>
+        /// <returns></returns>
+        [HttpGet("list/info")]
+        public async Task<CaseInfoDto> GetCase(string Id, bool IsAddPv)
+        {
+            return await _caseApplication.GetCaseAsync(Id, IsAddPv, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 案例库申请理由
+        /// </summary>
+        /// <param name="Id">案例库ID</param>
+        /// <returns></returns>
+        [HttpGet("list/reason/{Id}")]
+        public async Task<CaseApplyReasonDto> ReasonPlan(string Id)
+        {
+            var reason = await _caseListRepository.GetAsync(x => x.Id == Id);
+            return _mapper.Map<CaseApplyReasonDto>(reason);
+        }
+
+        /// <summary>
+        /// 案例库评分
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/score")]
+        public async Task ScoreCase([FromBody] PvCaseListDto dto)
+        {
+            var collect = await _caseCollectRepository.GetAsync(x => x.CaseId == dto.Id && x.CreatorId == _sessionContext.UserId);
+            if (collect != null)
+            {
+                if (collect.Score > 0)
+                    throw UserFriendlyException.SameMessage("当前知识已经评分");
+
+                collect.Score = dto.Score;
+                await _caseCollectRepository.UpdateAsync(collect, HttpContext.RequestAborted);
+            }
+            else
+            {
+                collect = new CaseCollect();
+                collect.CaseId = dto.Id;
+                collect.Score = dto.Score;
+                await _caseCollectRepository.AddAsync(collect, HttpContext.RequestAborted);
+            }
+
+            //计算总分
+            var sugar = _caseCollectRepository.Queryable().Where(x => x.CaseId == dto.Id);
+            var count = await sugar.CountAsync();
+            var collects = await sugar.SumAsync(x => x.Score);
+            var scoreTemp = collects / count;
+            var Case = await _caseListRepository.GetAsync(x => x.Id == dto.Id);
+            if (Case != null)
+            {
+                Case.Score = decimal.Round(scoreTemp.Value, 1);
+                await _caseListRepository.UpdateAsync(Case, HttpContext.RequestAborted);
+            }
+        }
+
+        /// <summary>
+        /// 案例库查重
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("list/exist")]
+        public async Task<bool> ExistCase([FromBody] CaseExistDto dto)
+        {
+            var any = await _caseListRepository.Queryable()
+                .Where(x => x.Status == ECaseStatus.Auditing || x.Status >= ECaseStatus.OnShelf)
+                .WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.Title.Equals(dto.Title))
+                .WhereIF(!string.IsNullOrEmpty(dto.Abstract), x => x.Abstract.Equals(dto.Abstract))
+                .WhereIF(!string.IsNullOrEmpty(dto.Describe), x => x.Describe.Equals(dto.Describe))
+                .WhereIF(!string.IsNullOrEmpty(dto.Result), x => x.Result.Equals(dto.Result))
+                .WhereIF(!string.IsNullOrEmpty(dto.Reason), x => x.Reason.Equals(dto.Reason))
+                .WhereIF(!string.IsNullOrEmpty(dto.Id), x => x.Id != dto.Id)
+                .AnyAsync();
+            return any;
+        }
+
+        /// <summary>
+        /// 案例库详情导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("list/info/export")]
+        public async Task<IActionResult> CaseInfoExport([FromBody] CaseInfoExportDto dto)
+        {
+            if (dto.Ids.Length > 1)
+            {
+                var streams = await _caseApplication.CaseInfoListExportAsync(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 _caseListRepository.GetAsync(dto.Ids[0]) ?? throw UserFriendlyException.SameMessage("案例不存在");
+            return info.Result.HtmlToStream(dto.FileType).GetFileStreamResult(dto.FileType, info.Title, false);
+        }
+
+        /// <summary>
+        /// 案例库列表导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("export")]
+        public async Task<IActionResult> GetPlanListExportAsync([FromBody] ExportExcelDto<CaseListDto> dto)
+        {
+            var items = (await _caseApplication.QueryAllCaseListAsync(dto.QueryDto, HttpContext.RequestAborted)).Item2;
+            return _exportApplication.GetExcelFile(dto, items, "案例明细导出");
+        }
+
+        /// <summary>
+        /// 案例库列表页面枚举值
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("list/status-data")]
+        public Dictionary<string, dynamic> PlanStatus()
+        {
+            var tabStatusName = new List<KeyValuePair<int, string>>
+            {
+                new KeyValuePair<int, string>(3, "已上架"),
+                new KeyValuePair<int, string>(4, "已下架"),
+                new KeyValuePair<int, string>(1, "审核中"),
+                new KeyValuePair<int, string>(7, "草稿"),
+                new KeyValuePair<int, string>(-1, "全部")
+            };
+
+            var tabExamineName = new List<KeyValuePair<int, string>>
+            {
+                new KeyValuePair<int, string>(-1, "全部"),
+                new KeyValuePair<int, string>(0, "新增审核"),
+                new KeyValuePair<int, string>(1, "修改审核"),
+                new KeyValuePair<int, string>(2, "删除审核")
+            };
+
+            var StatusName = new List<KeyValuePair<int, string>>
+            {
+                new KeyValuePair<int, string>(-1, "全部"),
+                new KeyValuePair<int, string>(0, "待提交"),
+                new KeyValuePair<int, string>(1, "审核中"),
+                new KeyValuePair<int, string>(3, "已上架"),
+                new KeyValuePair<int, string>(4, "已下架"),
+                new KeyValuePair<int, string>(5, "审核不通过"),
+                new KeyValuePair<int, string>(6, "已过期"),
+                new KeyValuePair<int, string>(7, "草稿")
+            };
+
+            var ApplyStatusName = new List<KeyValuePair<int, string>>
+            {
+                new KeyValuePair<int, string>(-1, "全部"),
+                new KeyValuePair<int, string>(0, "新增审核"),
+                new KeyValuePair<int, string>(1, "修改审核"),
+                new KeyValuePair<int, string>(2, "删除审核")
+            };
+
+            var ignoreFileType = EFileType.excel | EFileType.pdf;
+            var items = EnumExts.GetDescriptions<EFileType>();
+            var filteredDictionary = items
+                 .Where(kvp => (ignoreFileType & (EFileType)kvp.Key) == 0)
+                 .ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
+                 .ToList();
+
+            return new Dictionary<string, dynamic>
+            {
+                { "fileType", filteredDictionary},
+                { "tabStatusName", tabStatusName },
+                { "tabExamineName", tabExamineName },
+                { "StatusName", StatusName },
+                { "ApplyStatusName", ApplyStatusName }
+            };
+        }
+
+        #endregion
+
+        #region 案例库检索
+
+        /// <summary>
+        /// 检索列表
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <returns></returns>
+        [HttpGet("search")]
+        public async Task<PagedDto<CaseDataDto>> QueryOnShelfCaseList([FromQuery] CaseListDto pagedDto)
+        {
+            pagedDto.Status = ECaseStatus.OnShelf;
+            return (await _caseApplication.QueryAllCaseListAsync(pagedDto, HttpContext.RequestAborted)).ToPaged();
+        }
+
+        /// <summary>
+        /// 检索列表前10
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("search/top10")]
+        public async Task<List<CasePageViewDto>> QueryTop10CaseList()
+        {
+            return await _caseListRepository.Queryable()
+                .Take(10)
+                .Where(x => x.Status == ECaseStatus.OnShelf)
+                .Select(x => new CasePageViewDto
+                {
+                    Id = x.Id,
+                    Title = x.Title,
+                    PageView = x.PageView
+                })
+                .OrderBy(x => x.PageView, OrderByType.Desc)
+                .ToListAsync();
+        }
+
+        #endregion
+    }
+}
+

+ 10 - 10
src/Hotline.Api/Controllers/CommonPController.cs

@@ -432,20 +432,20 @@ namespace Hotline.Api.Controllers
 					.ToListAsync();
 				var waitedList = waitedDataList.Where(x => x.Status != EOrderStatus.Countersigning &&
 				 x.Time > DateTime.Now && x.Status < EOrderStatus.Filed && DateTime.Now < x.NearlyExpiredTime
-				).OrderBy(x=>x.Time).Take(40).ToList();
+				).OrderBy(x=>x.Time).ToList();
 				waitedList = waitedList.Count > 0 ? waitedList.Copy() : waitedList;
 				//allNum += waitedList.Count > 40 ? 40 : waitedList.Count;
 				//allList.AddRange(waitedList);
 				//已超期
 				var waitedExpiredDataList = waitedDataList.Where(x => (x.Time < DateTime.Now && x.Status < EOrderStatus.Filed) ||
 				                                                      (x.Time < x.ActualHandleTime && x.Status >= EOrderStatus.Filed))
-														  .OrderBy(x=> x.Time).Take(40).ToList();
+														  .OrderBy(x=> x.Time).ToList();
 				waitedExpiredDataList = waitedExpiredDataList.Count > 0 ? waitedExpiredDataList.Copy() : waitedExpiredDataList;
 				waitedExpiredDataList.ForEach(x => x.Type = "WaitedExpired");
 				//allNum += waitedExpiredDataList.Count;
 				//allList.AddRange(waitedExpiredDataList);
 				//会签待办
-				var signDataList = waitedDataList.Where(x => x.Status == EOrderStatus.Countersigning).OrderBy(x => x.Time).Take(40).ToList();
+				var signDataList = waitedDataList.Where(x => x.Status == EOrderStatus.Countersigning).OrderBy(x => x.Time).ToList();
 				signDataList = signDataList.Count > 0 ? signDataList.Copy() : signDataList;
 				signDataList.ForEach(x => x.Type = "Sign");
 				
@@ -467,7 +467,7 @@ namespace Hotline.Api.Controllers
 						Time = d.Order.ExpiredTime,
 						Status = d.Order.Status,
 						CounterSignType = d.Order.CounterSignType
-					}).Take(40)
+					})
 					.ToListAsync();
 				//allNum += screenDataList.Count;
 				//allList.AddRange(screenDataList);
@@ -487,7 +487,7 @@ namespace Hotline.Api.Controllers
 						Time = d.Order.ExpiredTime,
 						Status = d.Order.Status,
 						CounterSignType = d.Order.CounterSignType
-					}).Take(40)
+					})
 					.ToListAsync();
 				//allNum += delayDataList.Count;
 				//allList.AddRange(delayDataList);
@@ -518,7 +518,7 @@ namespace Hotline.Api.Controllers
 					Time = x.OrderVisit.Order.ExpiredTime,
 					Status = x.OrderVisit.Order.Status,
 					CounterSignType = x.OrderVisit.Order.CounterSignType
-				}).Take(40).ToListAsync();
+				}).ToListAsync();
 				//allNum += screenApplyDataList.Count;
 				//allList.AddRange(screenApplyDataList);
 				//退回待审批
@@ -538,7 +538,7 @@ namespace Hotline.Api.Controllers
 						Time = d.Order.ExpiredTime,
 						Status = d.Order.Status,
 						CounterSignType = d.Order.CounterSignType
-					}).Take(40)
+					})
 					.ToListAsync();
 				//allNum += sendBackAuditDataList.Count;
 				//allList.AddRange(sendBackAuditDataList);
@@ -564,14 +564,14 @@ namespace Hotline.Api.Controllers
 						Time = d.ExpiredTime,
 						Status = d.Status,
 						CounterSignType = d.CounterSignType
-					}).Take(40)
+					})
 					.ToListAsync();
 				return new
 				{
 					//AllNum = allNum,
 					//AllList = allList,
-					WaitedNum = waitedList.Count > 40 ? 40 : waitedList.Count,
-					WaitedList = waitedList.Take(40).ToList(),
+					WaitedNum = waitedList.Count,
+					WaitedList = waitedList.ToList(),
 					WaitedExpiredNum = waitedExpiredDataList.Count,
 					WaitedExpiredList = waitedExpiredDataList,
 					SignDataNum = signDataList.Count,

+ 143 - 0
src/Hotline.Api/Controllers/ExportData/ExportDataController.cs

@@ -0,0 +1,143 @@
+using Hotline.Application.ExportExcel;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Tools;
+using Microsoft.AspNetCore.Mvc;
+using SqlSugar;
+using System.ComponentModel;
+using System.Reflection;
+using XF.Domain.Exceptions;
+
+namespace Hotline.Api.Controllers.ExportData;
+
+[ApiController]
+[Route("{*path:regex(.*export_excel$)}")]
+public class ExportDataController : BaseController
+{
+
+    private readonly IServiceProvider _serviceProvider;
+    private readonly EndpointDataSource _endpointDataSource;
+    private readonly IExportApplication _exportApplication;
+
+    public ExportDataController(IServiceProvider serviceProvider, EndpointDataSource endpointDataSource, IExportApplication exportApplication)
+    {
+        _serviceProvider = serviceProvider;
+        _endpointDataSource = endpointDataSource;
+        _exportApplication = exportApplication;
+    }
+
+    /// <summary>
+    /// 动态导出数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpPost]
+    public async Task<FileStreamResult> HandleExportPost()
+    {
+        var fullPath = HttpContext.Request.Path.Value;
+        var originalPath = fullPath?.Substring(0, fullPath.LastIndexOf("/export_excel")).TrimStart('/');
+        if (string.IsNullOrEmpty(originalPath))
+            throw UserFriendlyException.SameMessage("无效的URL地址" + fullPath);
+
+        var matchingEndpoints = _endpointDataSource.Endpoints
+            .OfType<RouteEndpoint>()
+            .Where(endpoint => endpoint.RoutePattern.RawText == originalPath)
+            .ToList()
+            ?? throw UserFriendlyException.SameMessage($"根据URL查询路由失败: {originalPath}");
+        var matchingEndpoint = matchingEndpoints.First();
+        if (matchingEndpoints.Count > 1)
+        {
+            matchingEndpoint = matchingEndpoints.First(m => m.DisplayName.Contains("Get"));
+        }
+        if (matchingEndpoint == null)
+            throw UserFriendlyException.SameMessage($"根据URL查询路由失败: {originalPath}");
+
+        var controllerName = matchingEndpoint.RoutePattern.RequiredValues["controller"]?.ToString();
+        var actionName = matchingEndpoint.RoutePattern.RequiredValues["action"]?.ToString();
+
+        var applicationServiceType = GetApplicationServiceType(controllerName)
+            ?? throw UserFriendlyException.SameMessage($"根据名称查找方法失败. '{controllerName}'");
+
+        var method = applicationServiceType.GetMethod(actionName);
+        if (method == null)
+        {
+            method = applicationServiceType.GetMethod(actionName + "Async");
+            if (method == null)
+                throw UserFriendlyException.SameMessage($"根据方法名查找方法失败. '{actionName}'");
+        }
+
+        var serviceInstance = _serviceProvider.GetService(applicationServiceType);
+        if (serviceInstance == null)
+        {
+            serviceInstance = _serviceProvider.GetService(applicationServiceType.GetInterfaces()[0]);
+            if (serviceInstance == null)
+                throw UserFriendlyException.SameMessage($"获取注入失败. '{applicationServiceType.Name}'.");
+        }
+
+        var parameters = method.GetParameters();
+
+        using var reader = new StreamReader(HttpContext.Request.Body);
+        var body = await reader.ReadToEndAsync();
+        var param = parameters[0];
+        var genericType = typeof(ExportExcelDto<>).MakeGenericType(param.ParameterType);
+        var exportData = body.FromJson(genericType);
+        var queryDto = genericType.GetProperty("QueryDto")?.GetValue(exportData);
+        var isExportAll = genericType.GetProperty("IsExportAll")?.GetValue(exportData);
+        var pageIndex = param.ParameterType.GetProperty("PageIndex")?.GetValue(queryDto);
+        var pageSize = param.ParameterType.GetProperty("PageSize")?.GetValue(queryDto);
+        var result = method.Invoke(serviceInstance, [queryDto]);
+
+        var returnType = method.ReturnType.GetGenericArguments()[0];
+        var description = method.GetCustomAttribute<DescriptionAttribute>()?.Description;
+        if (pageIndex == null || pageSize == null)
+        {
+            isExportAll = true;
+            pageIndex = 1;
+            pageSize = 20;
+        }
+
+        return _exportApplication.GetExcelFile(returnType, genericType, exportData, ConvertToList(result, (bool)isExportAll, (int)pageIndex, (int)pageSize), description);
+    }
+
+    public static List<object>? ConvertToList(object? result, bool isExportAll, int pageIndex, int pageSize)
+    {
+        if (result == null)
+        {
+            return null;
+        }
+
+        var type = result.GetType();
+
+        if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(PostgreSQLQueryable<>))
+        {
+            throw UserFriendlyException.SameMessage("被导出方法的返回类型不是 ISugarQueryable");
+        }
+
+        var genericArgument = type.GetGenericArguments()[0];
+
+        if (isExportAll)
+        {
+            var toListMethod = type.GetMethods()
+            .Where(m => m.Name == "ToList" && m.GetParameters().Length == 0)
+            .First();
+            var list = toListMethod.Invoke(result, null) as IEnumerable<object>;
+            return list?.ToList();
+        }
+        else
+        {
+            var toListMethod = type.GetMethods()
+            .Where(m => m.Name == "ToPageList" && m.GetParameters().Length == 2)
+            .First();
+            var list = toListMethod.Invoke(result, [pageIndex, pageSize]) as IEnumerable<object>;
+            return list?.ToList();
+        }
+    }
+
+    private Type GetApplicationServiceType(string controllerName)
+    {
+        var name = controllerName + "Application";
+        var type = AppDomain.CurrentDomain.GetAssemblies().ToList()
+        .SelectMany(d => d.GetTypes())
+           .Where(d => d.Name == name)
+           .First();
+        return type;
+    }
+}

+ 84 - 4
src/Hotline.Api/Controllers/ExportWordController.cs

@@ -2,12 +2,15 @@
 using Hotline.Caching.Interfaces;
 using Hotline.Configurations;
 using Hotline.Orders;
+using Hotline.Quality;
 using Hotline.Settings;
 using Hotline.Share.Dtos.OrderExportWord;
+using Hotline.Share.Dtos.QualityExportWord;
 using Hotline.Share.Enums.Order;
 using MapsterMapper;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Options;
+using MongoDB.Driver.Linq;
 using XF.Domain.Repository;
 
 namespace Hotline.Api.Controllers
@@ -22,15 +25,17 @@ namespace Hotline.Api.Controllers
         private readonly ILogger<ExportWordController> _logger;
         private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
         private readonly ISystemSettingCacheManager _systemSettingCacheManager;
+		private readonly IQualityRepository _qualityRepository;
 
-        public ExportWordController(IOrderRepository orderRepository,
+		public ExportWordController(IOrderRepository orderRepository,
             IWordHelperService wordHelperService,
             IMapper mapper,
            IRepository<OrderVisit> orderVisitRepository,
            IRepository<OrderVisitDetail> orderVisitedDetailRepository,
            ILogger<ExportWordController> logger,
             IOptionsSnapshot<AppConfiguration> appOptions,
-            ISystemSettingCacheManager systemSettingCacheManager)
+            ISystemSettingCacheManager systemSettingCacheManager,
+			IQualityRepository qualityRepository)
         {
             _orderRepository = orderRepository;
             _wordHelperService = wordHelperService;
@@ -40,7 +45,9 @@ namespace Hotline.Api.Controllers
             _logger = logger;
             _appOptions = appOptions;
             _systemSettingCacheManager = systemSettingCacheManager;
-        }
+			_qualityRepository = qualityRepository;
+
+		}
 
         /// <summary>
         /// 工单交办单导出         
@@ -120,5 +127,78 @@ namespace Hotline.Api.Controllers
             return File(fileBytes, "application/octet-stream", $"{name}.zip");
         }
 
-    }
+
+        /// <summary>
+        /// 质检单导出
+        /// </summary>
+        /// <param name="Ids"></param>
+        /// <returns></returns>
+		[HttpPost("quality_certificate")]
+		public async Task<IActionResult> QualityCertificate([FromBody] List<string> Ids) {
+
+			var streams = new Dictionary<string, Stream>();
+			var path = $"{Directory.GetCurrentDirectory()}/Template/QualityCertificate.doc";
+			int num = 1;
+			foreach (var item in Ids)
+			{
+                var quality = await _qualityRepository.GetAsync(item, HttpContext.RequestAborted);
+				if (quality == null)
+					continue;
+				var order = await _orderRepository.GetAsync(quality.OrderId, HttpContext.RequestAborted);
+				if (order == null)
+					continue;
+
+				var exportTest = _mapper.Map<QualityCertificate>(order);
+
+				if (_appOptions.Value.IsZiGong)
+					exportTest.CityName = "自贡市";
+				else if (_appOptions.Value.IsYiBin)
+					exportTest.CityName = "宜宾市";
+				else if (_appOptions.Value.IsLuZhou)
+					exportTest.CityName = "泸州市";
+
+                //质检信息
+                exportTest.Grade = quality.Grade;
+                exportTest.QualityContent = quality.Content;
+				exportTest.UserName = quality.UserName;
+				exportTest.QualityTime = quality.QualityTime.HasValue? quality.QualityTime.Value.ToString("yyyy-MM-dd HH:mm:ss") :string.Empty;
+
+				//查询回访信息
+				var visitData = await _orderVisitRepository.GetAsync(p => p.OrderId == order.Id && p.VisitState == EVisitState.Visited, HttpContext.RequestAborted);
+				if (visitData != null)
+				{
+					//回访明细
+					var visitDetail = await _orderVisitedDetailRepository.Queryable().Where(p => p.VisitId == visitData.Id && p.VisitTarget == Share.Enums.Order.EVisitTarget.Org).ToListAsync();
+					string visit = "";
+					foreach (var itemVisit in visitDetail)
+					{
+						visit += "回访部门:" + itemVisit.VisitOrgName;
+						visit += " \n办件结果:" + itemVisit.OrgProcessingResults?.Value + "    办事态度:" + itemVisit.OrgHandledAttitude?.Value + "\n";
+
+						if (itemVisit.VisitOrgCode == order.ActualHandleOrgCode)
+							exportTest.VisitContent = "回访内容:" + itemVisit.VisitContent;
+					}
+					exportTest.VisitOrg = visit;
+				}
+				if (Ids.Count > 1)
+				{
+					streams.Add(order.No + "_" + num + path.Substring(path.LastIndexOf(".")), _wordHelperService.WordStream(path, exportTest));
+					num = num + 1;
+				}
+				else
+				{
+					var btyes = _wordHelperService.WordByte(path, exportTest);
+					HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
+					return File(btyes, "application/vnd.ms-word", order.No + path.Substring(path.LastIndexOf(".")));
+				}
+			}
+
+			//调用压缩方法 进行压缩 (接收byte[] 数据)
+			byte[] fileBytes = _wordHelperService.ConvertZipStream(streams);
+			HttpContext.Response.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");
+			var name = DateTime.Now.ToString("yyyyMMddHHmmss");
+			return File(fileBytes, "application/octet-stream", $"{name}.zip");
+		}
+
+	}
 }

+ 134 - 6
src/Hotline.Api/Controllers/FileController.cs

@@ -9,6 +9,13 @@ using XF.Domain.Exceptions;
 using XF.Domain.Repository;
 using Hotline.Caching.Interfaces;
 using Microsoft.AspNetCore.Authorization;
+using DocumentFormat.OpenXml.Presentation;
+using Hotline.File;
+using NPOI.HPSF;
+using Hotline.Share.Requests;
+using SqlSugar;
+using Hotline.Tools;
+using Hotline.Share.Dtos.Order;
 
 namespace Hotline.Api.Controllers
 {
@@ -19,20 +26,24 @@ namespace Hotline.Api.Controllers
         private readonly ISystemDicDataCacheManager _sysDicDataCacheManager;
         private readonly IRepository<File.File> _fileRepository;
         private readonly ILogger<FileController> _logger;
+        private readonly IRepository<UploadAudioFiles> _uploadAudioFilesRepository;
 
         public FileController(
             ISessionContext sessionContext,
             IMapper mapper,
             ISystemDicDataCacheManager sysDicDataCacheManager,
             IRepository<File.File> fileRepository,
-            ILogger<FileController> logger)
+            ILogger<FileController> logger,
+            IRepository<UploadAudioFiles> uploadAudioFilesRepository)
         {
             _sessionContext = sessionContext;
             _mapper = mapper;
             _sysDicDataCacheManager = sysDicDataCacheManager;
             _fileRepository = fileRepository;
             _logger = logger;
+            _uploadAudioFilesRepository = uploadAudioFilesRepository;
         }
+
         #region 附件管理
         /// <summary>
         /// 新增附件
@@ -148,11 +159,128 @@ namespace Hotline.Api.Controllers
         [HttpGet("download-proxy")]
         public async Task<IActionResult> DownloadProxy([FromServices] IHttpClientFactory clientFactory, string path)
         {
-                using var client = clientFactory.CreateClient();
-                var responseMessage = await client.GetAsync(path, HttpContext.RequestAborted);
-                responseMessage.EnsureSuccessStatusCode();
-                var stream = await responseMessage.Content.ReadAsStreamAsync(HttpContext.RequestAborted);
-                return File(stream, responseMessage?.Content?.Headers?.ContentType?.MediaType);
+            using var client = clientFactory.CreateClient();
+            var responseMessage = await client.GetAsync(path, HttpContext.RequestAborted);
+            responseMessage.EnsureSuccessStatusCode();
+            var stream = await responseMessage.Content.ReadAsStreamAsync(HttpContext.RequestAborted);
+            return File(stream, responseMessage?.Content?.Headers?.ContentType?.MediaType);
+        }
+
+        #region 录音上传
+        /// <summary>
+        /// 上传录音保存
+        /// </summary>
+        /// <param name="dtos"></param>
+        /// <returns></returns>
+        [HttpPost("adduploadaudiofiles")]
+        public async Task<object> AddUploadAudioFiles([FromBody] List<FileDto> dtos)
+        {
+            if (dtos == null || dtos.Count == 0)
+                throw UserFriendlyException.SameMessage("请上传附件");
+            var successNum = 0;
+            var errorNum = 0;
+            foreach (var item in dtos)
+            {
+                var fileName = item.FileName;
+                string strSuffix = fileName.LastIndexOf(".") > 0 ? fileName.Substring(fileName.LastIndexOf(".") + 1) : "";
+                string strSubFileName = fileName.LastIndexOf(".") > 0 ? fileName.Substring(0, fileName.LastIndexOf(".")) : fileName;
+                UploadAudioFiles audioFiles = new UploadAudioFiles()
+                {
+                    FileId = item.Additions,
+                    FileName = item.FileName,
+                    Name = strSubFileName,
+                    Type = strSuffix,
+                    Path = item.Path,
+                    AllPath = item.AllPath
+                };
+                var id = await _uploadAudioFilesRepository.AddAsync(audioFiles, HttpContext.RequestAborted);
+                if (!string.IsNullOrEmpty(id))
+                    successNum++;
+                else
+                    errorNum++;
+
+            }
+            return new
+            {
+                successNum = successNum,
+                errorNum = errorNum
+            };
         }
+
+        /// <summary>
+        /// 删除
+        /// </summary>
+        /// <param name="Ids"></param>
+        /// <returns></returns>
+        [HttpGet("deleteuploadaudiofiles")]
+        public async Task DeleteUploadAudioFiles([FromQuery] List<string> Ids)
+        {
+            if (Ids == null || Ids.Count == 0)
+                throw UserFriendlyException.SameMessage("请选择需要删除的数据");
+            foreach (var item in Ids)
+            {
+                await _uploadAudioFilesRepository.RemoveAsync(p => p.Id == item, false, HttpContext.RequestAborted);
+            }
+        }
+
+        /// <summary>
+        /// 查询列表
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("getaudiofileslist")]
+        public async Task<PagedDto<UploadAudioFilesRequestDto>> GetAudioFilesList([FromQuery] PagedKeywordRequest dto)
+        {
+            var (total, items) = await _uploadAudioFilesRepository.Queryable()
+                .Where(p => p.CreationTime >= dto.StartTime && p.CreationTime <= dto.EndTime)
+                .WhereIF(!string.IsNullOrEmpty(dto.Keyword), p => p.Name.Contains(dto.Keyword))
+                .OrderByIF(dto is { SortField: "creationTime", SortRule: 0 }, d => d.CreationTime, OrderByType.Asc) //创建时间升序
+                .OrderByIF(dto is { SortField: "creationTime", SortRule: 1 }, d => d.CreationTime, OrderByType.Desc) //创建时间降序
+                 .ToPagedListAsync(dto, HttpContext.RequestAborted);
+
+            return new PagedDto<UploadAudioFilesRequestDto>(total, _mapper.Map<IReadOnlyList<UploadAudioFilesRequestDto>>(items));
+        }
+
+        /// <summary>
+        /// 查询列表导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("getaudiofileslist/export")]
+        public async Task<FileStreamResult> ExportGetAudioFilesList([FromBody] ExportExcelDto<PagedKeywordRequest> dto)
+        {
+            var query = _uploadAudioFilesRepository.Queryable()
+                .Where(p => p.CreationTime >= dto.QueryDto.StartTime && p.CreationTime <= dto.QueryDto.EndTime)
+                .WhereIF(!string.IsNullOrEmpty(dto.QueryDto.Keyword), p => p.Name.Contains(dto.QueryDto.Keyword))
+                .OrderByIF(dto.QueryDto is { SortField: "creationTime", SortRule: 0 }, d => d.CreationTime, OrderByType.Asc) //创建时间升序
+                .OrderByIF(dto.QueryDto is { SortField: "creationTime", SortRule: 1 }, d => d.CreationTime, OrderByType.Desc);//创建时间降序
+
+            List<UploadAudioFiles> lists;
+            if (dto.IsExportAll)
+            {
+                lists = await query.ToListAsync(HttpContext.RequestAborted);
+            }
+            else
+            {
+                var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+                lists = items;
+            }
+
+            var listDtos = _mapper.Map<ICollection<UploadAudioFilesRequestDto>>(lists);
+
+            dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+            var dtos = listDtos
+                .Select(stu => _mapper.Map(stu, typeof(UploadAudioFilesRequestDto), dynamicClass))
+                .Cast<object>()
+                .ToList();
+
+            var stream = ExcelHelper.CreateStream(dtos);
+
+            return ExcelStreamResult(stream, "录音上传数据导出");
+
+        }
+
+        #endregion
     }
 }

+ 1 - 0
src/Hotline.Api/Controllers/HomeController.cs

@@ -193,6 +193,7 @@ public class HomeController : BaseController
             NationalPlatformWordLimit = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.NationalPlatformWordLimit).SettingValue[0]),
             HandleOpinionWordLimit= int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.HandleOpinionWordLimit).SettingValue[0]),
             CallInOpenType = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.CallInOpenType).SettingValue[0]),
+            Snapshot = _systemSettingCacheManager.Snapshot
         };
         return rsp;
     }

+ 12 - 1
src/Hotline.Api/Controllers/IdentityController.cs

@@ -101,6 +101,16 @@ jxrWXHbT1FB6DqkdOnBbQqS1Azqz5HxLlSyEK3F60e3SgB5iZsDZ
     public async Task<TokenOutDto> GetThirdTokenAsync([FromBody] ThirdTokenInDto dto)
         => await _identityAppService.GetThredTokenAsync(dto);
 
+    /// <summary>
+    /// 根据OpenId刷新令牌
+    /// </summary>
+    /// <param name="openId"></param>
+    /// <returns></returns>
+    [HttpGet("third/refresh")]
+    [AllowAnonymous]
+    public async Task<TokenOutDto> RefreshTokenAsync(string openId)
+        => await _identityAppService.RefreshTokenAsync(openId);
+
     [AllowAnonymous]
     [ApiExplorerSettings(IgnoreApi = true)]
     [HttpPost("token")]
@@ -170,7 +180,8 @@ jxrWXHbT1FB6DqkdOnBbQqS1Azqz5HxLlSyEK3F60e3SgB5iZsDZ
             MenuLogoImageMini = menuLogoImageMini,
             IsLoginMessageCode = IsLoginMessageCode,
             AppScope = _appOptions.Value.AppScope,
-            CallCenterType = _appOptions.Value.GetDefaultAppScopeConfiguration().CallCenterType
+            CallCenterType = _appOptions.Value.GetDefaultAppScopeConfiguration().CallCenterType,
+            Snapshot = _systemSettingCacheManager.Snapshot
         };
     }
 

+ 64 - 19
src/Hotline.Api/Controllers/JudicialManagementOrdersController.cs

@@ -1,4 +1,5 @@
-using Hotline.Api.Filter;
+using DocumentFormat.OpenXml.Office2010.Excel;
+using Hotline.Api.Filter;
 using Hotline.Application.JudicialManagement;
 using Hotline.Caching.Interfaces;
 using Hotline.File;
@@ -37,6 +38,7 @@ namespace Hotline.Api.Controllers
         private readonly IRepository<EnforcementOrdersHandler> _enforcementOrdersHandlerRepository;
         private readonly IEnforcementApplication _enforcementApplication;
         private readonly IRepository<Order> _orderRepository;
+        private readonly IRepository<LawEnforcementAgencies> _lawEnforcementAgenciesRepository;
 
         /// <summary>
         /// 
@@ -63,11 +65,12 @@ namespace Hotline.Api.Controllers
          ISessionContext sessionContext,
          IRepository<SystemArea> systemAreaRepository,
          IRepository<JudicialManagementOrders> judicialManagementOrdersRepository,
-          IFileRepository fileRepository,
-          IJudicialManagementOrdersService judicialManagementOrdersService,
-           IRepository<EnforcementOrdersHandler> enforcementOrdersHandlerRepository,
-           IEnforcementApplication enforcementApplication,
-            IRepository<Order> orderRepository
+         IFileRepository fileRepository,
+         IJudicialManagementOrdersService judicialManagementOrdersService,
+         IRepository<EnforcementOrdersHandler> enforcementOrdersHandlerRepository,
+         IEnforcementApplication enforcementApplication,
+         IRepository<Order> orderRepository,
+         IRepository<LawEnforcementAgencies> lawEnforcementAgenciesRepository
          )
         {
             _judicialComplaintsEventTypeRepository = judicialComplaintsEventTypeRepository;
@@ -83,6 +86,7 @@ namespace Hotline.Api.Controllers
             _enforcementOrdersHandlerRepository = enforcementOrdersHandlerRepository;
             _enforcementApplication = enforcementApplication;
             _orderRepository = orderRepository;
+            _lawEnforcementAgenciesRepository = lawEnforcementAgenciesRepository;
         }
 
         /// <summary>
@@ -100,13 +104,14 @@ namespace Hotline.Api.Controllers
             if (dto.Files.Any())
                 order.FileJson = await _fileRepository.AddFileAsync(dto.Files, order.Id, "", HttpContext.RequestAborted);
             await _judicialManagementOrdersService.AddAsync(order, true, HttpContext.RequestAborted);
+
             //处理执法部门
-            if (dto.EnforcementOrdersHandler != null && dto.EnforcementOrdersHandler.Any())
+            if (dto.LawEnforcementAgencies != null && dto.LawEnforcementAgencies.Any())
             {
-                List<EnforcementOrdersHandler> enforcementOrdersHandlers = new();
-                foreach (var item in dto.EnforcementOrdersHandler)
+                List<LawEnforcementAgencies> lawEnforcementAgencies = new();
+                foreach (var item in dto.LawEnforcementAgencies)
                 {
-                    enforcementOrdersHandlers.Add(new EnforcementOrdersHandler
+                    lawEnforcementAgencies.Add(new LawEnforcementAgencies
                     {
                         OrderId = order.Id,
                         OrderNo = order.No,
@@ -115,8 +120,8 @@ namespace Hotline.Api.Controllers
                         OrgName = item.Key
                     });
                 }
-                if (enforcementOrdersHandlers != null && enforcementOrdersHandlers.Any())
-                    await _enforcementOrdersHandlerRepository.AddRangeAsync(enforcementOrdersHandlers, HttpContext.RequestAborted);
+                if (lawEnforcementAgencies != null && lawEnforcementAgencies.Any())
+                    await _lawEnforcementAgenciesRepository.AddRangeAsync(lawEnforcementAgencies, HttpContext.RequestAborted);
             }
             return order.Id;
         }
@@ -143,12 +148,12 @@ namespace Hotline.Api.Controllers
             await _judicialManagementOrdersRepository.UpdateAsync(order, HttpContext.RequestAborted);
 
             //处理执法部门
-            if (dto.EnforcementOrdersHandler != null && dto.EnforcementOrdersHandler.Any())
+            if (dto.LawEnforcementAgencies != null && dto.LawEnforcementAgencies.Any())
             {
-                List<EnforcementOrdersHandler> enforcementOrdersHandlers = new();
-                foreach (var item in dto.EnforcementOrdersHandler)
+                List<LawEnforcementAgencies> lawEnforcementAgencies = new();
+                foreach (var item in dto.LawEnforcementAgencies)
                 {
-                    enforcementOrdersHandlers.Add(new EnforcementOrdersHandler
+                    lawEnforcementAgencies.Add(new LawEnforcementAgencies
                     {
                         OrderId = order.Id,
                         OrderNo = order.No,
@@ -156,10 +161,10 @@ namespace Hotline.Api.Controllers
                         OrgCode = item.Value,
                         OrgName = item.Key
                     });
-                    if (enforcementOrdersHandlers != null && enforcementOrdersHandlers.Any())
+                    if (lawEnforcementAgencies != null && lawEnforcementAgencies.Any())
                     {
-                        await _enforcementOrdersHandlerRepository.RemoveAsync(p => p.OrderId == order.Id, false, HttpContext.RequestAborted);
-                        await _enforcementOrdersHandlerRepository.AddRangeAsync(enforcementOrdersHandlers, HttpContext.RequestAborted);
+                        await _lawEnforcementAgenciesRepository.RemoveAsync(p => p.OrderId == order.Id, false, HttpContext.RequestAborted);
+                        await _lawEnforcementAgenciesRepository.AddRangeAsync(lawEnforcementAgencies, HttpContext.RequestAborted);
                     }
 
                 }
@@ -185,6 +190,46 @@ namespace Hotline.Api.Controllers
             await _judicialManagementOrdersRepository.UpdateAsync(order, HttpContext.RequestAborted);
         }
 
+        /// <summary>
+        /// 关联执法部门
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("associated_law_enforcement_gencies")]
+        [LogFilter("关联执法部门")]
+        public async Task AssociatedLawEnforcementAgencies([FromBody] AssociatedLawEnforcementAgenciesDto dto)
+        {
+            if (dto.LawEnforcementAgencies == null || dto.LawEnforcementAgencies.Count == 0)
+                throw UserFriendlyException.SameMessage("请选择执法部门");
+
+            var order = await _judicialManagementOrdersRepository.GetAsync(dto.Id, HttpContext.RequestAborted);
+            if (order == null)
+                throw UserFriendlyException.SameMessage("工单查询失败");
+            //处理执法部门
+            if (dto.LawEnforcementAgencies != null && dto.LawEnforcementAgencies.Any())
+            {
+                List<LawEnforcementAgencies> lawEnforcementAgencies = new();
+                foreach (var item in dto.LawEnforcementAgencies)
+                {
+                    lawEnforcementAgencies.Add(new LawEnforcementAgencies
+                    {
+                        OrderId = order.Id,
+                        OrderNo = order.No,
+                        OrderSoure = EOrderSoure.Enforcement,
+                        OrgCode = item.Value,
+                        OrgName = item.Key
+                    });
+                }
+                if (lawEnforcementAgencies != null && lawEnforcementAgencies.Any())
+                {
+                    await _lawEnforcementAgenciesRepository.AddRangeAsync(lawEnforcementAgencies, HttpContext.RequestAborted);
+                    order.LawEnforcementAgencies = dto.LawEnforcementAgencies;
+                    await _judicialManagementOrdersRepository.UpdateAsync(order, HttpContext.RequestAborted);
+                }
+            }
+
+        }
+
         /// <summary>
         /// 查询工单详情
         /// </summary>

+ 456 - 42
src/Hotline.Api/Controllers/OrderController.cs

@@ -1,12 +1,15 @@
 using DotNetCore.CAP;
 using Hotline.Api.Filter;
 using Hotline.Application.CallCenter;
+using Hotline.Application.Contracts.Validators.FlowEngine;
 using Hotline.Application.ExportExcel;
 using Hotline.Application.FlowEngine;
 using Hotline.Application.Orders;
 using Hotline.Application.Quality;
 using Hotline.Application.Systems;
+using Hotline.Authentications;
 using Hotline.Caching.Interfaces;
+using Hotline.CallCenter.Calls;
 using Hotline.Configurations;
 using Hotline.ContingencyManagement.Notifies;
 using Hotline.EventBus;
@@ -18,7 +21,6 @@ using Hotline.Import;
 using Hotline.Orders;
 using Hotline.Orders.Notifications;
 using Hotline.OrderTranspond;
-using Hotline.Permissions;
 using Hotline.Push.FWMessage;
 using Hotline.Push.Notifies;
 using Hotline.Repository.SqlSugar.CallCenter;
@@ -30,9 +32,12 @@ using Hotline.Settings.Hotspots;
 using Hotline.Settings.TimeLimitDomain;
 using Hotline.Settings.TimeLimits;
 using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.FlowEngine.Workflow;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.Order.Detail;
+using Hotline.Share.Dtos.Order.Handle;
 using Hotline.Share.Dtos.Order.Migration;
 using Hotline.Share.Dtos.Order.Publish;
 using Hotline.Share.Dtos.Settings;
@@ -41,6 +46,7 @@ using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Push;
 using Hotline.Share.Enums.Settings;
+using Hotline.Share.Mq;
 using Hotline.Share.Requests;
 using Hotline.Share.Tools;
 using Hotline.Tools;
@@ -56,7 +62,6 @@ using MiniExcelLibs;
 using SqlSugar;
 using System.Text;
 using System.Text.Json;
-using Hotline.Share.Dtos.Order.Handle;
 using XF.Domain.Authentications;
 using XF.Domain.Cache;
 using XF.Domain.Entities;
@@ -66,12 +71,18 @@ using XF.Utility.EnumExtensions;
 using Hotline.Application.Contracts.Validators.FlowEngine;
 using Hotline.Authentications;
 using Hotline.Share.Dtos.CallCenter;
-using NPOI.SS.Formula.Functions;
-using System.Threading;
 using Hotline.Share.Mq;
 using Hotline.CallCenter.Calls;
+using Hotline.FlowEngine.Notifications;
 using Hotline.Share.Dtos.Order.Detail;
 using Hotline.Share.Dtos.File;
+using Hotline.Share.Dtos.Org;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Snapshot.Notifications;
+using Hotline.Snapshot;
+using Hotline.Application.Snapshot;
+using Hotline.Share.Dtos.Snapshot;
+using OrderDto = Hotline.Share.Dtos.Order.OrderDto;
 
 namespace Hotline.Api.Controllers;
 
@@ -126,7 +137,6 @@ public class OrderController : BaseController
     private readonly IOrderApplication _orderApplication;
     private readonly IPushDomainService _pushDomainService;
     private readonly ILogger<OrderController> _logger;
-    private readonly ITypedCache<YbEnterpriseToken> _cacheResponse;
     private readonly IRepository<OrderSendBackAudit> _orderSendBackAuditRepository;
     private readonly IRepository<User> _userRepository;
     private readonly IExportApplication _exportApplication;
@@ -147,6 +157,10 @@ public class OrderController : BaseController
     private readonly IRepository<OrderRevoke> _orderRevokeRepository;
     private readonly IOrderTerminateRepository _orderTerminateRepository;
     private readonly ISystemLogApplication _systemLogApplication;
+    private readonly IRepository<OrderCarboncopy> _orderCarboncopy;
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
+    private readonly IOrderSnapshotApplication _orderSnapshotApplication;
+    private readonly IIndustryRepository _industryRepository;
 
     public OrderController(
         IOrderDomainService orderDomainService,
@@ -212,8 +226,12 @@ public class OrderController : BaseController
         IRepository<OrderRevoke> orderRevokeRepository,
         BaseDataApplication baseDataApplication,
         IOrderTerminateRepository orderTerminateRepository,
+        IRepository<OrderCarboncopy> orderCarboncopy,
         ITypedCache<string> typeCache,
-        ISystemLogApplication systemLogApplication)
+        ISystemLogApplication systemLogApplication,
+        IOrderSnapshotRepository orderSnapshotRepository,
+        IIndustryRepository industryRepository,
+        IOrderSnapshotApplication orderSnapshotApplication)
     {
         _orderDomainService = orderDomainService;
         _orderRepository = orderRepository;
@@ -257,7 +275,6 @@ public class OrderController : BaseController
         _logger = logger;
         _orderApplication = orderApplication;
         _pushDomainService = pushDomainService;
-        _cacheResponse = cacheResponse;
         _orderSendBackAuditRepository = orderSendBackAuditRepository;
         _userRepository = userRepository;
         _exportApplication = exportApplication;
@@ -277,10 +294,14 @@ public class OrderController : BaseController
         _callNativeRepository = callNativeRepository;
         _baseDataApplication = baseDataApplication;
         _orderTerminateRepository = orderTerminateRepository;
+        _orderCarboncopy = orderCarboncopy;
         _orderRevokeRepository = orderRevokeRepository;
         _typeCache = typeCache;
         _baseDataApplication = baseDataApplication;
         _systemLogApplication = systemLogApplication;
+        _orderSnapshotRepository = orderSnapshotRepository;
+        _industryRepository = industryRepository;
+        _orderSnapshotApplication = orderSnapshotApplication;
     }
 
     #endregion
@@ -412,14 +433,17 @@ public class OrderController : BaseController
     [HttpPost("batch-publish")]
     public async Task BatchPublishOrder([FromBody] BatchPublishOrderDto dto)
     {
+        //任务 127  市州通用-会签件需支持批量发布
+        //会签件也需支持批量发布;若会签件要批量发布,就默认不公开,只对实际办理部门进行回访即可
         var hasHuiQian = await _orderRepository.Queryable().AnyAsync(x => dto.Ids.Contains(x.Id) && x.CounterSignType != null);
+        //var hasHuiQian = await _orderRepository.Queryable().AnyAsync(x => dto.Ids.Contains(x.Id));
         if (hasHuiQian)
             throw UserFriendlyException.SameMessage("选择的工单中含有会签工单, 不能批量发布. 请排除会签工单.");
 
         var hasProvince = await _orderRepository.Queryable().AnyAsync(x => dto.Ids.Contains(x.Id) && x.Source == ESource.ProvinceStraight);
         //if (hasProvince)
         //    throw UserFriendlyException.SameMessage("选择的工单中含有省工单, 不能批量发布. 请排除省工单.");
-        if (_appOptions.Value.IsYiBin)
+        if (_appOptions.Value.IsYiBin || _appOptions.Value.IsZiGong)
         {
             hasProvince = await _orderRepository.Queryable().AnyAsync(x => dto.Ids.Contains(x.Id) && x.IsProvince);
         }
@@ -543,7 +567,7 @@ public class OrderController : BaseController
             orderVisit.EmployeeId = string.Empty;
         }
 
-        if (order is { FileOrgIsCenter:true, CounterSignType: null } && !order.IsProvince)
+        if (order is { FileOrgIsCenter: true, CounterSignType: null } && !order.IsProvince)
         {
             orderVisit.VisitState = EVisitState.Visited;
             orderVisit.VisitTime = DateTime.Now;
@@ -564,6 +588,8 @@ public class OrderController : BaseController
         }
 
         string visitId = await _orderVisitRepository.AddAsync(orderVisit);
+        await _orderSnapshotApplication.UpdateLabelAsync(order.Id, dto.SnapshotLabels);
+
 
         //新增回访信息
         var visitedDetail = new List<OrderVisitDetail>();
@@ -725,6 +751,12 @@ public class OrderController : BaseController
         res.idNames = order.CounterSignType == null
             ? null
             : idNames.Where(d => d.Key != idName.Key).ToList();
+
+        if (_systemSettingCacheManager.Snapshot)
+        {
+            res.IsSnapshot = await _orderSnapshotRepository.AnyAsync(m => m.Id == order.Id);
+            res.SnapshotLabel = _sysDicDataCacheManager.SnapshotOrderLabel;
+        }
         return res;
     }
 
@@ -858,6 +890,15 @@ public class OrderController : BaseController
             }
         }
 
+        if (_systemSettingCacheManager.Snapshot)
+        {
+            await _orderSnapshotRepository.GetAsync(pubentity.OrderId)
+                .Then(m =>
+                {
+                    pubentity.Labels = m.Labels;
+                });
+        }
+
         return pubentity;
     }
 
@@ -1074,17 +1115,36 @@ public class OrderController : BaseController
     /// </summary>
     /// <returns></returns>
     [HttpGet("visit/basedata")]
-    public Dictionary<string, dynamic> VisitBaseData() => new Dictionary<string, dynamic>
+    public Dictionary<string, dynamic> VisitBaseData()
+    {
+        var voiceEvaluate = EnumExts.GetDescriptions<EVoiceEvaluate>();
+        var seatEvaluate = EnumExts.GetDescriptions<ESeatEvaluate>();
+        if (_appOptions.Value.IsZiGong == true)
         {
-            { "seatEvaluate", EnumExts.GetDescriptions<ESeatEvaluate>() },
+            voiceEvaluate = EnumExtensions.GetEnumKeyValueList<EVoiceEvaluate>();
+            seatEvaluate = EnumExtensions.GetEnumKeyValueList<ESeatEvaluate>();
+        }
+        var visitSatisfaction = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitSatisfaction);
+        var visitManner = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitManner);
+        if (_appOptions.Value.IsZiGong == false)
+        {
+            seatEvaluate = seatEvaluate.Where(m => new int[] { -1, 1, 3 }.Contains(m.Key) == false).ToList();
+            visitSatisfaction = visitSatisfaction.Where(x => x.DicDataValue != "-1").ToList();
+            visitManner = visitManner.Where(x => x.DicDataValue != "-1").ToList();
+        }
+
+        return new Dictionary<string, dynamic>
+        {
+            { "seatEvaluate", seatEvaluate },
             { "visitType", EnumExts.GetDescriptions<EVisitType>() },
-            { "voiceEvaluate", EnumExts.GetDescriptions<EVoiceEvaluate>() },
-            { "visitSatisfaction", _sysDicDataCacheManager.GetVisitSatisfaction().Where(m => m.DicDataValue != "-1").ToList() },
-            { "visitMananer", _sysDicDataCacheManager.VisitMananer.Where(x => x.DicDataValue != "-1").ToList() },
+            { "voiceEvaluate", voiceEvaluate},
+            { "visitSatisfaction", visitSatisfaction },
+            { "visitMananer", visitManner},
             { "visitStateQuery", EnumExts.GetDescriptions<EVisitStateQuery>() },
             { "sourceChannel", _sysDicDataCacheManager.SourceChannel },
             { "aiVisitResult", EnumExts.GetDescriptions<EAiVisitResult>() }
         };
+    }
 
     /// <summary>
     /// 回访详情
@@ -1112,14 +1172,20 @@ public class OrderController : BaseController
         //    x => x.OrderId == orderVisit.OrderId && x.AgainState == EAgainState.DoAgain, HttpContext.RequestAborted);
         var voiceEvaluate = EnumExts.GetDescriptions<EVoiceEvaluate>();
         var seatEvaluate = EnumExts.GetDescriptions<ESeatEvaluate>();
+        if (_appOptions.Value.IsZiGong==true)
+        {
+            voiceEvaluate = EnumExtensions.GetEnumKeyValueList<EVoiceEvaluate>();
+            seatEvaluate=EnumExtensions.GetEnumKeyValueList<ESeatEvaluate>();
+        }
+        var visitSatisfaction = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitSatisfaction);
+        var visitManner = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitManner);
         if (_appOptions.Value.IsZiGong == false)
         {
-            seatEvaluate = seatEvaluate.Where(m => new int[] { 1, 3 }.Contains(m.Key) == false).ToList();
+            seatEvaluate = seatEvaluate.Where(m => new int[] { -1, 1, 3 }.Contains(m.Key) == false).ToList();
+            visitSatisfaction = visitSatisfaction.Where(x => x.DicDataValue != "-1").ToList();
+            visitManner = visitManner.Where(x => x.DicDataValue != "-1").ToList();
         }
-
-        var visitSatisfaction = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitSatisfaction).Where(x => x.DicDataValue != "-1");
         var dissatisfiedReason = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.DissatisfiedReason);
-        var visitManner = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.VisitManner).Where(x => x.DicDataValue != "-1");
         //var callRecord = await _trCallRecordRepository.GetAsync(x => x.CallAccept == orderVisit.CallId); //由CallAccept改为OtherAccept
         //var callRecord = await _trCallRecordRepository.GetAsync(x => x.OtherAccept == orderVisit.CallId && string.IsNullOrEmpty(x.OtherAccept) == false, HttpContext.RequestAborted);
         var recordingFileUrl = "";
@@ -1919,11 +1985,11 @@ public class OrderController : BaseController
         if (int.Parse(setting?.SettingValue[0]) != 0 && !_sessionContext.OrgIsCenter)
         {
             int count = await _orderDelayRepository.CountAsync(x =>
-                x.OrderId == delaydto.OrderId && x.ApplyOrgCode == _sessionContext.RequiredOrgId && x.DelayState == EDelayState.Pass);
+                x.OrderId == delaydto.OrderId && x.ApplyOrgCode.Substring(SqlFunc.MappingColumn<int>("0"), SqlFunc.MappingColumn<int>("6")) == _sessionContext.RequiredOrgId.Substring(SqlFunc.MappingColumn<int>("0"), SqlFunc.MappingColumn<int>("6")) && x.DelayState == EDelayState.Pass);
             if (_appOptions.Value.IsZiGong)
             {
                 count = await _orderDelayRepository.CountAsync(x =>
-                    x.OrderId == delaydto.OrderId && x.ApplyOrgCode == _sessionContext.RequiredOrgId &&
+                    x.OrderId == delaydto.OrderId &&  x.ApplyOrgCode == _sessionContext.RequiredOrgId &&
                     (x.DelayState == EDelayState.Pass || x.DelayState == EDelayState.NoPass));
             }
 
@@ -2003,6 +2069,98 @@ public class OrderController : BaseController
         }
     }
 
+	/// <summary>
+	/// 批量审批延期
+	/// </summary>
+	[HttpPost("delay/batch_audit")]
+	[LogFilter("批量审批延期")]
+	public async Task<string> BatchAuditDelay([FromBody] BatchDelayNextFlowDto dto) {
+		var result = new StringBuilder();
+		var fail = 0;
+		var success = 0;
+		foreach (var item in dto.DelayId)
+		{
+			try
+			{
+                var workflow = dto.NextWorkflow;
+				var delay = await _orderDelayRepository.Queryable().Includes(x=>x.Order).Where(x=>x.Id == item).FirstAsync(HttpContext.RequestAborted);
+				workflow.WorkflowId = delay.WorkflowId;
+				var workflowEntuty = await _workflowDomainService.GetWorkflowAsync(workflow.WorkflowId, withDefine: true, withSteps: true,cancellationToken: HttpContext.RequestAborted);
+				var currentStep =
+					workflowEntuty.Steps.FirstOrDefault(d => d.Status == EWorkflowStepStatus.WaitForAccept || d.Status == EWorkflowStepStatus.WaitForHandle);
+
+				NextStepsWithOpinionDto<NextStepOption> next = null;
+
+				try
+				{
+					next = await _workflowApplication.GetNextStepsAsync(delay.WorkflowId, HttpContext.RequestAborted);
+				}
+				catch (UserFriendlyException e)
+				{
+					if (e.Message.Contains("未找到对应节点"))
+					{
+						result.Append("无权审核:" + delay.No);
+						fail++;
+					}
+					else
+					{
+						throw;
+					}
+				}
+				if (next == null) continue;
+
+				if (!delay.Order.IsProvince)
+				{
+					if (next.Steps.Any(x => x.Value == "省审批"))
+					{
+						next.Steps.Remove(next.Steps.First(x => x.Value == "省审批"));
+					}
+				}
+
+				if (!_sessionContext.OrgIsCenter && currentStep.Name != "中心初审")
+				{
+					if (next.Steps.Any(x => x.Value == "中心终审"))
+					{
+						next.Steps.Remove(next.Steps.First(x => x.Value == "中心终审"));
+					}
+				}
+
+				var isBatch = next.Steps.Where(x => x.Value == workflow.NextStepName).Any();
+                if (isBatch)
+                {
+					var step = next.Steps.Where(x => x.Value == workflow.NextStepName).FirstOrDefault();
+					workflow.NextStepCode = step.Key;
+					workflow.NextStepName = step.Value;
+				}
+                else {
+					result.Append("无权审核:" + delay.No);
+					fail++;
+                    continue;
+				}
+
+				workflow.StepId = next.StepId;
+                workflow.ReviewResult = dto.IsPass ? EReviewResult.Approval : EReviewResult.Failed;
+
+				if (workflow.ReviewResult == EReviewResult.Approval) {
+					await _workflowDomainService.NextAsync(_sessionContext, workflow,cancellationToken: HttpContext.RequestAborted);
+				}
+				else
+				{
+					var reject = workflow.Adapt<RejectDto>();
+					await _workflowApplication.RejectAsync(reject, HttpContext.RequestAborted);
+				}
+				success++;
+			}
+			catch (UserFriendlyException e)
+			{
+				result.Append(e.Message);
+				fail++;
+			}
+		}
+		return $"总共: {dto.DelayId.Length}, 成功: {success}, 失败: {fail}, 失败原因: {result.ToString()}";
+
+	}
+
     /// <summary>
     ///  延期查询流程办理下一步可选节点
     /// </summary>
@@ -3186,12 +3344,31 @@ public class OrderController : BaseController
     [HttpGet("history_all")]
     public async Task<PagedDto<OrderDto>> QueryAll([FromQuery] QueryOrderHistoryDto dto)
     {
-        var (total, items) = await _orderRepository.Queryable()
+        var query = _orderRepository.Queryable()
             .WhereIF(!string.IsNullOrEmpty(dto.PhoneNo), d => d.Contact == dto.PhoneNo)
             .WhereIF(!string.IsNullOrEmpty(dto.OrderId), d => d.Id != dto.OrderId)
-            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!) || d.No.Contains(dto.Keyword!))
+            .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!) || d.No.Contains(dto.Keyword!));
+
+        //随手拍
+        bool.TryParse(
+            _systemSettingCacheManager.GetSetting(SettingConstants.Snapshot)?.SettingValue[0],
+            out bool isSnapshotEnable);
+        if (isSnapshotEnable && !string.IsNullOrEmpty(dto.IndustryId))
+        {
+            query.Where(d => d.OrderSnapshot.IndustryId == dto.IndustryId);
+        }
+
+        //var (total, items) = await _orderRepository.Queryable()
+        //    .WhereIF(!string.IsNullOrEmpty(dto.PhoneNo), d => d.Contact == dto.PhoneNo)
+        //    .WhereIF(!string.IsNullOrEmpty(dto.OrderId), d => d.Id != dto.OrderId)
+        //    .WhereIF(!string.IsNullOrEmpty(dto.Keyword), d => d.Title.Contains(dto.Keyword!) || d.No.Contains(dto.Keyword!))
+        //    .OrderByDescending(d => d.CreationTime)
+        //    .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+
+        var (total, items) = await query
             .OrderByDescending(d => d.CreationTime)
             .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+
         return new PagedDto<OrderDto>(total, _mapper.Map<IReadOnlyList<OrderDto>>(items));
     }
 
@@ -3343,6 +3520,10 @@ public class OrderController : BaseController
         var delayModel = order.OrderDelays.MaxBy(x => x.CreationTime);
         if (delayModel != null)
         {
+            if (delayModel.IsProDelay)
+            {
+                dto.ProvinceDelayString = "该工单已向省平台发送延期申请!延期状态:" + delayModel.DelayState.GetDescription();
+            }
             var workFlow = await _workflowRepository.GetAsync(delayModel.WorkflowId);
             switch (delayModel.DelayState)
             {
@@ -3365,6 +3546,7 @@ public class OrderController : BaseController
         else
         {
             dto.DelayString = "";
+            dto.ProvinceDelayString = "";
         }
 
         //dto.CanPrevious = canPrevious;
@@ -3473,12 +3655,32 @@ public class OrderController : BaseController
             dto.ProvinceRevokeString = "该工单已由省平台发送撤单!请直接归档办理!";
         }
 
+        //省甄别
+        var orderScreen = await _orderScreenRepository.Queryable().Where(x => x.OrderId == order.Id && x.IsProScreen == true).OrderByDescending(x => x.CreationTime)
+                 .FirstAsync();
+        if (orderScreen != null)
+        {
+            dto.ProvinceScreenString = "该工单已向省平台发送甄别申请!甄别状态:" + orderScreen.Status.GetDescription();
+        }
+
         //终止
         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;
 
+        if (_systemSettingCacheManager.Snapshot)
+        {
+            await _orderSnapshotRepository.Queryable()
+                .Where(m => m.Id == order.Id)
+                .Select(m => new { m.IndustryId, m.IndustryName })
+                .FirstAsync(HttpContext.RequestAborted)
+                .Then(async snapshot =>
+                {
+                    dto.IndustryName = snapshot.IndustryName;
+                    dto.IndustryId = snapshot.IndustryId;
+                });
+        }
         dto.IsReTransact = await _orderSpecialRepository.Queryable()
             .Where(x => x.OrderId == dto.Id && x.SpecialType == ESpecialType.ReTransact).AnyAsync();
 
@@ -3537,6 +3739,20 @@ public class OrderController : BaseController
 
         await _orderDomainService.AddAsync(order, true, HttpContext.RequestAborted);
 
+        if (_systemSettingCacheManager.Snapshot && dto.IndustryId.NotNullOrEmpty() && dto.IndustryName.NotNullOrEmpty())
+        {
+            var snapshot = new OrderSnapshot
+            {
+                Id = order.Id,
+                IndustryId = dto.IndustryId,
+                IndustryName = dto.IndustryName
+            };
+            await _orderSnapshotRepository.AddAsync(snapshot, HttpContext.RequestAborted);
+            order.Latitude = 29.33924;
+            order.Longitude = 104.779307;
+            await _orderRepository.UpdateAsync(order, HttpContext.RequestAborted);
+        }
+
         //订阅此事件的内部处理工单数据只能更新各自业务的字段,不能全部更新
         //新增工单其他处理事件  (受理短信)
         if (!string.IsNullOrEmpty(order.Contact) && order.Contact.Length == 11)
@@ -3567,11 +3783,11 @@ public class OrderController : BaseController
             await _callApplication.RelateTianrunCallWithOrderAsync(order.CallId, order.Id, HttpContext.RequestAborted);
 
         //内容分词
-        await _orderApplication.OrderParticiple(dto.Content, order.Id, order.CreationTime, HttpContext.RequestAborted);
+        await _orderApplication.OrderParticiple(dto.Content, order.Id, order.No, order.Title, order.CreationTime, HttpContext.RequestAborted);
         //敏感分词
         await _orderApplication.OrderSensitiveParticiple(dto.Content, order.Id, HttpContext.RequestAborted);
-
-
+        //知识库引用
+        await _orderApplication.AddKnowledgeQuote(order.Id, order.Title, order.No, order.KnowledgeQuote, HttpContext.RequestAborted);
         ////sms
         //try
         //{
@@ -3708,7 +3924,7 @@ public class OrderController : BaseController
         await _orderCopyRepository.AddAsync(copy, HttpContext.RequestAborted);
 
         if (order.Content != dto.Content)
-            await _orderApplication.OrderParticiple(dto.Content, dto.Id, order.CreationTime, HttpContext.RequestAborted);
+            await _orderApplication.OrderParticiple(dto.Content, dto.Id, order.No, order.Title, order.CreationTime, HttpContext.RequestAborted);
         if (dto.RepeatableEventDetails?.Any() ?? false)
         {
             var reAdds = dto.RepeatableEventDetails.Where(x => string.IsNullOrEmpty(x.OrderId) && !x.IsDeleted)
@@ -3794,6 +4010,8 @@ public class OrderController : BaseController
 
         //敏感分词
         await _orderApplication.OrderSensitiveParticiple(dto.Content, order.Id, HttpContext.RequestAborted);
+        //知识库引用
+        await _orderApplication.AddKnowledgeQuote(order.Id, order.Title, order.No, order.KnowledgeQuote, HttpContext.RequestAborted);
 
         return new { Id = order.Id, No = order.No, Password = order.Password, CallId = order.CallId };
     }
@@ -3910,6 +4128,19 @@ public class OrderController : BaseController
         if (orderId.NotNullOrEmpty())
         {
             outDto.Opinion = await _typeCache.GetAsync($"tmp_opinion_{orderId}{_sessionContext.UserId}", HttpContext.RequestAborted);
+            outDto.Content = (await _orderRepository.GetAsync(orderId, HttpContext.RequestAborted))?.Content;
+        }
+
+        //随手拍
+        bool.TryParse(_systemSettingCacheManager.GetSetting(SettingConstants.Snapshot)?.SettingValue[0],
+            out bool isSnapshotEnable);
+        if (isSnapshotEnable)
+        {
+            var orderSnapshot = await _orderSnapshotRepository.GetAsync(orderId, HttpContext.RequestAborted);
+            if (orderSnapshot != null && string.CompareOrdinal(orderSnapshot.IndustryName, "安全隐患") == 0)
+            {
+                outDto.Steps = outDto.Steps.Where(d => d.BusinessType != EBusinessType.Send).ToList();
+            }
         }
 
         return outDto;
@@ -3928,7 +4159,13 @@ public class OrderController : BaseController
                 $"非法参数, {string.Join(',', validResult.Errors.Select(d => d.ErrorMessage))}");
         var order = await _orderApplication.SaveOrderWorkflowInfo(dto, HttpContext.RequestAborted);
 
-        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.Workflow.WorkflowId, withSteps: true, withTraces: true,
+        // 随手拍业务处理
+        if (_systemSettingCacheManager.Snapshot)
+        {
+            await _orderSnapshotApplication.SaveOrderWorkflowInfo(dto);
+        }
+
+        var workflow = await _workflowDomainService.GetWorkflowAsync(dto.Workflow.WorkflowId, withDefine: true, withSteps: true, withTraces: true,
             cancellationToken: HttpContext.RequestAborted);
 
         //await _workflowApplication.NextAsync(dto.WorkflowDto, order.ExpiredTime, HttpContext.RequestAborted);
@@ -3946,6 +4183,77 @@ public class OrderController : BaseController
         }
     }
 
+    /// <summary>
+    /// 工单批量标注是否安全生产
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("sign/bath")]
+    public async Task<string> OrderSignBathAsync([FromBody] OrderSignBathInDto dto)
+    {
+        var stringBuilder = new StringBuilder();
+        foreach (var orderId in dto.OrderIds)
+        {
+            var order = await _orderRepository.GetAsync(orderId, HttpContext.RequestAborted);
+            if (order is null)
+            {
+                continue;
+            }
+            var snapshot = await _orderSnapshotApplication.UpdateSafetyAsync(orderId, dto.IsSafetyDepartment, dto.Remark);
+            if (snapshot is null)
+            {
+                stringBuilder.Append($"随手拍: {order.No} 不存在");
+                continue;
+            }
+            var workflow = await _workflowDomainService.GetWorkflowAsync(order.WorkflowId, withDefine: true, withSteps: true, withTraces: true,
+    cancellationToken: HttpContext.RequestAborted);
+
+            NextStepsWithOpinionDto<NextStepOption> nextSteps;
+            try
+            {
+                nextSteps = await _workflowApplication.GetNextStepsAsync(order.WorkflowId, HttpContext.RequestAborted);
+            }
+            catch (UserFriendlyException e)
+            {
+                if (e.Message.Contains("未找到对应节点"))
+                {
+                    stringBuilder.AppendLine($"{order.No} 修改成功");
+                    continue;
+                }
+                throw;
+            }            
+            var stepInfo = nextSteps.Steps.FirstOrDefault(m => m.BusinessType == EBusinessType.Send);
+            if (stepInfo == null)
+            {
+                stringBuilder.Append($"下一步节点: [派单组] 未找到");
+                continue;
+            }
+            var data = new OrderHandleFlowDto
+            {
+                OrderId = orderId,
+                IsSafetyDepartment = dto.IsSafetyDepartment
+            };
+            var workflowDto = new NextWorkflowDto
+            {
+                WorkflowId = order.WorkflowId,
+                StepId = nextSteps.StepId,
+                NextStepCode = stepInfo.Key,
+                NextStepName = stepInfo.Value,
+                Opinion = dto.Remark,
+                BackToCountersignEnd = false,
+                IsSms = false,
+                IsForwarded = false,
+                HandlerType = EHandlerType.OrgLevel,
+                BusinessType = EBusinessType.Send,
+                FlowDirection = EFlowDirection.CenterToCenter,
+            };
+            var startStep = workflow.Steps.First(d => d.Id == nextSteps.StepId);
+            await HandleOrderAsync(order, workflow, startStep, data, workflowDto, HttpContext.RequestAborted);
+            stringBuilder.AppendLine($"{orderId} 标注完成;");
+        }
+        return stringBuilder.ToString();
+    }
+
     private async Task HandleOrderAsync(Order order, Workflow workflow, WorkflowStep startStep, OrderHandleFlowDto orderHandleFlowDto,
         BasicWorkflowDto workflowDto, CancellationToken cancellationToken)
     {
@@ -3996,14 +4304,6 @@ public class OrderController : BaseController
                     }).Where(o => o.Id == order.Id).ExecuteCommandAsync(HttpContext.RequestAborted);
                 }
 
-                //if (workflowDto.BusinessType == EBusinessType.Seat)
-                //{
-                // await _orderRepository.Updateable().SetColumns(o => new Order()
-                // {
-                //  Status = EOrderStatus.WaitForAccept
-                // }).Where(o => o.Id == order.Id).ExecuteCommandAsync(HttpContext.RequestAborted);
-                //}
-
                 await _workflowDomainService.NextAsync(_sessionContext, nextDto, order.ExpiredTime, isAutoFillSummaryOpinion, cancellationToken);
                 break;
             case EOrderAssignMode.CrossLevel:
@@ -4013,7 +4313,7 @@ public class OrderController : BaseController
                 orderHandleFlowDto.CrossSteps = orderHandleFlowDto.CrossSteps.OrderBy(d => d.Sort).ToList();
                 var stepCount = orderHandleFlowDto.CrossSteps.Count;
                 var unhandleSteps = new List<WorkflowStep> { startStep };
-                for (int i = 0; i < stepCount; i++)
+                for (int i = 0;i < stepCount;i++)
                 {
                     var crossStep = orderHandleFlowDto.CrossSteps[i];
                     var tempSteps = new List<WorkflowStep>();
@@ -4044,12 +4344,98 @@ public class OrderController : BaseController
 
                 break;
             case EOrderAssignMode.MainAndSecondary:
+                //主协办暂只支持指派给部门办理,且主办部门有且只有一个一级部门
+                nextDto = _mapper.Map<NextWorkflowDto>(workflowDto);
+                nextDto.WorkflowId = startStep.WorkflowId;
+                nextDto.StepId = startStep.Id;
+                nextDto.HandlerType = EHandlerType.OrgLevel;
+                nextDto.FlowDirection = EFlowDirection.CenterToOrg;
+                var secondaryOrgs = orderHandleFlowDto.SecondaryOrgs.DistinctBy(d => d.Id).ToList();
+                var nextHandleOrgs = secondaryOrgs
+                    .Where(d => d.Level == 1 && d.Id.StartsWith(OrgSeedData.CenterId))
+                    .ToList();
+                if (nextHandleOrgs.Any())
+                    nextDto.NextHandlers.AddRange(nextHandleOrgs.Select(d => new FlowStepHandler
+                    {
+                        Key = d.Id,
+                        Value = d.Name,
+                        OrgId = d.Id,
+                        OrgName = d.Name
+                    }));
+                nextDto.IsStartCountersign = nextDto.NextHandlers.Count > 1;
+
+                await HandleNextInMainAndSecondaryAsync(_sessionContext, workflow.WorkflowDefinition,
+                    secondaryOrgs, nextDto, order.ExpiredTime, isAutoFillSummaryOpinion,
+                    cancellationToken);
+
+                //抄送
+                var ccs = orderHandleFlowDto.Copys
+                     .Where(d => !string.IsNullOrEmpty(d.OrgId)
+                                 || !string.IsNullOrEmpty(d.RoleId)
+                                 || !string.IsNullOrEmpty(d.UserId))
+                     .Select(d => _mapper.Map<OrderCarboncopy>(d))
+                     .Distinct()
+                     .ToList();
+                ccs.ForEach(d => d.OrderId = order.Id);
+                await _orderCarboncopy.AddRangeAsync(ccs, HttpContext.RequestAborted);
+
                 break;
             default:
                 throw new ArgumentOutOfRangeException();
         }
     }
 
+    private async Task HandleNextInMainAndSecondaryAsync(ISessionContext current, WorkflowDefinition definition, List<OrgDto> orgs,
+      NextWorkflowDto? flowDto, DateTime? expiredTime, bool isAutoFillSummaryOpinion, CancellationToken cancellation)
+    {
+        if (flowDto is null || !flowDto.NextHandlers.Any()) return;
+        var currentSteps = await _workflowDomainService.NextAsync(current, flowDto, expiredTime,
+            isAutoFillSummaryOpinion, cancellation);
+
+        foreach (var currentStep in currentSteps)
+        {
+            var currentStepHandlerOrgId = currentStep?.HandlerOrgId;
+            if (string.IsNullOrEmpty(currentStepHandlerOrgId))
+                throw new UserFriendlyException($"数据异常, 待办部门id为空, stepId: {currentStep.Id}");
+            var nextStepHandlerOrgLevel = currentStepHandlerOrgId.CalcOrgLevel() + 1;
+            var nextHandlers = orgs.Where(d => d.Level == nextStepHandlerOrgLevel && d.Id.StartsWith(currentStepHandlerOrgId))
+                .Select(d => new FlowStepHandler
+                {
+                    Key = d.Id,
+                    Value = d.Name,
+                    OrgId = d.Id,
+                    OrgName = d.Name
+                })
+                .ToList();
+            if (nextHandlers.Any())
+            {
+                var nextStepDefine = definition.FindStepDefines(currentStep.NextSteps.Select(d => d.Code))
+                    .FirstOrDefault(d =>
+                        d.HandlerType == EHandlerType.OrgLevel &&
+                        d.HandlerTypeItems.Any(x => x.Key == nextStepHandlerOrgLevel.ToString()));
+                if (nextStepDefine == null)
+                    throw new UserFriendlyException($"流程模板未配置该部门等级, defineId: {definition.Id}, level: {nextStepHandlerOrgLevel}");
+
+                var nextDto = new NextWorkflowDto
+                {
+                    WorkflowId = flowDto.WorkflowId,
+                    StepId = currentStep.Id,
+                    NextStepCode = nextStepDefine.Code,
+                    NextStepName = nextStepDefine.Name,
+                    FlowDirection = EFlowDirection.OrgToOrg,
+                    HandlerType = nextStepDefine.HandlerType,
+                    StepType = nextStepDefine.StepType,
+                    IsSms = false,
+                    NextHandlers = nextHandlers,
+                    IsStartCountersign = nextHandlers.Count > 1,
+                    BusinessType = nextStepDefine.BusinessType,
+                };
+
+                await HandleNextInMainAndSecondaryAsync(current, definition, orgs, nextDto, expiredTime, isAutoFillSummaryOpinion, cancellation);
+            }
+        }
+    }
+
     private async Task AverageSendOrderAsync(NextWorkflowDto nextDto, CancellationToken cancellationToken)
     {
         // 平均派单
@@ -4085,6 +4471,7 @@ public class OrderController : BaseController
             throw UserFriendlyException.SameMessage("该工单未开启流程");
         var dto = await _workflowApplication.GetNextStepsAsync(order.WorkflowId, HttpContext.RequestAborted);
         dto.ExpiredTime = order.ExpiredTime;
+        dto.Content = order.Content;
         var rsp = _mapper.Map<NextStepsWithOpinionDto<RecommendStepOption>>(dto);
         foreach (var step in rsp.Steps)
         {
@@ -4108,6 +4495,7 @@ public class OrderController : BaseController
             .ToList().Adapt<List<SystemDicDataOutDto>>();
 
         rsp.CounterSignType = order.CounterSignType;
+        await _orderSnapshotApplication.GetNextStepsDatabaseAsync(rsp, orderId);
         return rsp;
     }
 
@@ -4238,8 +4626,12 @@ public class OrderController : BaseController
         ////todo 自贡add 4
         //FocusOnEvents.Add(new Kv { Key = "4", Value = "24小时办结" });
 
+        var industryItems = await _industryRepository.Queryable()
+            .Select(d => new { d.Id, d.Name, })
+            .ToListAsync();
         var rsp = new
         {
+            Industry = industryItems,
             TranspondCity = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.TranspondCity),
             ChannelOptions = _sysDicDataCacheManager.GetSysDicDataCache(TimeLimitBaseDataConsts.SourceChannel),
             AcceptTypeOptions = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.AcceptType),
@@ -4373,6 +4765,19 @@ public class OrderController : BaseController
         return _mapper.Map<IReadOnlyList<OrderFlowTraceDto>>(traces);
     }
 
+    /// <summary>
+    /// 流转到坐席的次数
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("count-of-flow-to-seat/{orderId}")]
+    public async Task<int> FlowToSeatCountAsync(string orderId)
+    {
+        var order = await _orderRepository.Queryable()
+            .Includes(d => d.WorkflowTraces)
+            .FirstAsync(d => d.Id == orderId, HttpContext.RequestAborted);
+        return order.WorkflowTraces.Count(d => d.IsOrigin && d.BusinessType == EBusinessType.Seat);
+    }
+
     #endregion
 
     #region 工单待办
@@ -4549,14 +4954,23 @@ public class OrderController : BaseController
     /// </summary>
     /// <returns></returns>
     [HttpGet("waited/center/base")]
-    public async Task<object> WaitedForCenterBaseData()
+    public async Task<Dictionary<string, object>> WaitedForCenterBaseData()
     {
-        var rsp = new
+        var rsp = new Dictionary<string, object>
         {
-            OrderStatus = EnumExts.GetDescriptions<EOrderStatus>(),
-            ExpiredStatus = EnumExts.GetDescriptions<EExpiredStatus>(),
-            StepNames = new string[] { "话务部", "派单组", "班长审批" }
+            {"orderStatus" ,  EnumExts.GetDescriptions<EOrderStatus>() },
+            {"expiredStatus",  EnumExts.GetDescriptions<EExpiredStatus>() },
+            { "stepNames" , new string[] { "话务部", "派单组", "班长审批" } },
         };
+
+        if (_systemSettingCacheManager.Snapshot)
+        {
+            var industry = await _industryRepository.Queryable()
+           .Select(d => new { d.Id, d.Name, })
+           .ToListAsync();
+            rsp.Add("industry", industry);
+        }
+
         return rsp;
     }
 
@@ -4695,7 +5109,7 @@ public class OrderController : BaseController
                 dto.Handler = handler;
             }
         }
-         
+
         if (oneSendBack || twoSendBack)
         {
             var sendBack = await _orderSendBackAuditRepository.Queryable()

+ 75 - 0
src/Hotline.Api/Controllers/OrderModuleControllers/OrderCarbonCopyController.cs

@@ -0,0 +1,75 @@
+using Hotline.Application.Orders;
+using Hotline.Caching.Services;
+using Hotline.FlowEngine.WorkflowModules;
+using Hotline.Orders;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Settings.TimeLimits;
+using Hotline.Settings;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.Order.CarbonCopy;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Requests;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Authentications;
+using XF.Domain.Repository;
+using XF.Utility.EnumExtensions;
+using Hotline.Caching.Interfaces;
+using Mapster;
+
+namespace Hotline.Api.Controllers.OrderModuleControllers
+{
+    /// <summary>
+    /// 工单抄送
+    /// </summary>
+    public class OrderCarbonCopyController : BaseController
+    {
+        private readonly IOrderCarbonCopyApplication _orderCarbonCopyApplication;
+        private readonly ISystemDicDataCacheManager _sysDicDataCacheManager;
+        private readonly IMapper _mapper;
+
+        public OrderCarbonCopyController(
+            IOrderCarbonCopyApplication orderCarbonCopyApplication,
+            ISystemDicDataCacheManager sysDicDataCacheManager,
+            IMapper mapper
+        )
+        {
+            _orderCarbonCopyApplication = orderCarbonCopyApplication;
+            _sysDicDataCacheManager = sysDicDataCacheManager;
+            _mapper = mapper;
+        }
+
+        [HttpGet]
+        public async Task<IReadOnlyList<OrderDto>> Query([FromQuery] QueryCarbonCopyRequest dto)
+        {
+            var orderccs = await _orderCarbonCopyApplication
+                .Query(dto)
+                .ToPageListWithoutTotalAsync(dto, HttpContext.RequestAborted);
+
+            var orderdtos = orderccs.Select(d => d.Order.Adapt<OrderDto>()).ToList();
+            return orderdtos;
+        }
+
+        [HttpGet("count")]
+        public async Task<int> Count([FromQuery] QueryCarbonCopyRequest dto)
+        {
+            return await _orderCarbonCopyApplication
+                .Query(dto)
+                .CountAsync(HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 列表页面基础数据
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("base-data")]
+        public async Task<object> BaseData()
+        {
+            var rsp = new
+            {
+                AcceptTypeOptions = _sysDicDataCacheManager.GetSysDicDataCache(SysDicTypeConsts.AcceptType),
+            };
+            return rsp;
+        }
+    }
+}

+ 1 - 1
src/Hotline.Api/Controllers/OrderControlls/OrderComplementController.cs → src/Hotline.Api/Controllers/OrderModuleControllers/OrderComplementController.cs

@@ -2,7 +2,7 @@
 using Hotline.Share.Dtos.Order;
 using Microsoft.AspNetCore.Mvc;
 
-namespace Hotline.Api.Controllers.OrderControlls;
+namespace Hotline.Api.Controllers.OrderModuleControllers;
 
 public class OrderComplementController : BaseController
 {

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

@@ -199,6 +199,7 @@ namespace Hotline.Api.Controllers
 			else
 				model.FileJson = new List<Share.Dtos.File.FileJson>();
 
+			model.Status = ETerminateStatus.Approval;
 			model.Content = dto.Data.Content;
 			model.IsRecommit = true;
 			await _orderTerminateRepository.UpdateAsync(model, HttpContext.RequestAborted);

+ 561 - 0
src/Hotline.Api/Controllers/PlanController.cs

@@ -0,0 +1,561 @@
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Authentications;
+using XF.Domain.Repository;
+using Hotline.Application.Planlibrary;
+using Hotline.Share.Dtos.Planlibrary;
+using SqlSugar;
+using Hotline.Planlibrary;
+using XF.Domain.Exceptions;
+using Hotline.Share.Dtos;
+using Hotline.Share.Tools;
+using Hotline.Share.Enums.Planlibrary;
+using Hotline.Application.ExportWord;
+using Hotline.Application.Tools;
+using Hotline.Share.Enums.Article;
+using XF.Utility.EnumExtensions;
+using Hotline.Share.Dtos.Order;
+using Hotline.Application.ExportExcel;
+
+namespace Hotline.Api.Controllers
+{
+    /// <summary>
+    /// 预案库
+    /// </summary>
+    public class PlanController : BaseController
+    {
+
+
+        #region 注入
+
+        private readonly IMapper _mapper;
+        private readonly ISessionContext _sessionContext;
+        private readonly IPlanApplication _planApplication;
+        private readonly IRepository<PlanType> _planTypeRepository;
+        private readonly IRepository<PlanList> _planListRepository;
+        private readonly IRepository<PlanCollect> _planCollectRepository;
+        private readonly IWordHelperService _wordHelperService;
+        private readonly IExportApplication _exportApplication;
+
+        public PlanController(
+           IMapper mapper,
+           ISessionContext sessionContext,
+           IPlanApplication planApplication,
+           IRepository<PlanType> planTypeRepository,
+           IRepository<PlanList> planListRepository,
+           IRepository<PlanCollect> planCollectRepository,
+           IWordHelperService wordHelperService,
+           IExportApplication exportApplication)
+        {
+            _mapper = mapper;
+            _sessionContext = sessionContext;
+            _planApplication = planApplication;
+            _planTypeRepository = planTypeRepository;
+            _planListRepository = planListRepository;
+            _planCollectRepository = planCollectRepository;
+            _wordHelperService = wordHelperService;
+            _exportApplication = exportApplication;
+        }
+
+        #endregion
+
+        #region 预案库类型管理
+
+        /// <summary>
+        /// 获取列表层级分类
+        /// </summary>
+        /// <param name="IsEnable">是否启用</param>
+        /// <returns></returns>
+        [HttpGet("type/treelist")]
+        public async Task<List<PlanTypeDto>> QueryAllTreeList(bool? IsEnable)
+        {
+            return await _planTypeRepository.Queryable()
+                .WhereIF(IsEnable.HasValue, x => x.IsEnable == IsEnable)
+                .Where(x => SqlFunc.Subqueryable<PlanTypeOrg>().Where(to => to.TypeId == x.Id).Any() ||
+                SqlFunc.Subqueryable<PlanTypeOrg>().Where(to => to.TypeId == x.Id).NotAny()
+                )
+                .Select(x => new PlanTypeDto()
+                {
+                    Id = x.Id.SelectAll()
+                }
+                )
+                .OrderBy(x => x.Sort).ToTreeAsync(it => it.children, it => it.ParentId, null, it => it.Id);
+        }
+
+        /// <summary>
+        /// 新增
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("type/add")]
+        public async Task<string> AddType([FromBody] AddPlanTypeDto dto)
+        {
+            return await _planApplication.AddTypeAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 编辑
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("type/update")]
+        public async Task UpdateType([FromBody] UpdatePlanTypeDto dto)
+        {
+            await _planApplication.UpdateTypeAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 查询详情
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        [HttpGet("type/info/{Id}")]
+        public async Task<PlanType> GetType(string Id)
+        {
+            var types = await _planTypeRepository.Queryable()
+                .Includes(x => x.PlanTypeOrgs)   // 填充子对象
+                .Where(x => x.Id == Id)
+                .FirstAsync(HttpContext.RequestAborted);
+            if (types is null)
+                throw UserFriendlyException.SameMessage("查询失败!");
+            return types;
+        }
+
+        /// <summary>
+        /// 删除
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        [HttpDelete("type/remove/{Id}")]
+        public async Task RemoveType(string Id)
+        {
+            await _planApplication.RemoveTypeAsync(Id, HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+        #region 预案库管理
+
+        /// <summary>
+        /// 预案库分类列表
+        /// </summary>
+        /// <param name="IsEnable"></param>
+        /// <returns></returns>
+        [HttpGet("list/treelist")]
+        public async Task<List<PlanTypeDto>> QueryAllPlanTypeTreeList(bool? IsEnable)
+        {
+            return await _planTypeRepository.Queryable()
+                .WhereIF(IsEnable.HasValue, x => x.IsEnable == IsEnable)
+                .Where(x => SqlFunc.Subqueryable<PlanTypeOrg>().Where(to => to.TypeId == x.Id).Any() ||
+                            SqlFunc.Subqueryable<PlanTypeOrg>().Where(to => to.TypeId == x.Id).NotAny()
+                )
+                .Select(x => new PlanTypeDto()
+                {
+                    Id = x.Id.SelectAll(),
+                    PlanNum = SqlFunc.Subqueryable<PlanRelationType>().LeftJoin<PlanList>((kr, k) => kr.PlanId == k.Id)
+                         .Where((kr, k) => kr.PlanTypeId == x.Id)
+                         .DistinctCount(kr => kr.PlanId)
+                }
+                )
+                .OrderBy(x => x.Sort).ToTreeAsync(it => it.children, it => it.ParentId, null, it => it.Id);
+        }
+
+        /// <summary>
+        /// 预案库列表
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <returns></returns>
+        [HttpGet("list")]
+        public async Task<PagedDto<PlanDataDto>> QueryAllPlanList([FromQuery] PlanListDto pagedDto)
+        {
+            return (await _planApplication.QueryAllPlanListAsync(pagedDto, HttpContext.RequestAborted)).ToPaged();
+        }
+
+        /// <summary>
+        /// 预案库草稿
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("list/draft")]
+        public async Task<string> PlanDraft([FromBody] AddPlanListDto dto)
+        {
+            dto.Status = EPlanStatus.NewDrafts;
+            return await _planApplication.AddPlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库草稿修改
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/draftupdate")]
+        public async Task UpdatePlanDraft([FromBody] UpdatePlanListDto dto)
+        {
+            dto.Status = EPlanStatus.NewDrafts;
+            await _planApplication.UpdatePlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库草稿到审核
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/draftadd")]
+        public async Task AddPlanDraft([FromBody] UpdatePlanListDto dto)
+        {
+            dto.ApplyStatus = EPlanApplyStatus.Add;
+            dto.Status = EPlanStatus.OnShelf;
+            await _planApplication.UpdatePlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库草稿删除
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        [HttpDelete("list/draftremove/{Id}")]
+        public async Task RemovePlanDraft(string Id)
+        {
+            UpdatePlanListDto dto = new UpdatePlanListDto();
+            dto.ApplyStatus = EPlanApplyStatus.Delete;
+            dto.Status = EPlanStatus.Auditing;
+            dto.Id = Id;
+            await _planApplication.RemovePlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库删除到审核
+        /// </summary>
+        /// <param name="dtoDel"></param>
+        /// <returns></returns>
+        [HttpDelete("list/remove")]
+        public async Task RemovePlan([FromBody] DelPlanListDto dtoDel)
+        {
+            UpdatePlanListDto dto = new UpdatePlanListDto();
+            dto.ApplyStatus = EPlanApplyStatus.Delete;
+            dto.Status = EPlanStatus.Auditing;
+            dto.Id = dtoDel.Id;
+            dto.ApplyReason = dtoDel.ApplyReason;
+            await _planApplication.UpdatePlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库新增到审核
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("list/add")]
+        public async Task<string> AddPlan([FromBody] AddPlanListDto dto)
+        {
+            dto.ApplyStatus = EPlanApplyStatus.Add;
+            dto.Status = EPlanStatus.Auditing;
+            return await _planApplication.AddPlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库编辑提交到审核
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/update")]
+        public async Task UpdatePlan([FromBody] UpdatePlanListDto dto)
+        {
+            dto.ApplyStatus = EPlanApplyStatus.Update;
+            dto.Status = EPlanStatus.Auditing;
+            await _planApplication.UpdatePlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库上架
+        /// </summary>
+        /// <param name="Id">预案库ID</param>
+        /// <returns></returns>
+        [HttpGet("list/onshelf/{Id}")]
+        public async Task OnshelfPlan(string Id)
+        {
+            UpdatePlanListDto dto = new UpdatePlanListDto();
+            dto.Id = Id;
+            dto.ApplyStatus = EPlanApplyStatus.Add;
+            dto.Status = EPlanStatus.OnShelf;
+            dto.OnShelfTime = DateTime.Now;
+            await _planApplication.AuditPlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库下架
+        /// </summary>
+        /// <param name="Id">预案库ID</param>
+        /// <returns></returns>
+        [HttpGet("list/offshelf/{Id}")]
+        public async Task OffshelfPlan(string Id)
+        {
+            UpdatePlanListDto dto = new UpdatePlanListDto();
+            dto.Id = Id;
+            dto.ApplyStatus = EPlanApplyStatus.Offshelf;
+            dto.Status = EPlanStatus.OffShelf;
+            dto.OffShelfTime = DateTime.Now;
+
+            await _planApplication.AuditPlanAsync(dto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库审核(新增、修改、删除)
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/examin")]
+        public async Task ExaminPlan([FromBody] AuditPlanListDto dto)
+        {
+            var plan = await _planListRepository.GetAsync(dto.Id);
+            if (plan == null)
+                throw UserFriendlyException.SameMessage("预案库查询失败");
+
+            var planDto = _mapper.Map<UpdatePlanListDto>(plan);
+
+            if (dto.State == 0)
+            {//不同意
+                planDto.Status = EPlanStatus.Revert;
+            }
+            else if (dto.State == 1)
+            {//同意 
+                if (planDto.ApplyStatus == EPlanApplyStatus.Add)
+                {
+                    planDto.Status = EPlanStatus.OnShelf;
+                    planDto.OnShelfTime = DateTime.Now;
+                }
+                if (planDto.ApplyStatus == EPlanApplyStatus.Update)
+                {
+                    planDto.Status = EPlanStatus.OnShelf;
+                    planDto.OnShelfTime = DateTime.Now;
+                    planDto.UpdateTime = DateTime.Now;
+                }
+                if (planDto.ApplyStatus == EPlanApplyStatus.Offshelf)
+                {
+                    planDto.Status = EPlanStatus.OffShelf;
+                    planDto.OffShelfTime = DateTime.Now;
+                }
+                if (planDto.ApplyStatus == EPlanApplyStatus.Delete)
+                {
+                    planDto.Status = EPlanStatus.Drafts;
+                }
+            }
+            planDto.Id = dto.Id;
+            planDto.ExaminTime = DateTime.Now;
+            planDto.ExaminManId = _sessionContext.UserId;
+            planDto.ExaminOrganizeId = _sessionContext.OrgId;
+            planDto.ExaminOpinion = dto.ExaminOpinion;
+
+            await _planApplication.AuditPlanAsync(planDto, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库详情
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <param name="IsAddPv"></param>
+        /// <returns></returns>
+        [HttpGet("list/info")]
+        public async Task<PlanInfoDto> GetPlan(string Id, bool IsAddPv)
+        {
+            return await _planApplication.GetPlanAsync(Id, IsAddPv, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 预案库申请理由
+        /// </summary>
+        /// <param name="Id">预案库ID</param>
+        /// <returns></returns>
+        [HttpGet("list/reason/{Id}")]
+        public async Task<PlanApplyReasonDto> ReasonPlan(string Id)
+        {
+            var reason = await _planListRepository.GetAsync(x => x.Id == Id);
+            return _mapper.Map<PlanApplyReasonDto>(reason);
+        }
+
+        /// <summary>
+        /// 预案库评分
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("list/score")]
+        public async Task ScorePlan([FromBody] PvPlanListDto dto)
+        {
+            var collect = await _planCollectRepository.GetAsync(x => x.PlanId == dto.Id && x.CreatorId == _sessionContext.UserId);
+            if (collect != null)
+            {
+                if (collect.Score > 0)
+                    throw UserFriendlyException.SameMessage("当前知识已经评分");
+
+                collect.Score = dto.Score;
+                await _planCollectRepository.UpdateAsync(collect, HttpContext.RequestAborted);
+            }
+            else
+            {
+                collect = new PlanCollect();
+                collect.PlanId = dto.Id;
+                collect.Score = dto.Score;
+                await _planCollectRepository.AddAsync(collect, HttpContext.RequestAborted);
+            }
+
+            //计算总分
+            var sugar = _planCollectRepository.Queryable().Where(x => x.PlanId == dto.Id);
+            var count = await sugar.CountAsync();
+            var collects = await sugar.SumAsync(x => x.Score);
+            var scoreTemp = collects / count;
+            var plan = await _planListRepository.GetAsync(x => x.Id == dto.Id);
+            if (plan != null)
+            {
+                plan.Score = decimal.Round(scoreTemp.Value, 1);
+                await _planListRepository.UpdateAsync(plan, HttpContext.RequestAborted);
+            }
+        }
+
+        /// <summary>
+        /// 预案库查重
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("list/exist")]
+        public async Task<bool> ExistPlan([FromBody] PlanExistDto dto)
+        {
+            var any = await _planListRepository.Queryable()
+                .Where(x => x.Status == EPlanStatus.Auditing || x.Status >= EPlanStatus.OnShelf)
+                .WhereIF(!string.IsNullOrEmpty(dto.Title), x => x.Title.Equals(dto.Title))
+                .WhereIF(!string.IsNullOrEmpty(dto.Content), x => x.Content.Equals(dto.Content))
+                .WhereIF(!string.IsNullOrEmpty(dto.Id), x => x.Id != dto.Id)
+                .AnyAsync();
+            return any;
+        }
+
+        /// <summary>
+        /// 预案库详情导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("list/info/export")]
+        public async Task<IActionResult> PlanInfoExport([FromBody] PlanInfoExportDto dto)
+        {
+            if (dto.Ids.Length > 1)
+            {
+                var streams = await _planApplication.PlanInfoListExportAsync(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 _planListRepository.GetAsync(dto.Ids[0]) ?? throw UserFriendlyException.SameMessage("预案不存在");
+            return info.Content.HtmlToStream(dto.FileType).GetFileStreamResult(dto.FileType, info.Title, false);
+        }
+
+        /// <summary>
+        /// 预案库列表导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("export")]
+        public async Task<IActionResult> GetPlanListExportAsync([FromBody] ExportExcelDto<PlanListDto> dto)
+        {
+            var items = (await _planApplication.QueryAllPlanListAsync(dto.QueryDto, HttpContext.RequestAborted)).Item2;
+            return _exportApplication.GetExcelFile(dto, items, "预案明细导出");
+        }
+
+
+        /// <summary>
+        /// 预案库列表页面枚举值
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("list/status-data")]
+        public Dictionary<string, dynamic> PlanStatus()
+        {
+            var tabStatusName = new List<KeyValuePair<int, string>>
+            {
+                new KeyValuePair<int, string>(3, "已上架"),
+                new KeyValuePair<int, string>(4, "已下架"),
+                new KeyValuePair<int, string>(1, "审核中"),
+                new KeyValuePair<int, string>(7, "草稿"),
+                new KeyValuePair<int, string>(-1, "全部")
+            };
+
+            var tabExamineName = new List<KeyValuePair<int, string>>
+            {
+                new KeyValuePair<int, string>(-1, "全部"),
+                new KeyValuePair<int, string>(0, "新增审核"),
+                new KeyValuePair<int, string>(1, "修改审核"),
+                new KeyValuePair<int, string>(2, "删除审核")
+            };
+
+            var StatusName = new List<KeyValuePair<int, string>>
+            {
+                new KeyValuePair<int, string>(-1, "全部"),
+                new KeyValuePair<int, string>(0, "待提交"),
+                new KeyValuePair<int, string>(1, "审核中"),
+                new KeyValuePair<int, string>(3, "已上架"),
+                new KeyValuePair<int, string>(4, "已下架"),
+                new KeyValuePair<int, string>(5, "审核不通过"),
+                new KeyValuePair<int, string>(6, "已过期"),
+                new KeyValuePair<int, string>(7, "草稿")
+            };
+
+            var ApplyStatusName = new List<KeyValuePair<int, string>>
+            {
+                new KeyValuePair<int, string>(-1, "全部"),
+                new KeyValuePair<int, string>(0, "新增审核"),
+                new KeyValuePair<int, string>(1, "修改审核"),
+                new KeyValuePair<int, string>(2, "删除审核")
+            };
+
+            var ignoreFileType = EFileType.excel | EFileType.pdf;
+            var items = EnumExts.GetDescriptions<EFileType>();
+            var filteredDictionary = items
+                 .Where(kvp => (ignoreFileType & (EFileType)kvp.Key) == 0)
+                 .ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
+                 .ToList();
+
+            return new Dictionary<string, dynamic>
+            {
+                { "fileType", filteredDictionary},
+                { "tabStatusName", tabStatusName },
+                { "tabExamineName", tabExamineName },
+                { "StatusName", StatusName },
+                { "ApplyStatusName", ApplyStatusName }
+            };
+        }
+
+        #endregion
+
+        #region 预案库检索
+
+        /// <summary>
+        /// 检索列表
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <returns></returns>
+        [HttpGet("search")]
+        public async Task<PagedDto<PlanDataDto>> QueryOnShelfPlanList([FromQuery] PlanListDto pagedDto)
+        {
+            pagedDto.Status = EPlanStatus.OnShelf;
+            return (await _planApplication.QueryAllPlanListAsync(pagedDto, HttpContext.RequestAborted)).ToPaged();
+        }
+
+        /// <summary>
+        /// 检索列表前10
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("search/top10")]
+        public async Task<List<PlanPageViewDto>> QueryTop10PlanList()
+        {
+            return await _planListRepository.Queryable()
+                .Take(10)
+                .Where(x => x.Status == EPlanStatus.OnShelf)
+                .Select(x => new PlanPageViewDto
+                {
+                    Id = x.Id,
+                    Title = x.Title,
+                    PageView = x.PageView
+                })
+                .OrderBy(x => x.PageView, OrderByType.Desc)
+                .ToListAsync();
+        }
+
+        #endregion
+    }
+}

+ 69 - 9
src/Hotline.Api/Controllers/QualityController.cs

@@ -21,6 +21,9 @@ using Hotline.Application.CallCenter;
 using Hotline.CallCenter.Configs;
 using Microsoft.Extensions.Options;
 using Hotline.Configurations;
+using Hotline.CallCenter.Calls;
+using System.Linq;
+using Hotline.Repository.SqlSugar.Quality;
 
 namespace Hotline.Api.Controllers
 {
@@ -47,8 +50,9 @@ namespace Hotline.Api.Controllers
         private readonly ICallApplication _callApplication;
         private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
         private readonly IRepository<SystemLog> _logRepository;
+		private readonly IRepository<QualityTransferRecords> _qualityTransferRecordsRepository;
 
-        public QualityController(
+		public QualityController(
             ISessionContext sessionContext,
             IMapper mapper,
             IQualityRepository qualitey,
@@ -67,7 +71,8 @@ namespace Hotline.Api.Controllers
             ISystemSettingCacheManager systemSettingCacheManager,
             ICallApplication callApplication,
             IOptionsSnapshot<AppConfiguration> appOptions,
-            IRepository<SystemLog> logRepository)
+            IRepository<SystemLog> logRepository,
+			IRepository<QualityTransferRecords> qualityTransferRecordsRepository)
         {
             _sessionContext = sessionContext;
             _mapper = mapper;
@@ -88,7 +93,8 @@ namespace Hotline.Api.Controllers
             _callApplication = callApplication;
             _appOptions = appOptions;
             _logRepository = logRepository;
-        }
+            _qualityTransferRecordsRepository = qualityTransferRecordsRepository;
+		}
         #region 质检管理
         /// <summary>
         /// 删除质检
@@ -611,12 +617,66 @@ namespace Hotline.Api.Controllers
             return rsp;
         }
 
-        /// <summary>
-        /// 智能质检结果返回接收 
-        /// </summary>
-        /// <param name="dto"></param>
-        /// <returns></returns>
-        [AllowAnonymous]
+		/// <summary>
+		/// 智能质检转写_兴唐
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[HttpPost("aitransfer_xt")]
+		[LogFilter("智能质检转写_兴唐")]
+		public async Task AiTransfer_XT([FromBody] List<AiQualityXTDto> dto)
+		{
+			var transfers = new List<QualityTransferRecords>();
+			foreach (var item in dto)
+			{
+                var records =  await _qualityTransferRecordsRepository.Queryable().Where(x=>x.QualityId == item.Id  &&  x.IsFinished == false).AnyAsync();
+                if (records)
+                    continue;
+
+				var transfer = new QualityTransferRecords();
+				transfer.QualityId = item.Id;
+				transfer.IsFinished = false;
+				transfer.TransferState = EQualityTransferState.NotStarted;
+				transfers.Add(transfer);
+				await _qualitey.Updateable().SetColumns(x => new Hotline.Quality.Quality { TransferState = EQualityTransferState.Translating }).Where(x => x.Id == item.Id).ExecuteCommandAsync();
+			}
+			await _qualityTransferRecordsRepository.AddRangeAsync(transfers);
+		}
+
+		/// <summary>
+		/// 智能质检转写
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[AllowAnonymous]
+		[HttpPost("transfer")]
+		[LogFilter("智能质检转写_兴唐_定时调用")]
+		public async Task AiTransfer_XT()
+		{
+            var translatings = await _qualityTransferRecordsRepository.Queryable().Where(x => x.IsFinished == false && x.TransferState == EQualityTransferState.Translating).OrderBy(x => x.TransferTime).ToListAsync();
+            if (translatings.Any())
+            {
+                var transfer = translatings.FirstOrDefault();
+                if ((DateTime.Now - transfer.TransferTime.Value).TotalMinutes >= 30)
+                {
+                    await _qualityTransferRecordsRepository.Updateable().SetColumns(x => new QualityTransferRecords { TransferState = EQualityTransferState.Lose }).Where(x => x.Id == transfer.Id).ExecuteCommandAsync();
+                }
+                return;
+            }
+            else {
+				var notStarted = await _qualityTransferRecordsRepository.Queryable().Where(x => x.IsFinished == false && x.TransferState == EQualityTransferState.NotStarted).OrderBy(x => x.TransferTime).FirstAsync();
+				await _qualityTransferRecordsRepository.Updateable().SetColumns(x => new QualityTransferRecords { TransferState = EQualityTransferState.Translating , TransferTime = DateTime.Now }).Where(x => x.Id == notStarted.Id).ExecuteCommandAsync();
+				_qualityApplication.Transfer_XT(notStarted.QualityId, HttpContext.RequestAborted);
+			}
+		}
+
+
+		/// <summary>
+		/// 智能质检结果返回接收 
+		/// </summary>
+		/// <param name="dto"></param>
+		/// <returns></returns>
+		[AllowAnonymous]
         [HttpPost("AiResult")]
         [LogFilter("智能质检结果返回接收")]
         public async Task AiResult([FromBody] List<AiQualityResultDto> dto)

+ 290 - 2
src/Hotline.Api/Controllers/Snapshot/IndustryController.cs

@@ -1,7 +1,18 @@
-using Hotline.Application.Snapshot;
+using Amazon.Runtime.Internal.Transform;
+using Hotline.Application.Snapshot;
+using Hotline.Caching.Interfaces;
+using Hotline.Configurations;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Settings;
+using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
 using Hotline.Snapshot;
+using Hotline.Snapshot.Interfaces;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using SqlSugar;
 
 namespace Hotline.Api.Controllers.Snapshot;
 
@@ -11,14 +22,36 @@ namespace Hotline.Api.Controllers.Snapshot;
 public class IndustryController : BaseController
 {
     private readonly IIndustryRepository _industryRepository;
+    private readonly ISystemDicDataCacheManager _systemDicDataCacheManager;
     private readonly IIndustryApplication _industryApplication;
+    private readonly ISystemAreaDomainService _systemAreaDomainService;
+    private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
+    private readonly ISystemOrganizeRepository _systemOrganizeRepository;
 
-    public IndustryController(IIndustryRepository industryRepository, IIndustryApplication industryApplication)
+    public IndustryController(IIndustryRepository industryRepository, IIndustryApplication industryApplication, ISystemDicDataCacheManager systemDicDataCacheManager, ISystemAreaDomainService systemAreaDomainService, IOptionsSnapshot<AppConfiguration> appOptions, ISystemOrganizeRepository systemOrganizeRepository)
     {
         _industryRepository = industryRepository;
         _industryApplication = industryApplication;
+        _systemDicDataCacheManager = systemDicDataCacheManager;
+        _systemAreaDomainService = systemAreaDomainService;
+        _appOptions = appOptions;
+        _systemOrganizeRepository = systemOrganizeRepository;
     }
 
+    /// <summary>
+    /// 添加行业页面基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("basedata")]
+    public async Task<Dictionary<string, object>> GetBaseData()
+    {
+        return new Dictionary<string, object>
+        {
+            { "department", await _systemOrganizeRepository.GetOrgEnabled() },
+            { "acceptType", _systemDicDataCacheManager.AcceptType},
+            { "bulletinType", _systemDicDataCacheManager.SnapshotBulletinType}
+        };
+    }
     /// <summary>
     /// 新增行业
     /// </summary>
@@ -26,4 +59,259 @@ public class IndustryController : BaseController
     [HttpPost]
     public async Task<string> AddIndustry([FromBody] AddIndustryDto dto)
         => await _industryApplication.AddIndustryAsync(dto, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 行业详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("{id}")]
+    public async Task<IndustryDetailOutDto> GetIndustryDetailAsync(string id)
+        => await _industryApplication.GetIndustryDetailAsync(id);
+
+    /// <summary>
+    /// 获取行业集合
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("industry")]
+    public async Task<PagedDto<IndustryItemsOutDto>> GetIndustres([FromQuery] IndustryListInDto dto)
+        => (await _industryApplication.GetIndustres(dto).ToPagedListAsync(dto, HttpContext.RequestAborted)).ToPaged();
+
+    /// <summary>
+    /// 修改行业
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut]
+    public async Task UpdateIndustry([FromBody] UpdateIndustryInDto dto)
+        => await _industryApplication.UpdateIndustryAsync(dto, HttpContext.RequestAborted);
+
+    #region 行业线索
+    /// <summary>
+    /// 行业线索集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("case")]
+    public async Task<PagedDto<IndustryCaseItemOutDto>> GetIndustryCaseItems([FromQuery] IndustryCaseItemInDto dto)
+        => (await _industryApplication.GetIndustryCaseItems(dto).ToPagedListAsync(dto, HttpContext.RequestAborted)).ToPaged();
+
+    /// <summary>
+    /// 添加行业线索
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("case")]
+    public async Task AddIndustryCaseAsync([FromBody] AddIndustryCaseDto dto)
+        => await _industryApplication.AddIndustryCaseAsync(dto);
+
+    /// <summary>
+    /// 更新行业线索
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("case")]
+    public async Task UpdateIndustryCaseAsync([FromBody] UpdateIndustryCaseDto dto)
+        => await _industryApplication.UpdateIndustryCaseAsync(dto);
+
+    /// <summary>
+    /// 获取行业线索详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("case/{id}")]
+    public async Task<IndustryCase> GetIndustryCaseDetailAsync(string id)
+        => await _industryApplication.GetIndustryCaseAsync(id);
+
+    /// <summary>
+    /// 页面基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("case/database")]
+    public async Task<Dictionary<string, object>> GetIndustryCaseDataBaseAsync()
+    {
+        var items = await _industryRepository.Queryable()
+            .Select(m => new { m.Id, m.Name })
+            .ToListAsync();
+        return new Dictionary<string, object>
+        {
+            { "industry", items }
+        };
+    }
+    #endregion
+
+    #region 行业短信模板
+    /// <summary>
+    /// 行业短信模板集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("sms_template")]
+    public async Task<PagedDto<SnapshotSMSTemplateItemsOutDto>> GetSMSTemplates([FromQuery] SnapshotSMSTemplateItemsInDto dto)
+        => (await _industryApplication.GetSMSTemplates(dto).ToPagedListAsync(dto, HttpContext.RequestAborted)).ToPaged();
+
+    /// <summary>
+    /// 添加行业短信模板
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("sms_template")]
+    public async Task AddSmsTemplateAsync([FromBody] AddSnapshotSMSTemplateInDto dto)
+        => await _industryApplication.AddSMSTemplateAsync(dto);
+
+    /// <summary>
+    /// 修改行业短信模板
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("sms_template")]
+    public async Task UpdateSmsTemplateAsync([FromBody] UpdateSnapshotSMSTemplateInDto dto)
+        => await _industryApplication.UpdateSMSTemplateAsync(dto);
+
+    /// <summary>
+    /// 短信详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("sms_template/{id}")]
+    public async Task<SnapshotSMSTemplateItemsOutDto> GetSMSTemplateDetailAsync(string id)
+        => await _industryApplication.GetSMSTemplateDetailAsync(id);
+
+    /// <summary>
+    /// 删除短信模板
+    /// </summary>
+    /// <param name="ids"></param>
+    /// <returns></returns>
+    [HttpDelete("sms_template")]
+    public async Task DeleteSmsTemplateAsync([FromBody] IList<string> ids)
+        => await _industryApplication.DeleteSMSTemplateAsync(ids);
+    #endregion
+
+    #region 区域从业人员
+    /// <summary>
+    /// 区域从业人员集合
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("practitioner")]
+    public async Task<PagedDto<PractitionerItemsOutDto>> GetPractitionerItems([FromQuery] PractitionerItemsInDto dto)
+        => (await _industryApplication.GetPractitionerItems(dto).ToPagedListAsync(dto, HttpContext.RequestAborted)).ToPaged();
+
+    /// <summary>
+    /// 添加区域从业人员
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("practitioner")]
+    public async Task<string> AddPractitionerAsync([FromBody] AddPractitionerInDto dto)
+        => await _industryApplication.AddPractitionerAsync(dto);
+
+    /// <summary>
+    /// 删除区域从业人员
+    /// </summary>
+    /// <param name="ids"></param>
+    /// <returns></returns>
+    [HttpDelete("practitioner")]
+    public async Task DeletePractitionerAsync([FromBody] IList<string> ids)
+        => await _industryApplication.DeletePractitionerAsync(ids);
+
+    /// <summary>
+    /// 修改区域从业人员
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("practitioner")]
+    public async Task UpdatePractitionerAsync(UpdatePractitionerInDto dto)
+        => await _industryApplication.UpdatePractitionerAsync(dto);
+
+    /// <summary>
+    /// 从业人员详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("practitioner/{id}")]
+    public async Task<PractitionerItemsOutDto> GetPractitionerAsync(string id)
+        => await _industryApplication.GetPractitionerAsync(id);
+
+    /// <summary>
+    /// 添加从业人员基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("practitioner/basedata")]
+    public async Task<Dictionary<string, object>> GetPractitionerDataBaseAsync()
+    {
+        var parentId = "";
+        if (_appOptions.Value.IsZiGong) parentId = _appOptions.Value.ZiGong.AreaCode;
+        return new Dictionary<string, object>
+        {
+            { "area", await _systemAreaDomainService.GetAreaTree(parentId: parentId)}
+        };
+    }
+    #endregion
+
+    #region 志愿者
+    /// <summary>
+    /// 志愿者集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("volunteer")]
+    public async Task<PagedDto<VolunteerItemsOutDto>> GetVolunteerItems([FromQuery] VolunteerItemsInDto dto)
+        => (await _industryApplication.GetVolunteerItems(dto).ToPagedListAsync(dto, HttpContext.RequestAborted)).ToPaged();
+
+    /// <summary>
+    /// 添加志愿者
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("volunteer")]
+    public async Task<string> AddVolunteerAsync([FromBody] AddVolunteerInDto dto)
+        => await _industryApplication.AddVolunteerAsync(dto);
+
+    /// <summary>
+    /// 批量删除志愿者
+    /// </summary>
+    /// <param name="ids"></param>
+    /// <returns></returns>
+    [HttpDelete("volunteer")]
+    public async Task DeleteVolunteerAsync([FromBody] IList<string> ids)
+        => await _industryApplication.DeleteVolunteerAsync(ids);
+
+    /// <summary>
+    /// 志愿者详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("volunteer/{id}")]
+    public async Task<Volunteer> GetVolunteerAsync(string id)
+        => await _industryApplication.GetVolunteerAsync(id);
+
+    /// <summary>
+    /// 修改志愿者
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("volunteer")]
+    public async Task UpdateVolunteerAsync([FromBody] UpdateVolunteerInDto dto)
+        => await _industryApplication.UpdateVolunteerAsync(dto);
+
+    /// <summary>
+    /// 志愿者上报集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("volunteer/report")]
+    public async Task<PagedDto<VolunteerReportItemsOutDto>> GetVolunteerReportItems([FromQuery] VolunteerReportItemsInDto dto)
+        => (await _industryApplication.GetVolunteerReportItems(dto).ToPagedListAsync(dto)).ToPaged();
+    #endregion
+
+    #region 修改行业记录
+    /// <summary>
+    /// 行业修改记录
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("order/industry/log")]
+    public async Task<PagedDto<IndustryLogItemsOutDto>> GetIndustryLogItems([FromQuery] IndustryLogItemsInDto dto)
+        => (await _industryApplication.GetIndustryLogItems(dto).ToPagedListAsync(dto)).ToPaged();
+    #endregion
 }

+ 102 - 0
src/Hotline.Api/Controllers/Snapshot/InviteCodeController.cs

@@ -0,0 +1,102 @@
+using Hotline.Application.Snapshot;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
+using Hotline.Snapshot;
+using Hotline.Snapshot.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using System.ComponentModel;
+
+namespace Hotline.Api.Controllers.Snapshot;
+
+/// <summary>
+/// 邀请码管理
+/// </summary>
+[Description("邀请码管理")]
+public class InviteCodeController : BaseController
+{
+    private readonly IInviteCodeApplication _inviteCodeApplication;
+
+    public InviteCodeController(IInviteCodeApplication inviteCodeApplication)
+    {
+        _inviteCodeApplication = inviteCodeApplication;
+    }
+
+    /// <summary>
+    /// 获取添加邀请码基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("basedata")]
+    public async Task<Dictionary<string, object>> GetBasedataAsync()
+    {
+        return new Dictionary<string, object>
+        {
+            { "OrgName", await _inviteCodeApplication.GetInviteCodeItems().Where(m => m.ParentOrgId == null)
+            .Select(m => new Kv{ Key = m.Id, Value = m.OrgName}).ToListAsync() }
+        };
+    }
+
+    /// <summary>
+    /// 获取邀请码集合
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet]
+    public async Task<PagedDto<InviteCode>> GetInviteCodeItems([FromQuery] GetInviteCodeItemsInDto dto)
+        => (await _inviteCodeApplication.GetInviteCodeItems().ToPagedListAsync(dto)).ToPaged();
+
+
+    /// <summary>
+    /// 删除邀请码
+    /// </summary>
+    /// <returns></returns>
+    [HttpDelete]
+    public async Task DeleteInviteCodeAsync([FromBody] IList<string> ids)
+        => await _inviteCodeApplication.DeleteInviteCodeAsync(ids);
+
+    /// <summary>
+    /// 邀请码详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+
+    [HttpGet("{id}")]
+    public async Task<InviteCode> GetInviteCodeDetailAsync( string id)
+        => await _inviteCodeApplication.GetInviteCodeItems().Where(m => m.Id == id).FirstAsync();
+
+    /// <summary>
+    /// 修改邀请码
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut]
+    public async Task UpdateInviteCodeAsync([FromBody] UpdateInviteCodeInDto dto)
+        => await _inviteCodeApplication.UpdateInviteCodeAsync(dto);
+
+    /// <summary>
+    /// 添加部门邀请码
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost]
+    public async Task AddInviteCodeAsync([FromBody] AddInviteCodeInDto dto)
+        => await _inviteCodeApplication.AddInviteCodeAsync(dto);
+
+    /// <summary>
+    /// 邀请码统计
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("statistic")]
+    public async Task<IList<InviteCodeStatisticOutDto>> GetInviteCodeStatisticAsync([FromQuery] GetInviteCodeStatisticInDto dto)
+        => await _inviteCodeApplication.GetInviteCodeStatisticAsync(dto).ToListAsync();
+
+    /// <summary>
+    /// 邀请码统计明细
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("statistic/detail")]
+    public async Task<PagedDto<InviteCodeStatisticDetailOutDto>> GetInviteCodeStatisticDetail([FromQuery] GetInviteCodeStatisticDetailInDto dto)
+        => (await _inviteCodeApplication.GetInviteCodeStatisticDetail(dto).ToPagedListAsync(dto)).ToPaged();
+}

+ 265 - 0
src/Hotline.Api/Controllers/Snapshot/RedPackController.cs

@@ -0,0 +1,265 @@
+using Hotline.Application.Snapshot;
+using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Snapshot;
+using Microsoft.AspNetCore.Mvc;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Share.Tools;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Caching.Interfaces;
+using XF.Utility.EnumExtensions;
+using Hotline.Share.Enums.Snapshot;
+using Quartz.Impl.Triggers;
+
+namespace Hotline.Api.Controllers.Snapshot;
+
+public class RedPackController : BaseController
+{
+    private readonly IRedPackApplication _redPackApplication;
+    private readonly IIndustryRepository _industryRepository;
+    private readonly ISystemDicDataCacheManager _sysDic;
+    private readonly ISnapshotSMSTemplateRepository _snapshotSMSTemplateRepository;
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
+
+    public RedPackController(IRedPackApplication redPackApplication, IIndustryRepository industryRepository, ISystemDicDataCacheManager sysDic, ISnapshotSMSTemplateRepository snapshotSMSTemplateRepository, IOrderSnapshotRepository orderSnapshotRepository)
+    {
+        _redPackApplication = redPackApplication;
+        _industryRepository = industryRepository;
+        _sysDic = sysDic;
+        _snapshotSMSTemplateRepository = snapshotSMSTemplateRepository;
+        _orderSnapshotRepository = orderSnapshotRepository;
+    }
+
+    #region 红包审核
+    /// <summary>
+    /// 获取市民红包审批列表
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("audit")]
+    public async Task<PagedDto<SnapshotOrderAuditItemsOutDto>> GetRedPackAuditItems([FromQuery] SnapshotOrderAuditItemsInDto dto)
+        => (await _redPackApplication.GetRedPackAuditItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 获取网格员红包审批集合(政法委或应急管理局:根据账号登录返回数据)
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("audit/guider")]
+    public async Task<PagedDto<SnapshotOrderGuiderAuditItemsOutDto>> GetRedPackGuiderAuditItems([FromQuery] SnapshotOrderGuiderAuditItemsInDto dto)
+        => (await _redPackApplication.GetRedPackGuiderAuditItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 获取网格员审核详情
+    /// </summary>
+    /// <param name="id">工单Id</param>
+    /// <returns></returns>
+    [HttpGet("audit/guider/{id}")]
+    public async Task<SnapshotOrderAuditDetailOutDto> GetRedPackGuiderAuditDetailAsync(string id)
+        => await _redPackApplication.GetRedPackGuiderAuditDetailAsync(id);
+
+    /// <summary>
+    /// 网格员红包审核通过或拒绝
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("audit/guider")]
+    public async Task UpdateRedPackGuiderAuditAsync([FromBody] UpdateRedPackGuiderAuditInDto dto)
+        => await _redPackApplication.AuditRedPackGuiderAuditAsync(dto);
+
+    /// <summary>
+    /// 获取审核市民详情
+    /// </summary>
+    /// <param name="id">工单Id</param>
+    /// <returns></returns>
+    [HttpGet("audit/{id}")]
+    public async Task<SnapshotOrderAuditDetailOutDto> GetRedPackAuditDetailAsync(string id)
+        => await _redPackApplication.GetRedPackAuditDetailAsync(id);
+
+    /// <summary>
+    /// 获取特提(退回)参数
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("audit/back/{id}")]
+    public async Task<GetAuditBackBaseDataOutDto> GetAuditBackBaseDataAsync(string id)
+        => await _redPackApplication.GetAuditBackBaseDataAsync(id);
+
+    /// <summary>
+    /// 添加备注
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("audit/remark")]
+    public async Task UpdateRedPackAuditRemarkAsync([FromBody] UpdateRedPackAuditRemarkInDto dto)
+        => await _redPackApplication.UpdateRedPackAuditRemarkAsync(dto);
+
+    /// <summary>
+    /// 添加备注页面基础信息
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("audit/remark/basedata")]
+    public async Task<Dictionary<string, object>> UpdateRedPackAuditRemarkBaseDataAsync()
+    {
+        return new Dictionary<string, object>()
+        {
+            { "failCase", EnumExts.GetDescriptions<ERedPackPickupFailCase>() }
+        };
+    }
+
+    /// <summary>
+    /// 添加补充发放信息页面基础信息
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("record/basedata/{id}")]
+    public async Task<Dictionary<string, object>> UpdateRedPackRecordBaseDataAsync(string id)
+    {
+        var snapshot = await _orderSnapshotRepository.Queryable()
+            .Where(m => m.Id == id)
+            .Select(m => new
+            {
+                m.Id,
+                m.AwardBankCardNo,
+                m.AwardOpenBank,
+                m.AwardName,
+                m.AwardAmount
+            })
+            .FirstAsync();
+        var dic = new Dictionary<string, object>()
+        {
+            { "replenishType", _sysDic.SnapshotReplenishType }
+        };
+        if (snapshot != null)
+        {
+            dic.Add("bankCardNo", snapshot.AwardBankCardNo);
+            dic.Add("openBank", snapshot.AwardOpenBank);
+            dic.Add("name", snapshot.AwardName);
+            dic.Add("amount", snapshot.AwardAmount);
+        }
+        return dic;
+    }
+
+    /// <summary>
+    /// 添加补充发放信息
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("record")]
+    public async Task UpdateRedPackRecordAsync([FromBody] UpdateRedPackRecordInDto dto)
+        => await _redPackApplication.UpdateRedPackRecordAsync(dto);
+
+    /// <summary>
+    /// 补充发放集合基础信息
+    /// </summary>
+    [HttpGet("record/supplement/basedata")]
+    public async Task<Dictionary<string, object>> GetRedPackRecordSupplementBaseDataAsync()
+    {
+        return new Dictionary<string, object>()
+        {
+            { "industry", await _industryRepository.GetDataBaseAsync() }
+        };
+    }
+
+    /// <summary>
+    /// 补充发放集合
+    /// </summary>
+    [HttpGet("record/supplement")]
+    public async Task<PagedDto<SnapshotRedPackRecordSupplementItemsOutDto>> GetRedPackRecordSupplementItems([FromQuery] SnapshotRedPackRecordSupplementItemsInDto dto)
+        => (await _redPackApplication.GetRedPackRecordSupplementItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 红包审核通过或拒绝
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("audit")]
+    public async Task UpdateRedPackAuditAsync([FromBody] UpdateRedPackAuditInDto dto)
+        => await _redPackApplication.AuditRedPackAuditAsync(dto);
+
+    /// <summary>
+    /// 获取审核短信模板
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("audit/sms_template")]
+    public async Task<IList<GetRedPackAuditSMSTemplateOutDto>> GetRedPackAuditSMSTemplateAsync([FromQuery] GetRedPackAuditSMSTemplateInDto dto)
+        => await _redPackApplication.GetRedPackAuditSMSTemplateAsync(dto);
+
+    /// <summary>
+    /// 红包审批页面基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("audit/basedata")]
+    public async Task<Dictionary<string, object>> GetAuditBaseDataAsync()
+    {
+        var industry = await _industryRepository.GetDataBaseAsync();
+
+        var configAmount = await _industryRepository.Queryable()
+            .Select(m => m.CitizenReadPackAmount)
+            .Distinct()
+            .ToListAsync();
+        var sms = await _snapshotSMSTemplateRepository.Queryable()
+            .ToListAsync();
+        return new Dictionary<string, object>
+        {
+            { "industry", industry },
+            { "configAmount",  configAmount },
+            { "status", EnumExts.GetDescriptions<ERedPackAuditStatus>() }
+        };
+    }
+    #endregion
+
+    #region 红包发放记录
+
+    /// <summary>
+    /// 市民红包发放记录
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("record")]
+    public async Task<PagedDto<SnapshotRedPackRecordItemsOutDto>> GetRedPackRecordItems([FromQuery] SnapshotRedPackRecordItemsInDto dto)
+        => (await _redPackApplication.GetRedPackRecordItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 网格员红包发放记录
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("record/guider")]
+    public async Task<PagedDto<SnapshotRedPackRecordItemsGuiderOutDto>> GetRedPackRecordGuiderItems([FromQuery] SnapshotRedPackRecordItemsGuiderInDto dto)
+        => (await _redPackApplication.GetRedPackRecordGuiderItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 红包发放明细基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("record/send/database")]
+    public async Task<Dictionary<string, object>> GetRedPackRecordSendBaseDataAsync()
+    {
+        return new Dictionary<string, object>()
+        {
+            { "industry", await _industryRepository.Queryable()
+            .Select(m => new { m.Id, m.Name })
+            .ToListAsync()},
+            { "sendStatus", EnumExts.GetDescriptions<EReadPackSendStatus>().Where(m => m.Key != 2).ToList() },
+            { "userType", EnumExts.GetDescriptions<EReadPackUserType>() }
+        };
+    }
+
+    /// <summary>
+    /// 红包发放明细
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("record/send")]
+    public async Task<PagedDto<SnapshotRedPackRecordSendOutDto>> GetRedPackRecordDetail([FromQuery] SnapshotRedPackRecordSendInDto dto)
+        => (await _redPackApplication.GetRedPackRecordDetail(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 批量发送红包
+    /// </summary>
+    /// <param name="ids"></param>
+    /// <returns></returns>
+    [HttpPut("record/send_bath")]
+    public async Task<string> BathSendRedPackAsync([FromBody] List<string> ids)
+    {
+        return "ok";
+    }
+    #endregion
+}

+ 250 - 0
src/Hotline.Api/Controllers/Snapshot/SnapshotBulletinController.cs

@@ -0,0 +1,250 @@
+using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Article;
+using Hotline.Snapshot.Interfaces;
+using Microsoft.AspNetCore.Mvc;
+using Hotline.Repository.SqlSugar.Extensions;
+using Mapster;
+using Hotline.Application.Snapshot;
+using XF.Domain.Exceptions;
+using Hotline.Share.Enums.Article;
+using XF.Domain.Authentications;
+using Hotline.Caching.Interfaces;
+using Hotline.Settings;
+using Hotline.Article;
+using Hotline.Snapshot;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
+using XF.Utility.EnumExtensions;
+
+namespace Hotline.Api.Controllers.Snapshot;
+
+public class SnapshotBulletinController : BaseController
+{
+    private readonly ISnapshotBulletinRepository _bulletinRepository;
+    private readonly ISnapshotBulletinApplication _bulletinApplication;
+    private readonly ISessionContext _sessionContext;
+    private readonly ISystemDicDataCacheManager _systemDicDataCacheManager;
+    private readonly ISystemOrganizeRepository _organizeRepository;
+
+    public SnapshotBulletinController(ISnapshotBulletinRepository bulletinRepository, ISessionContext sessionContext, ISystemDicDataCacheManager systemDicDataCacheManager, ISystemOrganizeRepository organizeRepository, ISnapshotBulletinApplication bulletinApplication)
+    {
+        _bulletinRepository = bulletinRepository;
+        _sessionContext = sessionContext;
+        _systemDicDataCacheManager = systemDicDataCacheManager;
+        _organizeRepository = organizeRepository;
+        _bulletinApplication = bulletinApplication;
+    }
+
+    #region 公告
+
+    /// <summary>
+    /// 查询公告列表
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("bulletin/query")]
+    public async Task<PagedDto<SnapshotBulletinItemsOutDto>> QueryBulletinList([FromQuery] SnapshotBulletinItemsInDto dto)
+    {
+        var query = _bulletinRepository.Queryable()
+            .Includes(x => x.ExaminMan)
+            .WhereIF(!string.IsNullOrEmpty(dto.SnapshotBulletinTypeName), d => d.SnapshotBulletinTypeName.Contains(dto.SnapshotBulletinTypeName))
+            .WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Title.Contains(dto.Title))
+            .WhereIF(dto.BeginCreationTime.HasValue, d => d.BulletinTime >= dto.BeginCreationTime)
+            .WhereIF(dto.EndCreationTime.HasValue, d => d.BulletinTime <= dto.EndCreationTime)
+            .WhereIF(dto.State.HasValue, d => d.BulletinState == dto.State)
+            .OrderByDescending(d => d.CreationTime)
+            .Select<SnapshotBulletinItemsOutDto>();
+        return (await query
+            .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted)).ToPaged();
+    }
+
+    /// <summary>
+    /// 公告详情(内部)
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("bulletin/entity/{id}")]
+    public async Task<SnapshotBulletinDetailOutDto> BulletinEntity(string id)
+    {
+        var model = await _bulletinRepository.Queryable()
+            .Includes(x => x.ExaminMan)
+            .FirstAsync(x => x.Id == id, HttpContext.RequestAborted);
+
+        if (model != null && !string.IsNullOrEmpty(model.Content))
+            model.Content = _bulletinApplication.GetSiteUrls(model.Content);
+
+        return model.Adapt<SnapshotBulletinDetailOutDto>();
+    }
+
+    /// <summary>
+    /// 公告上架或者下架
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("bulletin-arrive")]
+    public async Task BulletinArrive([FromBody] BulletinArriveDto dto)
+    {
+        var bulletin = await _bulletinRepository.GetAsync(dto.Id, HttpContext.RequestAborted);
+        if (bulletin == null)
+            throw UserFriendlyException.SameMessage("无效数据");
+
+        if (bulletin.BulletinState != EBulletinState.ReviewPass)
+            throw UserFriendlyException.SameMessage("当前状态不能操作上架或下架");
+
+        bulletin.IsArrive = dto.IsArrive;
+        if (bulletin.IsArrive == false)
+        {
+            bulletin.ExaminTime = null;
+            bulletin.ExaminManId = null;
+            bulletin.ExaminOpinion = null;
+            bulletin.CommitTime = null;
+            bulletin.BulletinState = EBulletinState.Draft;
+        }
+        await _bulletinRepository.UpdateAsync(bulletin, HttpContext.RequestAborted);
+    }
+
+
+    /// <summary>
+    /// 公告审核
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("bulletin/examine")]
+    public async Task ExamineBulletin([FromBody] ExamineBulletinDto dto)
+    {
+        var bulletin = await _bulletinRepository.GetAsync(dto.Id, HttpContext.RequestAborted);
+        if (bulletin == null)
+            throw UserFriendlyException.SameMessage("无效数据");
+
+        if (bulletin.BulletinState != EBulletinState.InReview)
+            throw UserFriendlyException.SameMessage("当前状态不能审核");
+
+        if (dto.IsPass)
+        {
+            bulletin.BulletinState = Share.Enums.Article.EBulletinState.ReviewPass;
+            bulletin.BulletinTime = DateTime.Now;
+            bulletin.ExaminOpinion = dto.Reason;
+            bulletin.ExaminTime = DateTime.Now;
+            bulletin.ExaminManId = _sessionContext.RequiredUserId;
+            await _bulletinRepository.UpdateAsync(bulletin, HttpContext.RequestAborted);
+            var publishBulletin = bulletin.Adapt<PublishBulletinDto>();
+
+            //await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlinePushBulletin, publishBulletin, cancellationToken: HttpContext.RequestAborted);
+
+            //todo await _bulletinService.PushBulletin(publishBulletin, HttpContext.RequestAborted);
+        }
+        else
+        {
+            bulletin.ExaminOpinion = dto.Reason;
+            bulletin.ExaminTime = DateTime.Now;
+            bulletin.ExaminManId = _sessionContext.RequiredUserId;
+            bulletin.BulletinState = Share.Enums.Article.EBulletinState.ReviewNoPass;
+            await _bulletinRepository.UpdateAsync(bulletin, HttpContext.RequestAborted);
+        }
+    }
+
+    /// <summary>
+    /// 提交公告
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("bulletin/commit")]
+    public async Task CommitBulletin(string id)
+    {
+        var bulletin = await _bulletinRepository.GetAsync(id, HttpContext.RequestAborted);
+        if (bulletin == null)
+            throw UserFriendlyException.SameMessage("无效数据");
+
+        if (bulletin.BulletinState != EBulletinState.Draft && bulletin.BulletinState != EBulletinState.ReviewNoPass)
+            throw UserFriendlyException.SameMessage("当前状态不能提交");
+
+        bulletin.BulletinState = EBulletinState.InReview;
+        bulletin.CommitTime = DateTime.Now;
+        await _bulletinRepository.UpdateAsync(bulletin, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 修改公告
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("bulletin/update")]
+    public async Task UpdateBulletin([FromBody] UpdateSnapshotBulletinInDto dto)
+    {
+        var bulletin = await _bulletinRepository.GetAsync(dto.Id, HttpContext.RequestAborted);
+        if (bulletin == null)
+            throw UserFriendlyException.SameMessage("无效数据");
+
+        if (bulletin.BulletinState != EBulletinState.Draft && bulletin.BulletinState != EBulletinState.ReviewNoPass)
+            throw UserFriendlyException.SameMessage("当前状态不能修改");
+
+        dto.Adapt(bulletin);
+        await _bulletinRepository.UpdateAsync(bulletin, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 删除公告
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("bulletin/del/{id}")]
+    public async Task DelBulletin(string id)
+    {
+        var bulletin = await _bulletinRepository.GetAsync(id, HttpContext.RequestAborted);
+        if (bulletin == null)
+            throw UserFriendlyException.SameMessage("无效数据");
+
+        if (bulletin.BulletinState != EBulletinState.Draft)
+            throw UserFriendlyException.SameMessage("当前状态不能删除");
+
+        await _bulletinRepository.RemoveAsync(x => x.Id == id, false, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 新增公告
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("bulletin/add")]
+    public async Task AddBulletin([FromBody] AddSnapshotBulletinInDto dto)
+    {
+        var model = dto.Adapt<SnapshotBulletin>();
+        model.BulletinState = Share.Enums.Article.EBulletinState.Draft;
+        model.ReadedNum = 0;
+        if (model.BulletinTime.HasValue == false) model.BulletinTime = DateTime.Now;
+        await _bulletinRepository.AddAsync(model, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 列表页面基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("bulletin/listbasedata")]
+    public async Task<object> BulletinListBaseData()
+    {
+        var rsp = new
+        {
+            BulletinType = _systemDicDataCacheManager.SnapshotBulletinType,
+            BulletinSource = _systemDicDataCacheManager.SnapshotBulletinSource,
+            BulletinState = EnumExts.GetDescriptions<EBulletinState>()
+        };
+        return rsp;
+    }
+
+    /// <summary>
+    /// 新增页面基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("bulletin/addbasedata")]
+    public async Task<object> BulletinAddBaseData()
+    {
+        var rsp = new
+        {
+            BulletinType = _systemDicDataCacheManager.SnapshotBulletinType,
+            BulletinSource = _systemDicDataCacheManager.SnapshotBulletinSource,
+            OrgsOptions = await _organizeRepository.GetOrgJson(),
+        };
+        return rsp;
+    }
+    #endregion
+}

+ 301 - 15
src/Hotline.Api/Controllers/Snapshot/SnapshotController.cs

@@ -1,15 +1,36 @@
-using Hotline.Application.Snapshot;
+using AngleSharp.Dom;
+using Fw.Utility.UnifyResponse;
+using Hotline.Api.Filter;
+using Hotline.Application.Orders;
+using Hotline.Application.Snapshot;
+using Hotline.Caching.Interfaces;
+using Hotline.File;
 using Hotline.Orders;
+using Hotline.Repository.SqlSugar.Snapshot;
 using Hotline.Settings;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Article;
+using Hotline.Share.Dtos.Order;
 using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Dtos.WebPortal;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Enums.Snapshot;
+using Hotline.Share.Tools;
+using Hotline.Snapshot;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Tools;
 using Mapster;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
+using SharpCompress.Compressors.Xz;
+using SqlSugar;
+using SqlSugar.Extensions;
 using System.ComponentModel.DataAnnotations;
 using System.Reflection;
+using XF.Domain.Authentications;
+using XF.Domain.Exceptions;
+using XF.Domain.Filters;
 using XF.Domain.Repository;
 
 namespace Hotline.Api.Controllers.Snapshot;
@@ -22,12 +43,26 @@ public class SnapshotController : BaseController
     private readonly IRepository<Order> _orderRepository;
     private readonly ISnapshotApplication _snapshotApplication;
     private readonly ISystemAreaDomainService _systemAreaDomainService;
+    private readonly IIndustryRepository _industryRepository;
+    private readonly IOrderDomainService _orderDomainService;
+    private readonly IFileRepository _fileRepository;
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
+    private readonly ISystemDicDataCacheManager _systemDicDataCacheManager;
+    private readonly ISessionContext _sessionContext;
+    private readonly IThirdAccountRepository _thirdAccountRepository;
 
-    public SnapshotController(IRepository<Order> orderRepository, ISnapshotApplication snapshotApplication, ISystemAreaDomainService systemAreaDomainService)
+    public SnapshotController(IRepository<Order> orderRepository, ISnapshotApplication snapshotApplication, ISystemAreaDomainService systemAreaDomainService, IIndustryRepository industryRepository, IOrderDomainService orderDomainService, IFileRepository fileRepository, IOrderSnapshotRepository orderSnapshotRepository, ISystemDicDataCacheManager systemDicDataCacheManager, ISessionContext sessionContext, IThirdAccountRepository thirdAccountRepository)
     {
         _orderRepository = orderRepository;
         _snapshotApplication = snapshotApplication;
         _systemAreaDomainService = systemAreaDomainService;
+        _industryRepository = industryRepository;
+        _orderDomainService = orderDomainService;
+        _fileRepository = fileRepository;
+        _orderSnapshotRepository = orderSnapshotRepository;
+        _systemDicDataCacheManager = systemDicDataCacheManager;
+        _sessionContext = sessionContext;
+        _thirdAccountRepository = thirdAccountRepository;
     }
 
     /// <summary>
@@ -39,7 +74,6 @@ public class SnapshotController : BaseController
     public async Task<HomePageOutDto> GetHomePageAsync()
         => await _snapshotApplication.GetHomePageAsync();
 
-
     /// <summary>
     /// 行业界面基础信息
     /// </summary>
@@ -50,6 +84,82 @@ public class SnapshotController : BaseController
     public async Task<IndustryBaseOutDto> GetIndustryBaseAsync(string id)
         => await _snapshotApplication.GetIndustryBaseAsync(id, HttpContext.RequestAborted);
 
+    /// <summary>
+    /// 添加随手拍工单
+    /// </summary>
+    /// <returns></returns>
+    [HttpPost("order")]
+    [LogFilter("添加随手拍工单")]
+    public async Task<AddSnapshotOrderOutDto> AddOrderAsync([FromBody] AddSnapshotOrderInDto dto)
+    {
+        var ssp = _systemDicDataCacheManager.SourceChannel.FirstOrDefault(m => m.DicDataName == "随手拍")
+            ?? throw UserFriendlyException.SameMessage("请添加[随手拍]来源.");
+        var order = dto.Adapt<Order>();
+        dto.ValidateObject();
+        var industry = await _industryRepository.GetAsync(dto.IndustryId, HttpContext.RequestAborted)
+            ?? throw UserFriendlyException.SameMessage("行业不存在:" + dto.IndustryId);
+        order.AcceptTypeCode = industry.AcceptTypeCode;
+        order.AcceptType = industry.AcceptType;
+        order.FromGender = EGender.Unknown;
+        order.Title = dto.GetTitle(industry.IndustryType, industry.AcceptType);
+        order.Content = dto.GetContent(industry.IndustryType);
+        order.FromPhone = _sessionContext.Phone;
+        order.Contact = _sessionContext.Phone;
+        order.SourceChannel = ssp.DicDataName;
+        order.SourceChannelCode = ssp.DicDataValue;
+        order.InitId();
+        await _orderDomainService.AddAsync(order);
+        if (dto.Files.NotNullOrEmpty())
+        {
+            order.FileJson = await _fileRepository.AddFileAsync(dto.Files, order.Id, HttpContext.RequestAborted);
+            await _orderRepository.UpdateAsync(order);
+        }
+        var orderSnapshot = dto.Adapt<OrderSnapshot>();
+        orderSnapshot.Id = order.Id;
+        orderSnapshot.IndustryId = dto.IndustryId;
+        orderSnapshot.IndustryName = industry.Name;
+        orderSnapshot.CompanyName = dto.CompanyName;
+        if (dto.StartWorkTime.NotNullOrEmpty()) orderSnapshot.StartWorkTime = dto.StartWorkTime.ObjToDate();
+        if (dto.EndWorkTime.NotNullOrEmpty()) orderSnapshot.EndWorkTime = dto.EndWorkTime.ObjToDate();
+        if (dto.Name.NotNullOrEmpty())
+        {
+            await _thirdAccountRepository.Updateable()
+               .SetColumns(m => m.UserName, dto.Name)
+               .Where(m => m.Id == _sessionContext.UserId)
+               .ExecuteCommandAsync(HttpContext.RequestAborted);
+        }
+        await _orderSnapshotRepository.AddAsync(orderSnapshot);
+        return order.Adapt<AddSnapshotOrderOutDto>();
+    }
+
+    /// <summary>
+    /// 获取行业集合
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("industry")]
+    [AllowAnonymous]
+    public async Task<IList<IndustryOutDto>> GetIndustresAsync()
+        => await _snapshotApplication.GetIndustresAsync();
+
+    /// <summary>
+    /// 获取公开的工单集合
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("order/published")]
+    [AllowAnonymous]
+    public async Task<IList<OrderPublishOutDto>> GetPublishOrderAsync([FromQuery] OrderPublishInDto dto)
+        => await _snapshotApplication.GetOrderPublishAsync(dto, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 获取公开的工单详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("order/published/{id}")]
+    [AllowAnonymous]
+    public async Task<OrderPublishDetailOutDto> GetOrderPublishDetailAsync(string id)
+        => await _snapshotApplication.GetOrderPublishDetailAsync(id, HttpContext.RequestAborted);
+
     /// <summary>
     /// 获取小程序公告列表
     /// </summary>
@@ -57,8 +167,17 @@ public class SnapshotController : BaseController
     /// <returns></returns>
     [HttpGet("bulletions")]
     [AllowAnonymous]
-    public virtual async Task<IReadOnlyList<BulletinOutDto>> QueryBulletinsAsync([FromQuery] BulletinInDto dto)
-        => await _snapshotApplication.GetBulletinsAsync(dto);
+    public async Task<IReadOnlyList<BulletinOutDto>> QueryBulletinsAsync([FromQuery] BulletinInDto dto)
+        => await _snapshotApplication.GetBulletinsAsync(dto, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 获取小程序首页弹窗
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("bulletions/popup")]
+    [AllowAnonymous]
+    public async Task<BulletinOutDto> GetBulletionPopupAsync()
+        => await _snapshotApplication.GetBulletionPopupAsync(HttpContext.RequestAborted);
 
     /// <summary>
     /// 公告详情
@@ -67,7 +186,7 @@ public class SnapshotController : BaseController
     /// <returns></returns>
     [HttpGet("bulletions/{id}")]
     [AllowAnonymous]
-    public virtual async Task<BulletinOutDto> QueryBulletionsDetailAsync(string id)
+    public async Task<BulletinOutDto> QueryBulletionsDetailAsync(string id)
         => await _snapshotApplication.GetBulletinsDetailAsync(id);
 
     /// <summary>
@@ -76,7 +195,7 @@ public class SnapshotController : BaseController
     /// <returns></returns>
     [AllowAnonymous]
     [HttpGet("user")]
-    public virtual async Task<SnapshotUserInfoOutDto> GetUserInfo()
+    public async Task<SnapshotUserInfoOutDto> GetUserInfo()
         => await _snapshotApplication.GetSnapshotUserInfoAsync();
 
     /// <summary>
@@ -85,8 +204,8 @@ public class SnapshotController : BaseController
     /// <param name="dto"></param>
     /// <returns></returns>
     [HttpGet("order")]
-    public virtual async Task<PagedDto<OrderOutDto>> QueryOrderListAsync([FromQuery] OrderInDto dto)
-        => await _snapshotApplication.GetSnapshotOrdersAsync(dto);
+    public async Task<IList<OrderOutDto>> QueryOrderListAsync([FromQuery] OrderInDto dto)
+        => await _snapshotApplication.GetSnapshotOrdersAsync(dto, HttpContext.RequestAborted);
 
     /// <summary>
     /// 获取我提交的线索详情
@@ -94,8 +213,17 @@ public class SnapshotController : BaseController
     /// <param name="id"></param>
     /// <returns></returns>
     [HttpGet("order/{id}")]
-    public virtual async Task<OrderDetailOutDto> QueryOrderListAsync([FromQuery] string id)
-        => await _snapshotApplication.GetSnapshotOrderDetailAsync(id);
+    public async Task<OrderPublishDetailOutDto> QueryOrderListAsync(string id)
+        => await _snapshotApplication.GetSnapshotOrderDetailAsync(id, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 获取回访详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("order/visit/{id}")]
+    public async Task<IList<OrderVisitItemsOutDto>> GetOrderVisitDetailAsync(string id)
+        => await _snapshotApplication.GetOrderVisitDetailAsync(id);
 
     /// <summary>
     /// 统计红包金额, 每月的总金额
@@ -103,8 +231,16 @@ public class SnapshotController : BaseController
     /// <param name="dto"></param>
     /// <returns></returns>
     [HttpGet("redpack")]
-    public virtual async Task<IReadOnlyList<RedPackDateOutDto>> QueryRedPackDateAsync([FromQuery] RedPackDateInDto dto)
-        => await _snapshotApplication.GetRedPackDateAsync(dto);
+    public async Task<IReadOnlyList<RedPackDateOutDto>> QueryRedPackDateAsync([FromQuery] RedPackDateInDto dto)
+        => await _snapshotApplication.GetRedPackDateAsync(dto, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 用户领取红包总金额
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("redpack/received")]
+    public async Task<string> GetRedPackReceivedTotalAsync()
+        => await _snapshotApplication.GetRedPackReceivedTotalAsync(HttpContext.RequestAborted);
 
     /// <summary>
     /// 获取当月详细红包列表
@@ -112,8 +248,8 @@ public class SnapshotController : BaseController
     /// <param name="dto"></param>
     /// <returns></returns>
     [HttpGet("redpack/month")]
-    public virtual async Task<PagedDto<RedPackOutDto>> QueryRedPackDateAsync([FromQuery] RedPacksInDto dto)
-        => await _snapshotApplication.GetRedPacksAsync(dto);
+    public async Task<IList<RedPackOutDto>> QueryRedPackDateAsync([FromQuery] RedPacksInDto dto)
+        => await _snapshotApplication.GetRedPacksAsync(dto, HttpContext.RequestAborted);
 
     /// <summary>
     /// 获取随手拍电气焊动火作业待处理工单数量
@@ -130,6 +266,156 @@ public class SnapshotController : BaseController
     /// </summary>
     /// <returns></returns>
     [HttpGet("area/tree")]
+    [AllowAnonymous]
     public async Task<List<SystemAreaOutDto>> GetAreaTreeAsync()
         => (await _systemAreaDomainService.GetAreaTree()).Adapt<List<SystemAreaOutDto>>();
+
+    /// <summary>
+    /// 批量添加从业人员
+    /// </summary>
+    /// <param name="dtos"></param>
+    /// <returns></returns>
+    [HttpPost("practitioner")]
+    public async Task AddPractitionerAsync([FromBody] IList<AddBatchPractitionerInDto> dtos)
+        => await _snapshotApplication.AddPractitionerAsync(dtos);
+
+    /// <summary>
+    /// 获取从业人员详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("practitioner/{id}")]
+    [AllowAnonymous]
+    public async Task<PractitionerDetailOutDto> GetPractitionerDetailAsync(string id)
+        => await _snapshotApplication.GetPractitionerDetailAsync(id, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 获取从业人员集合(每次随机)
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("practitioner")]
+    [AllowAnonymous]
+    public async Task<IList<PractitionerItemOutDto>> GetPractitionerDetailAsync([FromQuery] PractitionerItemInDto dto)
+        => await _snapshotApplication.GetPractitionerItemsAsync(dto, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 志愿者上报
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("report")]
+    [LogFilter("志愿者上报")]
+    public async Task<AddVolunteerReportOutDto> AddVolunteerReportAsync([FromBody] AddVolunteerReportInDto dto)
+        => await _snapshotApplication.AddVolunteerReportAsync(dto, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 志愿者上报页面基础数据
+    /// </summary>
+    /// <param name="id">行业Id</param>
+    /// <returns></returns>
+    [HttpGet("report/base")]
+    public Dictionary<string, dynamic> GetReportBaseAsync()
+    {
+        return new Dictionary<string, dynamic>
+        {
+            { "jobType", _systemDicDataCacheManager.JobType },
+        };
+    }
+
+    /// <summary>
+    /// 用户自己保存邀请码
+    /// </summary>
+    /// <returns></returns>
+    [HttpPut("third/invitationcode")]
+    public async Task SaveInvitationCodeAsync([FromBody] SaveInvitationCodeInDto dto)
+        => await _snapshotApplication.SaveInvitationCodeAsync(dto);
+
+    /// <summary>
+    /// 网格员系统补推接口;
+    /// 根据工单No推送到网格员系统, 多个使用,号分隔
+    /// </summary>
+    /// <param name="nos">工单No集合,多个使用 ',' 号分隔</param>
+    /// <returns></returns>
+    [HttpPut("guider/nos")]
+    public async Task PostOrderGuiderSystemAsync([FromBody] string nos)
+    {
+        foreach (var no in nos.Split(','))
+        {
+            var orderId = await _orderRepository.Queryable()
+                .Where(m => m.No == no)
+                .Select(m => m.Id)
+                .FirstAsync() ?? throw UserFriendlyException.SameMessage("工单不存在");
+            await _snapshotApplication.PostOrderGuiderSystemAsync(orderId, HttpContext.RequestAborted);
+        }
+    }
+
+    #region 第三方系统
+    /// <summary>
+    /// 接收网格员系统回调
+    /// </summary>
+    /// <returns></returns>
+    [HttpPost("guider/reply")]
+    [LogFilter("网格员系统回调")]
+    [AllowAnonymous]
+    public async Task SaveGuiderSystemReplayAsync([FromBody] GuiderSystemInDto dto)
+    {
+        await _snapshotApplication.SaveGuiderSystemReplyAsync(dto, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 电气焊申报系统申报工单查询
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("order/declare")]
+    [AllowAnonymous]
+    public async Task<OpenResponse> GetOrderDeclareItemsAsync([FromBody] OrderDeclareItemsInDto dto)
+    {
+        var disposeDate = dto.JsonData.AcceptContent.DisposeDate.ObjToDate();
+        var industryId = await _industryRepository.Queryable()
+            .Where(m => m.IndustryType == EIndustryType.Declare)
+            .Select(m => m.Id)
+            .ToListAsync();
+        var result = await _orderSnapshotRepository.Queryable()
+            .LeftJoin<Order>((snapshot, order) => order.Id == snapshot.Id)
+            .Where((snapshot, order) => industryId.Contains(snapshot.IndustryId))
+            .WhereIF(dto.JsonData.AcceptContent.Tyep == 2, (snapshot, order) => order.CreationTime.Date == disposeDate.Date)
+            .WhereIF(dto.JsonData.AcceptContent.Tyep == 1, (snapshot, order) => order.FiledTime != null && order.FiledTime.Value.Date == disposeDate.Date)
+            .Select((snapshot, order) => new OrderDeclareItemsOutDto
+            { 
+                AreaCode = order.AreaCode,
+                AreaName = order.County,
+                EventType = 0,
+                WorkAddress = order.Address,
+                CreatedTime = order.CreationTime,
+                WorkType = snapshot.JobType.ObjToInt(),
+                Source = 1,
+                WorkTimeStart = snapshot.StartWorkTime.Value,
+                WorkTimeStop = snapshot.EndWorkTime.Value,
+                OnsiteSituateDescription = order.Content,
+                Name = order.FromName,
+                Phone = order.FromPhone,
+                EventId = order.No,
+                PlaceType = snapshot.Workplace,
+                Compliance = 1,
+                //CheckTime = snapshot.CheckTime,
+                //CheckUser = snapshot.CheckUser,
+                //CheckDept = snapshot.CheckDept,
+                //CheckPhone = snapshot.CheckPhone,
+            })
+            .ToListAsync();
+
+
+        return OpenResponse.Ok(new List<WebPortalDeResponse<List<OrderDeclareItemsOutDto>>>()
+        {
+            new() { code = "1", msg = "ok", data =
+                [
+                    new OrderDeclareItemsOutDto()
+                ]
+            }
+        });
+    }
+
+    #endregion
 }

+ 259 - 0
src/Hotline.Api/Controllers/Snapshot/SnapshotOrderController.cs

@@ -0,0 +1,259 @@
+using Amazon.Runtime.Internal.Transform;
+using Hotline.Application.FlowEngine;
+using Hotline.Application.Orders;
+using Hotline.Application.Snapshot;
+using Hotline.Caching.Interfaces;
+using Hotline.Caching.Services;
+using Hotline.FlowEngine.Definitions;
+using Hotline.FlowEngine.Workflows;
+using Hotline.Orders;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Repository.SqlSugar.Snapshot;
+using Hotline.Settings;
+using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Enums.FlowEngine;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Enums.Snapshot;
+using Hotline.Share.Tools;
+using Hotline.Snapshot;
+using Hotline.Snapshot.Interfaces;
+using Mapster;
+using Microsoft.AspNetCore.Mvc;
+using System.ComponentModel;
+using System.Text;
+using XF.Domain.Authentications;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+using XF.Utility.EnumExtensions;
+
+namespace Hotline.Api.Controllers.Snapshot;
+
+/// <summary>
+/// 随手拍工单
+/// </summary>
+[Description("随手拍工单")]
+public class SnapshotOrderController : BaseController
+{
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
+    private readonly IOrderSnapshotApplication _orderSnapshotApplication;
+    private readonly ISystemDicDataCacheManager _systemDicDataCacheManager;
+    private readonly ISystemAreaDomainService _systemAreaDomainService;
+    private readonly IIndustryRepository _industryRepository;
+    private readonly ISessionContext _sessionContext;
+    private readonly IWorkflowDomainService _workflowDomainService;
+    private readonly IOrderRepository _orderRepository;
+    private readonly IWorkflowApplication _workflowApplication;
+    private readonly IRepository<WorkflowDefinition> _workflowDefinitionRepository;
+    private readonly IOrderApplication _orderApplication;
+
+    public SnapshotOrderController(IOrderSnapshotRepository orderSnapshotRepository, IOrderSnapshotApplication orderSnapshotApplication, ISystemAreaDomainService systemAreaDomainService, ISystemDicDataCacheManager systemDicDataCacheManager, IIndustryRepository industryRepository, ISessionContext sessionContext, IWorkflowDomainService workflowDomainService, IOrderRepository orderRepository, IWorkflowApplication workflowApplication, IRepository<WorkflowDefinition> workflowDefinitionRepository, IOrderApplication orderApplication)
+    {
+        _orderSnapshotRepository = orderSnapshotRepository;
+        _orderSnapshotApplication = orderSnapshotApplication;
+        _systemAreaDomainService = systemAreaDomainService;
+        _systemDicDataCacheManager = systemDicDataCacheManager;
+        _industryRepository = industryRepository;
+        _sessionContext = sessionContext;
+        _workflowDomainService = workflowDomainService;
+        _orderRepository = orderRepository;
+        _workflowApplication = workflowApplication;
+        _workflowDefinitionRepository = workflowDefinitionRepository;
+        _orderApplication = orderApplication;
+    }
+
+    /// <summary>
+    /// 随手拍所有工单集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("order")]
+    public async Task<PagedDto<OrderSnapshotItemsOutDto>> GetOrderSnapshotItems([FromQuery] OrderSnapshotItemsInDto dto)
+        => (await _orderSnapshotApplication.GetOrderSnapshotItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 随手拍所有工单页面基础信息
+    /// </summary>
+    [HttpGet("order/basedata")]
+    public async Task<Dictionary<string, object>> GetOrderSnapshotItemsDatabaseAsync()
+    {
+        IList<Kv> steps = new List<Kv>();
+        await _workflowDefinitionRepository.Queryable()
+            .Where(m => m.Status == EDefinitionStatus.Enable && m.Code == "gdbl")
+            .OrderByDescending(m => m.Version)
+            .Select(m => new { m.Id, m.Steps})
+            .FirstAsync().Then(async workflowSteps =>
+            {
+                steps = workflowSteps.Steps.Adapt<IList<NameCodeDto>>()
+               .ToList().Select(m => new Kv 
+               {
+                   Key = m.Code,
+                   Value = m.Name,
+               }).ToList();
+            });
+        return new Dictionary<string, object>
+        {
+            { "orderStatus", EnumExts.GetDescriptions<EOrderStatus>()},
+            { "area", await _systemAreaDomainService.GetAreaKeyValue(parentId: "510300")},
+            { "steps", steps},
+            { "orderTags", _systemDicDataCacheManager.OrderTag},
+            { "industry", await _industryRepository.Queryable().Select(d => new { d.Id, d.Name, }).ToListAsync()},
+            { "acceptType", _systemDicDataCacheManager.AcceptType.Adapt<List<Kv>>() }
+        };
+    }
+
+    /// <summary>
+    /// 获取网格员回复集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("guider/reply")]
+    public async Task<PagedDto<GuiderReplyItemsOutDto>> GetGuiderReplyItems([FromQuery] GuiderReplyItemsInDto dto)
+        => (await _orderSnapshotApplication.GetGuiderReplyItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 获取工单标注集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("label")]
+    public async Task<PagedDto<SignOrderSnapshotItemsOutDto>> GetSignOrderSnapshotItems([FromQuery] SignOrderSnapshotItemsInDto dto)
+        => (await _orderSnapshotApplication.GetSignOrderSnapshotItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 获取工单已经标签集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("labeled")]
+    public async Task<PagedDto<LabeledOrderSnapshotItemsOutDto>> GetLabeledOrderSnapshotItems([FromQuery] LabeledOrderSnapshotItemsInDto dto)
+        => (await _orderSnapshotApplication.GetLabeledOrderSnapshotItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 标签详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("label/{id}")]
+    public async Task<LabelOrderSnapshotDetailOutDto> GetLabelOrderSnapshotDetailAsync(string id)
+    { 
+        var order = await _orderRepository.Queryable()
+            .LeftJoin<OrderSnapshot>((order, snapshot) => order.Id == snapshot.Id)
+            .Where((order, snapshot) => order.Id == id)
+            .Select((order, snapshot) => new LabelOrderSnapshotDetailOutDto { Id = order.Id, Title = order.Title, Lables =snapshot.Labels })
+            .FirstAsync();
+        var snapshot = await _orderSnapshotRepository.GetAsync(id, HttpContext.RequestAborted);
+        order.Lables = snapshot.Labels;
+        order.LabelsBaseData = _systemDicDataCacheManager.SnapshotOrderLabel;
+        return order;
+    }
+
+    /// <summary>
+    /// 修改工单标签
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut("label")]
+    public async Task UpdateLabelOrderSnapshotAsync([FromBody] UpdateLabelOrderSnapshotInDto dto)
+        => await _orderSnapshotApplication.UpdateLabelAsync(dto.Id, dto.SnapshotLabels);
+
+    /// <summary>
+    /// 获取工单标签日志集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("label/log")]
+    public async Task<PagedDto<LabelOrderSnapshotLogItemsOutDto>> GetLabelOrderSnapshotLogItems([FromQuery] LabelOrderSnapshotLogItemsInDto dto)
+        => (await _orderSnapshotApplication.GetLabelOrderSnapshotLogItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 随手拍公开集合
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("publish")]
+    public async Task<PagedDto<GetOrderSnapshotPublishItemsOutDto>> GetOrderSnapshotPublishItems([FromQuery] GetOrderSnapshotPublishItemsInDto dto)
+        => (await _orderSnapshotApplication.GetOrderSnapshotPublishItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 随手拍公开详情
+    /// </summary>
+    /// <param name="id">工单Id</param>
+    /// <returns></returns>
+    [HttpGet("publish/{id}")]
+    public async Task<GetOrderSnapshotPublishDetailOutDto> GetOrderSnapshotPublishDetailAsync(string id)
+        => await _orderSnapshotApplication.GetOrderSnapshotPublishDetailAsync(id);
+
+    /// <summary>
+    /// 添加公开审核
+    /// </summary>
+    /// <returns></returns>
+    [HttpPost("publish")]
+    public async Task AddOrderSnapshotPublishItemsAsync([FromBody] AddSnapshotOrderPublishInDto dto)
+         => await _orderSnapshotApplication.AddOrderPublishAsync(dto, HttpContext.RequestAborted);
+
+    /// <summary>
+    /// 随手拍公开审批集合
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("publish/audit")]
+    public async Task<PagedDto<GetOrderSnapshotPublishAuditItemsOutDto>> GetOrderSnapshotPublishAuditItems([FromQuery] GetOrderSnapshotPublishAuditItemsInDto dto)
+        => (await _orderSnapshotApplication.GetOrderSnapshotPublishAuditItems(dto).ToPagedListAsync(dto)).ToPaged();
+
+    /// <summary>
+    /// 审核随手拍公开申请通过/不通过
+    /// </summary>
+    /// <returns></returns>
+    [HttpPut("publish/status")]
+    public async Task UpdateOrderSnapshotPublishStatusAsync([FromBody] UpdateOrderSnapshotPublishStatusInDto dto)
+        => await _orderSnapshotApplication.UpdateOrderSnapshotPublishStatusAsync(dto);
+
+    /// <summary>
+    /// 随手拍公开审批详情
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("publish/audit/{id}")]
+    public async Task<GetOrderSnapshotPublishAuditDetailOutDto> GetOrderSnapshotPublishAuditDetailAsync(string id)
+        => await _orderSnapshotApplication.GetOrderSnapshotPublishAuditDetailAsync(id);
+
+    /// <summary>
+    /// 批量设置随手拍公开申请不通过
+    /// </summary>
+    /// <returns></returns>
+    [HttpPut("publishs/status/refuse")]
+    public async Task UpdateOrderSnapshotPublishsStatusRefuseAsync(IList<string> ids)
+        => await _orderSnapshotApplication.UpdateOrderSnapshotPublishsStatusRefuseAsync(ids);
+
+    /// <summary>
+    /// 随手拍公开集合基础数据
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("publish/basedata")]
+    public async Task<Dictionary<string, object>> GetOrderSnapshotPublishItemsBasedataAsync()
+    {
+        var industry = await _industryRepository.Queryable()
+       .Select(d => new { d.Id, d.Name, })
+       .ToListAsync();
+
+        return new Dictionary<string, object>
+        {
+            { "publishStatus", EnumExts.GetDescriptions<EOrderSnapshotPublishStatus>()},
+            { "acceptCode", _systemDicDataCacheManager.AcceptType.Adapt<List<Kv>>() },
+            { "orderStatus", EnumExts.GetDescriptions<EOrderStatus>()},
+            { "area", await _systemAreaDomainService.GetAreaKeyValue(parentId: "510300")},
+            { "industry", industry}
+        };
+    }
+
+    /// <summary>
+    /// 网格员超时未回复, 处理工单
+    /// </summary>
+    /// <param name="id">工单Id</param>
+    /// <returns></returns>
+    [HttpPut("guider/timeout/{id}")]
+    public async Task HandleFromWanggeyuanToMaskAsync(string id)
+    {
+        await _orderApplication.HandleFromWanggeyuanToMaskAsync(id, HttpContext.RequestAborted);
+    }
+}

+ 4 - 0
src/Hotline.Api/Controllers/SysController.cs

@@ -119,6 +119,7 @@ namespace Hotline.Api.Controllers
         /// 获取菜单
         /// </summary>
         /// <returns></returns>
+        [Permission(EPermission.GetMenuJson)]
         [AllowAnonymous]
         [HttpGet("getmenujson")]
         public async Task<IReadOnlyList<SystemMenu>> GetMenuJson()
@@ -132,6 +133,7 @@ namespace Hotline.Api.Controllers
         /// </summary>
         /// <param name="dto"></param>
         /// <returns></returns>
+        //[Permission(EPermission.AddMenu)]
         [HttpPost("add-menu")]
         public async Task AddMenu([FromBody] AddMenuDto dto)
         {
@@ -144,6 +146,7 @@ namespace Hotline.Api.Controllers
         /// </summary>
         /// <param name="dto"></param>
         /// <returns></returns>
+        //[Permission(EPermission.UpdateMenu)]
         [HttpPost("update-menu")]
         public async Task UpdateMenu([FromBody] UpdateMenuDto dto)
         {
@@ -160,6 +163,7 @@ namespace Hotline.Api.Controllers
         /// </summary>
         /// <param name="id"></param>
         /// <returns></returns>
+        //[Permission(EPermission.RemoveMenu)]
         [HttpDelete("removemenu/{id}")]
         public async Task RemoveMenu(string id)
         {

+ 165 - 7
src/Hotline.Api/Controllers/TestController.cs

@@ -39,6 +39,7 @@ using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.FlowEngine.Workflow;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Dtos.Realtime;
+using Hotline.Share.Dtos.Snapshot;
 using Hotline.Share.Dtos.TrCallCenter;
 using Hotline.Share.Enums.CallCenter;
 using Hotline.Share.Enums.FlowEngine;
@@ -71,6 +72,13 @@ using XF.Domain.Repository;
 using static System.Runtime.InteropServices.JavaScript.JSType;
 using Order = Hotline.Orders.Order;
 using Hotline.Share.Dtos.Settings;
+using OrderDto = Hotline.Share.Dtos.Order.OrderDto;
+using Hotline.Share.Dtos.Home;
+using Google.Protobuf.WellKnownTypes;
+using Microsoft.AspNetCore.DataProtection;
+using Hotline.Share.Tools;
+using NETCore.Encrypt;
+using Hotline.Ai.Quality;
 
 namespace Hotline.Api.Controllers;
 
@@ -139,9 +147,13 @@ public class TestController : BaseController
     private readonly ICalcExpireTime _expireTime;
     private readonly ICallNativeRepository _callNativeRepository;
     private readonly IRepository<OldSendProData> _oldSendProDataRepository;
+	private readonly IOrderScreenRepository _orderScreenRepository;
+	private readonly IRepository<OrderVisit> _orderVisitRepository;
+    private readonly IThirdIdentiyService _thirdIdentiyService;
+	private readonly IServiceProvider _serviceProvider;
 
 
-    public TestController(
+	public TestController(
         //INewRockClient client,
         ILogger<TestController> logger,
         //IAuthorizeGenerator authorizeGenerator,
@@ -193,7 +205,12 @@ ICallApplication callApplication,
         ICalcExpireTime expireTime
 ,
         ICallNativeRepository callNativeRepository,
-        IRepository<OldSendProData> oldSendProDataRepository)
+        IRepository<OldSendProData> oldSendProDataRepository,
+        IThirdIdentiyService thirdIdentiyService,
+		IOrderScreenRepository orderScreenRepository,
+		IRepository<OrderVisit> orderVisitRepository,
+		IServiceProvider serviceProvider
+		)
     {
         _logger = logger;
         //_authorizeGenerator = authorizeGenerator;
@@ -243,6 +260,26 @@ ICallApplication callApplication,
         _expireTime = expireTime;
         _callNativeRepository = callNativeRepository;
         _oldSendProDataRepository = oldSendProDataRepository;
+        _thirdIdentiyService = thirdIdentiyService;
+        _orderScreenRepository = orderScreenRepository;
+        _orderVisitRepository = orderVisitRepository;
+        _serviceProvider = serviceProvider;
+	}
+
+    /// <summary>
+    /// 测试获取电话号码
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("phonenumber_test")]
+    [AllowAnonymous]
+    public async Task<ThirdPhoneOutDto> GetPhoneNumberTest()
+    {
+        var inDto = new ThirdTokenDto
+        {
+            AppId = _systemSettingCacheManager.WxOpenAppId,
+            Secret = _systemSettingCacheManager.WxOpenAppSecret
+        };
+        return await _thirdIdentiyService.GetPhoneNumberAsync(inDto);
     }
 
     /// <summary>
@@ -519,10 +556,12 @@ ICallApplication callApplication,
         //var r = _timeLimitDomainService.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToCenter, batchId);
         //var r = _timeLimitDomainService.CalcEndTime(DateTime.Parse("2024-09-12 14:45:47"), Share.Enums.Settings.ETimeType.WorkDay, 2, 80, 50);
         //_capPublisher.PublishDelay((DateTime.Now.AddMinutes(2) - DateTime.Now), EventNames.OrderRelateCall, "123");
-        var times =await _expireTime.CalcExpiredTime(DateTime.Parse("2024-12-31 13:25:13.137977"), DateTime.Parse("2024-12-31 13:25:13.137977"), EFlowDirection.CenterToOrg,new OrderTimeClacInfo() { AcceptTypeCode= "10" });
+        //var times =await _expireTime.CalcExpiredTime(DateTime.Parse("2024-12-31 13:25:13.137977"), DateTime.Parse("2024-12-31 13:25:13.137977"), EFlowDirection.CenterToOrg,new OrderTimeClacInfo() { AcceptTypeCode= "10" });
+        //await _expireTime.CalcWorkTimeToDecimal(visit.VisitTime.Value, DateTime.Now, false);
         //var times = await _expireTime.CalcWorkTimeToDecimal(DateTime.Parse("2024-12-16 21:36:27"), DateTime.Parse("2024-12-17 12:47:05"), false);
+        var query = _userRepository.Queryable().Where(x => false);
         //await _capPublisher.PublishDelay(EventNames.OrderRelateCall, "123", cancellationToken: HttpContext.RequestAborted);
-        return OpenResponse.Ok(times);
+        return OpenResponse.Ok(query.ToSqlString());
     }
 
 
@@ -987,12 +1026,36 @@ ICallApplication callApplication,
     }
 
     [HttpGet("t4")]
-    public async Task<string> Test4()
+	[AllowAnonymous]
+	public async Task<string> Test4()
     {
-        return DateTime.Now.ToString("O");
+        var a = await _expireTime.CalcWorkTimeToDecimal(DateTime.Parse("2024-12-17 22:33:22"), DateTime.Parse("2024-12-18 10:29:53"), false);
+		return DateTime.Now.ToString("O");
     }
 
-    [AllowAnonymous]
+
+    /// <summary>
+    /// 批量处理甄别耗时问题
+    /// </summary>
+    /// <returns></returns>
+	[HttpGet("order_screen_timeconsuming")]
+	[AllowAnonymous]
+	public async Task OrderScreenTimeConsuming()
+	{
+        var screens = await _orderScreenRepository.Queryable().ToListAsync();
+        foreach (var item in screens)
+        {
+            var visit = await _orderVisitRepository.Queryable().Where(x=>x.Id == item.VisitId).FirstAsync();
+            if (visit != null)
+            {
+				var time = await _expireTime.CalcWorkTimeToDecimal(visit.VisitTime.Value, item.CreationTime, false);
+                await _orderScreenRepository.Updateable().SetColumns(x => new OrderScreen { TimeConsuming = time }).Where(x => x.Id == item.Id).ExecuteCommandAsync();
+			}
+        }
+		
+	}
+
+	[AllowAnonymous]
     [HttpGet("t5")]
     public async Task<string> GetUserAllowAnonymous()
     {
@@ -1321,4 +1384,99 @@ ICallApplication callApplication,
         }
     }
 
+	/// <summary>
+	/// 老系统数据同步
+	/// </summary>
+	/// <returns></returns>
+	[HttpGet("old_data_synchronization")]
+	[AllowAnonymous]
+	public async Task oldDataSynchronization([FromQuery] string? no)
+	{
+		var orders = new List<Order>();
+		if (string.IsNullOrEmpty(no))
+		{
+			orders = await _orderRepository.Queryable().Where(x => x.No.Length == 12 && x.ActualHandleTime == null && x.CenterToOrgTime == null)
+                .Where(x=>x.CreationTime >= DateTime.Parse("2024-01-01") && x.CreationTime <  DateTime.Parse("2024-02-01")).ToListAsync();
+		}
+		else
+		{
+			orders = await _orderRepository.Queryable().Where(x => x.No == no).ToListAsync();
+		}
+		foreach (var order in orders)
+		{
+
+			var steps = await _workflowStepRepository.Queryable().Where(x => x.ExternalId == order.Id).ToListAsync();
+
+			var actualHandleStep = steps.Where(x => x.HandlerOrgId == order.ActualHandleOrgCode).OrderByDescending(x => x.CreationTime).FirstOrDefault();
+			var CenterToOrgStep = steps.Where(x => x.AssignerOrgId == "001" && x.HandlerOrgId != "001").OrderByDescending(x => x.CreationTime).FirstOrDefault();
+
+			order.ActualHandleTime = actualHandleStep.HandleTime;
+			order.CenterToOrgTime = CenterToOrgStep is null? null : CenterToOrgStep.CreationTime;
+
+			await _orderRepository.Updateable().SetColumns(x => new Order { ActualHandleTime = order.ActualHandleTime, CenterToOrgTime = order.CenterToOrgTime }).Where(x => x.Id == order.Id).ExecuteCommandAsync();
+
+
+			var now = order.FiledTime.Value;
+			var handleDuration = order.StartTime.HasValue
+				? //_timeLimitDomainService.CalcWorkTime(
+				await _expireTime.CalcWorkTime(
+					order.StartTime.Value,
+					now, order.ProcessType is EProcessType.Zhiban)
+				: 0;
+			var fileDuration = order.CenterToOrgTime.HasValue
+				? // _timeLimitDomainService.CalcWorkTime(
+				await _expireTime.CalcWorkTime(
+					order.CenterToOrgTime.Value,
+					now, order.ProcessType is EProcessType.Zhiban)
+				: 0;
+			var allDuration = order.StartTime.HasValue
+				?
+				//_timeLimitDomainService.CalcWorkTime(
+				await _expireTime.CalcWorkTime(
+					order.StartTime.Value, now,
+					order.ProcessType is EProcessType.Zhiban)
+				: 0;
+			var creationTimeHandleDurationWorkday = order.ActualHandleTime.HasValue
+				?
+				// _timeLimitDomainService.CalcWorkTime(
+				await _expireTime.CalcWorkTime(
+					order.CreationTime, now,
+					order.ProcessType is EProcessType.Zhiban)
+				: 0;
+			var centerToOrgHandleDurationWorkday = order.ActualHandleTime.HasValue && order.CenterToOrgTime.HasValue
+				?
+				// _timeLimitDomainService.CalcWorkTime(
+				await _expireTime.CalcWorkTime(
+					order.CenterToOrgTime.Value, now,
+					order.ProcessType is EProcessType.Zhiban)
+				: 0;
+			creationTimeHandleDurationWorkday = creationTimeHandleDurationWorkday <= 0 ? 10 : creationTimeHandleDurationWorkday;
+			centerToOrgHandleDurationWorkday = centerToOrgHandleDurationWorkday <= 0 ? 10 : centerToOrgHandleDurationWorkday;
+
+			order.File(now, handleDuration, fileDuration, allDuration, creationTimeHandleDurationWorkday, centerToOrgHandleDurationWorkday);
+
+		}
+
+	}
+
+	[HttpGet("aiXingTang")]
+	[AllowAnonymous]
+	public async Task aiXingTang() {
+		var aiQualityService = _serviceProvider.GetRequiredService<IAiQualityService>();
+        await aiQualityService.CreateAiOrderQualityTask("cs202501030002.mp3", HttpContext.RequestAborted);
+
+	}
+
+    /// <summary>
+    /// 加密验证
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("check_token")]
+    [AllowAnonymous]
+    public async Task<string> CheckAES([FromBody] CheckTokenDto dto)
+    {
+        var strString = dto.AppId + dto.Timestamp;
+        return dto.AppId + EncryptProvider.AESEncrypt(strString, dto.Secret, dto.AppId);
+    }
 }

+ 28 - 25
src/Hotline.Api/Controllers/WebPortalController.cs

@@ -58,6 +58,7 @@ namespace Hotline.Api.Controllers
         private readonly ISystemDicDataCacheManager _systemDicDataCacheManager;
         private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
         private readonly ITypedCache<string> _getVailData;
+        private readonly IRepository<KnowledgeWord> _knowledgeWordRepository;
 
         public WebPortalController(IMapper mapper,
             IMediator mediator,
@@ -81,7 +82,8 @@ namespace Hotline.Api.Controllers
             IRepository<Knowledge> knowledgeRepository,
             ISystemDicDataCacheManager systemDicDataCacheManager,
             IOptionsSnapshot<AppConfiguration> appOptions,
-            ITypedCache<string> getVailData
+            ITypedCache<string> getVailData,
+            IRepository<KnowledgeWord> knowledgeWordRepository
             )
         {
             _mapper = mapper;
@@ -107,6 +109,7 @@ namespace Hotline.Api.Controllers
             _systemDicDataCacheManager = systemDicDataCacheManager;
             _appOptions = appOptions;
             _getVailData = getVailData;
+            _knowledgeWordRepository = knowledgeWordRepository;
         }
 
         #region 通知
@@ -761,7 +764,7 @@ namespace Hotline.Api.Controllers
                            ConTypeName = p.HotspotName,
                            FlowAddDate = p.AcceptTime,
                            RSFlagName = p.State == "1" ? "办理完成" : "办理中",
-                           PubDate = p.PubDate
+                           PubDate = p.CreationTime
                        });
 
             var items = await _orderRepository.UnionAll(queryNew, queryold)
@@ -780,9 +783,6 @@ namespace Hotline.Api.Controllers
             return OpenResponse.Ok(WebPortalDeResponse<OrderListReturnDto>.Success(returnDto, "成功"));
         }
 
-        /// <summary>
-
-
         /// <summary>
         /// 查询工单发布后公开的数据
         /// </summary>
@@ -1038,26 +1038,29 @@ namespace Hotline.Api.Controllers
 
             var data = _mapper.Map<Hotline.Share.Dtos.Order.AddOrderDto>(dto);
             data.Source = ESource.WebPortal;
-            if (string.IsNullOrEmpty(data.SourceChannelCode))
-            {
-                data.SourceChannel = "因特网";
-                data.SourceChannelCode = "YTW";
-            }
+            data.SourceChannel = "因特网";
+            data.SourceChannelCode = "YTW";
 
-            if (dto.FromID == "2")
+            switch (dto.FromID)
             {
-                data.SourceChannel = "APP";
-                data.SourceChannelCode = "AP";
-                data.Source = ESource.APP;
+                case "2":
+                    data.SourceChannel = "APP";
+                    data.SourceChannelCode = "AP";
+                    data.Source = ESource.APP;
+                    break;
+                case "3":
+                    data.SourceChannel = "微信";
+                    data.SourceChannelCode = "WX";
+                    data.Source = ESource.WeChat;
+                    break;
+                case "9"://宜宾人社专用
+                    data.SourceChannel = "人社APP";
+                    data.SourceChannelCode = "RSAPP";
+                    data.Source = ESource.YBHumanSocietyAPP;
+                    break;
+                default:
+                    break;
             }
-
-            if (dto.FromID == "3")
-            {
-                data.SourceChannel = "微信";
-                data.SourceChannelCode = "WX";
-                data.Source = ESource.WeChat;
-            }
-
             if (!string.IsNullOrEmpty(data.LicenceNo))
             {
                 data.LicenceTypeCode = "10";
@@ -1360,14 +1363,14 @@ namespace Hotline.Api.Controllers
             var typeSpliceNameTags = string.Empty;
             if (!string.IsNullOrEmpty(dto.KnowledgeBaseTags))
             {
-                var type = await _knowledgeTypeRepository.GetAsync(x => x.Name == dto.KnowledgeBaseTags);
-                typeSpliceNameTags = type?.SpliceName;
+                var type = await _knowledgeWordRepository.GetAsync(x => x.Tag == dto.KnowledgeBaseTags && x.IsEnable == 0);
+                typeSpliceNameTags = type?.Id;
             }
 
             var (total, items) = await _knowledgeRepository.Queryable()
                 .Where(p => p.IsPublic == true && p.Status == EKnowledgeStatus.OnShelf)
                 .WhereIF(!string.IsNullOrEmpty(dto.Title), p => p.Title.Contains(dto.Title))
-                .WhereIF(!string.IsNullOrEmpty(typeSpliceNameTags), p => p.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceNameTags)))
+                .WhereIF(!string.IsNullOrEmpty(typeSpliceNameTags), p => SqlFunc.JsonArrayAny(p.Keywords, typeSpliceNameTags) == true)
                 .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.KnowledgeType.Any(t => t.KnowledgeTypeSpliceName.EndsWith(typeSpliceName)))
                 .OrderByDescending(p => p.CreationTime)
                 .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);

+ 261 - 0
src/Hotline.Api/Controllers/XthxController.cs

@@ -0,0 +1,261 @@
+using Hotline.Application.Xthx;
+using Hotline.Caching.Interfaces;
+using Hotline.CallCenter.Tels;
+using Hotline.Settings;
+using Hotline.Share.Dtos.CallCenter;
+using Hotline.Share.Enums.CallCenter;
+using MapsterMapper;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using SqlSugar;
+using System.Text.Json;
+using XF.Domain.Authentications;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+
+namespace Hotline.Api.Controllers
+{
+    /// <summary>
+    /// 兴唐恒信
+    /// </summary>
+    public class XthxController : BaseController
+    {
+
+        #region 注入
+
+        private readonly IMapper _mapper;
+        private readonly ISessionContext _sessionContext;
+        private readonly IXthxApplication _xthxApplication;
+        private readonly ISystemSettingCacheManager _systemSettingCacheManager;                //系统参数
+        private readonly IRepository<TelOperationXthx> _telOperationXthxRepository;            //坐席动作类型
+        private readonly IRepository<TelOperationXthxLog> _telOperationXthxLogRepository;      //坐席动作类型
+
+        public XthxController(
+           IMapper mapper,
+           ISessionContext sessionContext,
+           IXthxApplication xthxApplication,
+           ISystemSettingCacheManager systemSettingCacheManager,
+           IRepository<TelOperationXthx> telOperationXthxRepository,
+            IRepository<TelOperationXthxLog> telOperationXthxLogRepository)
+        {
+            _mapper = mapper;
+            _sessionContext = sessionContext;
+            _xthxApplication = xthxApplication;
+            _systemSettingCacheManager = systemSettingCacheManager;
+            _telOperationXthxRepository = telOperationXthxRepository;
+            _telOperationXthxLogRepository = telOperationXthxLogRepository;
+        }
+
+        #endregion
+
+        #region 分机状态变更接收
+
+        /// <summary>
+        /// 分机状态变更接收
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("status/add")]
+        [AllowAnonymous]
+        public async Task<string> AddOperationAsync([FromBody] TelOperationXthxDto dto)
+        {
+            #region 描述
+
+            /*
+                1、根据分机号(telNo)和员工号(staffNo)确定唯一关系。
+
+                2、设计话机动作状态【签入、示忙、小休】
+
+                3、当【签入】时记录签入状态、开始时间,当【签出】时找到未结束的签入记录更新结束时间。
+                   先结束之前的【签入】,再重新【签入】
+
+                4、当【示忙】时记录示忙状态、开始时间,当传入非【示忙】状态,并且有未结束的【示忙】状态时更新结束时间。
+                   先结束之前的【示忙】,再重新【示忙】
+
+                5、当【小休】时记录小休状态、开始时间,当传入非【小休】状态,并且有未结束的【小休】状态时更新结束时间。
+                   先结束之前的【小休】,再重新【小休】
+
+                6、当【空闲】时,需要检查示忙、小休是否结束,未结束需要结束
+
+                7、当【签出】时,需要检查示忙、小休是否结束,未结束需要结束
+
+                8、除了【签入签出】状态,其他状态都要验证当前是否有【签入】,没有则新增签入
+             */
+
+            #endregion
+
+            #region 鉴权
+
+            var settingBase = _systemSettingCacheManager.GetSetting(SettingConstants.XthxVerificationStatusAdd)?.SettingValue[0];
+            if (settingBase != null)
+            {
+                TelAppXthxDto telAppDto = JsonSerializer.Deserialize<TelAppXthxDto>(settingBase);
+                if (dto.appId != telAppDto.appId || dto.appSecret != telAppDto.appSecret)
+                    throw UserFriendlyException.SameMessage("appId或者appSecret错误");
+            }
+
+            #endregion
+
+            // 查询日期间隔天数
+            var day = 3;
+
+            // 增加动作日志
+            var id = await _xthxApplication.AddOperationLogAsync(dto, HttpContext.RequestAborted);
+            if (!string.IsNullOrEmpty(id))
+            {
+                if (dto.Status == EOperationStatus.SignIn)
+                {
+                    #region 签入 100
+
+                    var xthxList = await _telOperationXthxRepository.Queryable()
+                        .Where(x => x.TelNo == dto.TelNo &&
+                                    x.StaffNo == dto.StaffNo &&
+                                    x.OperationStatus == EOperationStatus.SignIn &&
+                                    SqlFunc.DateDiff(DateType.Day, x.CreationTime, DateTime.Now) < day).ToListAsync();
+                    if (xthxList != null && xthxList.Count > 0)
+                    {
+                        // 判断是否存在数据,存在数据需要判断结束时间是否为空
+                        var rowSign = xthxList.Where(x => x.EndTime == null).ToList();//.Select("EndTime is null");
+                        if (rowSign != null && rowSign.Count > 0)
+                        {// 如果结束时间为空 结束之前的(签收、示忙、小休)
+                            foreach (var item in rowSign)
+                            {
+                                await _xthxApplication.UpdateOperationAsync(id, item.Id, HttpContext.RequestAborted);
+                            }
+                        }
+                    }
+                    await _xthxApplication.AddOperationAsync(dto, id, HttpContext.RequestAborted);
+
+                    #endregion
+                }
+                if (dto.Status == EOperationStatus.ShowBusy)
+                {
+                    #region 示忙 202
+
+                    var xthxList = await _telOperationXthxRepository.Queryable()
+                        .Where(x => x.TelNo == dto.TelNo &&
+                                    x.StaffNo == dto.StaffNo &&
+                                    x.OperationStatus == EOperationStatus.ShowBusy &&
+                                    SqlFunc.DateDiff(DateType.Day, x.CreationTime, DateTime.Now) < day).ToListAsync();
+                    if (xthxList != null && xthxList.Count > 0)
+                    {
+                        // 判断是否存在数据,存在数据需要判断结束时间是否为空
+                        var rowSign = xthxList.Where(x => x.EndTime == null).ToList();//.Select("EndTime is null");
+                        if (rowSign != null && rowSign.Count > 0)
+                        {// 如果结束时间为空 结束之前的(签收、示忙、小休)
+                            foreach (var item in rowSign)
+                            {
+                                await _xthxApplication.UpdateOperationAsync(id, item.Id, HttpContext.RequestAborted);
+                            }
+                        }
+                    }
+                    await _xthxApplication.AddOperationAsync(dto, id, HttpContext.RequestAborted);
+
+                    #endregion
+                }
+                if (dto.Status == EOperationStatus.ShortBreak)
+                {
+                    #region 小休 201
+
+                    var xthxList = await _telOperationXthxRepository.Queryable()
+                        .Where(x => x.TelNo == dto.TelNo &&
+                                    x.StaffNo == dto.StaffNo &&
+                                    x.OperationStatus == EOperationStatus.ShortBreak &&
+                                    SqlFunc.DateDiff(DateType.Day, x.CreationTime, DateTime.Now) < day).ToListAsync();
+                    if (xthxList != null && xthxList.Count > 0)
+                    {
+
+                        // 判断是否存在数据,存在数据需要判断结束时间是否为空
+                        var rowSign = xthxList.Where(x => x.EndTime == null).ToList();//.Select("EndTime is null");
+                        if (rowSign != null && rowSign.Count > 0)
+                        {// 如果结束时间为空 结束之前的(签收、示忙、小休)
+                            foreach (var item in rowSign)
+                            {
+                                await _xthxApplication.UpdateOperationAsync(id, item.Id, HttpContext.RequestAborted);
+                            }
+                        }
+                    }
+                    await _xthxApplication.AddOperationAsync(dto, id, HttpContext.RequestAborted);
+
+                    #endregion
+                }
+                else if (dto.Status == EOperationStatus.FreeTime)
+                {
+                    #region 空闲 200
+
+                    // 当【空闲】时,需要检查示忙、小休是否结束,未结束需要结束
+
+                    var sign = await _telOperationXthxRepository.Queryable()
+                        .Where(x => x.TelNo == dto.TelNo &&
+                                    x.StaffNo == dto.StaffNo &&
+                                   (x.OperationStatus == EOperationStatus.ShowBusy ||
+                                    x.OperationStatus == EOperationStatus.ShortBreak) &&
+                                    x.StartTime != null &&
+                                    x.EndTime == null)
+                        .ToListAsync();
+                    if (sign != null)
+                    {
+                        // 判断是否有签入未结束的动作
+                        foreach (var item in sign)
+                        {
+                            await _xthxApplication.UpdateOperationAsync(id, item.Id!, HttpContext.RequestAborted);
+                        }
+                    }
+
+                    #endregion
+                }
+                else if (dto.Status == EOperationStatus.SignOut)
+                {
+                    #region 签出 0
+
+                    // 当【签出】时,需要检查示忙、小休是否结束,未结束需要结束
+
+                    var sign = await _telOperationXthxRepository.Queryable()
+                        .Where(x => x.TelNo == dto.TelNo &&
+                                    x.StaffNo == dto.StaffNo &&
+                                   (x.OperationStatus == EOperationStatus.SignIn ||
+                                    x.OperationStatus == EOperationStatus.ShowBusy ||
+                                    x.OperationStatus == EOperationStatus.ShortBreak) &&
+                                    x.StartTime != null &&
+                                    x.EndTime == null)
+                        .ToListAsync();
+                    if (sign != null)
+                    {
+                        // 判断是否有签入未结束的动作
+                        foreach (var item in sign)
+                        {
+                            await _xthxApplication.UpdateOperationAsync(id, item.Id!, HttpContext.RequestAborted);
+                        }
+                    }
+
+                    #endregion
+                }
+                else
+                {
+                    #region 其他动作
+
+                    // 除了【签入签出】状态,其他状态都要验证当前是否有【签入】,没有则新增
+                    var sign = await _telOperationXthxRepository.Queryable()
+                        .Where(x => x.TelNo == dto.TelNo &&
+                                    x.StaffNo == dto.StaffNo &&
+                                    x.OperationStatus == EOperationStatus.SignIn &&
+                                    x.StartTime != null &&
+                                    x.EndTime == null)
+                        .ToListAsync();
+                    if (sign.Count == 0)
+                    {
+                        // 未找到数据 新增
+                        dto.Status = EOperationStatus.SignIn;
+                        await _xthxApplication.AddOperationAsync(dto, id, HttpContext.RequestAborted);
+                    }
+
+                    #endregion
+                }
+            }
+            return id;
+        }
+
+        #endregion
+
+    }
+}

+ 5 - 4
src/Hotline.Api/Hotline.Api.csproj

@@ -16,6 +16,7 @@
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.10" />
     <PackageReference Include="MiniExcel" Version="1.36.0" />
+    <PackageReference Include="NETCore.Encrypt" Version="2.1.1" />
     <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
     <PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
     <PackageReference Include="Serilog.Sinks.MongoDB" Version="6.0.0" />
@@ -28,19 +29,19 @@
     <ProjectReference Include="..\Hotline.Application\Hotline.Application.csproj" />
     <ProjectReference Include="..\Hotline.Logger\Hotline.Logger.csproj" />
     <ProjectReference Include="..\Hotline.WeChat\Hotline.WeChat.csproj" />
+    <ProjectReference Include="..\TianQue.Sdk\TianQue.Sdk.csproj" />
   </ItemGroup>
 
   <ItemGroup>
     <None Update="Template\AssignmentForm.doc">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="Template\QualityCertificate.doc">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Remove="logs\**" />
   </ItemGroup>
 
-  <ItemGroup>
-    <Folder Include="Controllers\OrderControlls\" />
-  </ItemGroup>
-
   <ItemGroup>
     <Compile Remove="Controllers\Order\OrderComplementController.cs" />
     <Compile Remove="logs\**" />

+ 1 - 0
src/Hotline.Api/Realtimes/RealtimeMethods.cs

@@ -70,6 +70,7 @@
 
         public static string OrderHandlingDetail = "OrderHandlingDetail";
 
+        public static string OrderSecondaryHandlingDetail = "OrderSecondaryHandlingDetail";
         #endregion
 
         #region 司法大屏

+ 8 - 0
src/Hotline.Api/Realtimes/RealtimeService.cs

@@ -219,6 +219,14 @@ public class RealtimeService : IRealtimeService, IScopeDependency
     public Task OrderHandlingDetailAsync(object obj, CancellationToken cancellationToken) =>
         SendToGroupAsync(RealtimeGroupNames.BigDataScreen, RealtimeMethods.OrderHandlingDetail,obj, cancellationToken);
 
+    /// <summary>
+    /// 推送二次办理中工单概览
+    /// </summary>
+    /// <param name="obj"></param>
+    /// <param name="cancellationToken"></param>
+    /// <returns></returns>
+    public Task OrderSecondaryHandlingDetailAsync(object obj, CancellationToken cancellationToken) =>
+        SendToGroupAsync(RealtimeGroupNames.BigDataScreen, RealtimeMethods.OrderSecondaryHandlingDetail, obj, cancellationToken);
     #endregion
 
     /// <summary>

+ 18 - 9
src/Hotline.Api/StartupExtensions.cs

@@ -37,12 +37,16 @@ using Hotline.XingTang;
 using Hotline.Logger;
 using HotPot.Mvc.Filters;
 using Microsoft.AspNetCore.ResponseCompression;
-using Senparc.Weixin.RegisterServices;
-using Senparc.Weixin.AspNet;
 using Hotline.WeChat;
+using Hotline.Snapshot.Interfaces;
+using TianQue.Sdk;
+using Hotline.Application.Snapshot;
 using Hotline.Orders;
 using XF.Domain.Repository.Events;
 using Hotline.Orders.DatabaseEventHandler;
+using Hotline.Snapshot;
+using Hotline.WeChat;
+using Hotline.Ai.XingTang;
 
 
 namespace Hotline.Api;
@@ -149,6 +153,7 @@ internal static class StartupExtensions
                     .AddKeyedScoped<ISessionContext, ZzptSessionContext>(ZzptSessionContext.Key);
                 break;
             case AppDefaults.AppScope.ZiGong:
+				services.AddAiXingTang(appConfiguration.ZiGong.AiQuality.Url);
                 break;
             case AppDefaults.AppScope.LuZhou:
                 break;
@@ -189,6 +194,7 @@ internal static class StartupExtensions
 
         //services.AddScoped(typeof(IUpdateDatabase<>), typeof(UpdateDatabase<>));
         services.AddScoped<IUpdateDatabaseEvent<OrderVisitDetail>, OrderVisitDetailEventHandler>();
+        // services.AddScoped<IUpdateDatabaseEvent<OrderSnapshot>, OrderSnapshotEventHandler>();
 
         //sqlsugar
         services.AddSqlSugar(configuration);
@@ -218,7 +224,9 @@ internal static class StartupExtensions
 
         services.AddScoped<Users.IThirdIdentiyService, WeChatService>();
 
-        services.AddSenparcWeixin(configuration);
+        services.AddWeChatService();
+
+        services.AddScoped<IGuiderSystemService, TiqnQueService>();
 
         //services.AddScoped<LogFilterAttribute>();
         //ServiceLocator.Instance = services.BuildServiceProvider();
@@ -234,7 +242,11 @@ internal static class StartupExtensions
         if (swaggerEnable)
         {
             app.UseSwagger();
-            app.UseSwaggerUI();
+            app.UseSwaggerUI(options =>
+            {
+                options.DefaultModelsExpandDepth(1);
+                options.DefaultModelExpandDepth(5);
+            });
             //app.UseSwaggerUI(c =>
             //{
             //    //c.DocExpansion(DocExpansion.None);
@@ -252,14 +264,11 @@ internal static class StartupExtensions
         app.MapControllers()
             .RequireAuthorization();
         //app.MapSubscribeHandler();
+        app.UseWeChat();
 
-        var registerService = app.UseSenparcWeixin(app.Environment, null, null,
-            register => { },
-            (register, weixinSetting) => { }
-            );
         // 记录交互日志
         //app.UseRequestResponseLogging(app.Configuration);
-        
+
         app.UseResponseCompression();
 
         return app;

BIN
src/Hotline.Api/Template/QualityCertificate.doc


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

@@ -1,7 +1,7 @@
 {
   "AllowedHosts": "*",
   "AppConfiguration": {
-    "AppScope": "YiBin",
+    "AppScope": "ZiGong",
     "YiBin": {
       "AreaCode": "511500",
       "CallCenterType": "TianRun", //XunShi、WeiErXin、TianRun、XingTang
@@ -25,6 +25,11 @@
       }
     },
     "ZiGong": {
+      //智能质检
+      "AiQuality": {
+        "Url": "http://175.10.86.234:10095" // 正式
+        //"Url": "http://118.122.73.80:19072/", // 测试
+      },
       "AreaCode": "510300",
       "CallCenterType": "XingTang"
     },

+ 2 - 2
src/Hotline.Application.Contracts/Validators/Order/AddOrderDtoValidator.cs

@@ -102,9 +102,9 @@ public class AddOrderDtoValidator : AbstractValidator<AddOrderDto>
         #endregion
 
         #region 投诉对象信息
-        RuleFor(d => d.OrderExtension.EnterpriseName).MaxLengthWithChineseChar(100).When(d => d.OrderExtension != null).WithMessage("企业名称最多100字符");
+        RuleFor(d => d.OrderExtension.EnterpriseName).MaxLengthWithChineseChar(80).When(d => d.OrderExtension != null).WithMessage("企业名称最多80字符");
         RuleFor(d => d.OrderExtension.UnifiedSocialCreditCode).MaxLengthWithChineseChar(30).When(d => d.OrderExtension != null).WithMessage("统一社会信用代码最多30字符");
-        RuleFor(d => d.OrderExtension.RegisterAddress).MaxLengthWithChineseChar(500).When(d => d.OrderExtension != null).WithMessage("企业注册地址最多500字符");
+        RuleFor(d => d.OrderExtension.RegisterAddress).MaxLengthWithChineseChar(400).When(d => d.OrderExtension != null).WithMessage("企业注册地址最多400字符");
         RuleFor(d => d.OrderExtension.RegisterNumber).MaxLengthWithChineseChar(50).When(d => d.OrderExtension != null).WithMessage("注册号最多50字符");
         RuleFor(d => d.OrderExtension.EnterpriseContact).MaxLengthWithChineseChar(70).When(d => d.OrderExtension != null).WithMessage("联系人最多70字符");
         RuleFor(d => d.OrderExtension.MarketTypeCode).MaxLengthWithChineseChar(64).When(d => d.OrderExtension != null).WithMessage("市场主体类型代码最多64字符");

+ 0 - 2
src/Hotline.Application.Contracts/Validators/Snapshot/IndustryValidator.cs

@@ -12,7 +12,5 @@ public class IndustryValidator : AbstractValidator<AddIndustryDto>
     public IndustryValidator()
     {
         RuleFor(m => m.Name).NotEmpty().WithMessage("行业名称不能为空");
-        RuleFor(m => m.CitizenReadPackAmount).NotEqual(0).WithMessage("市民红包不能为空");
-        RuleFor(m => m.GuiderReadPackAmount).NotEqual(0).WithMessage("网格员红包不能为空");
     }
 }

+ 2 - 1
src/Hotline.Application.Tests/Application/DefaultCallApplicationTest.cs

@@ -8,6 +8,7 @@ using Hotline.Orders;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Enums.CallCenter;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.DependencyInjection;
@@ -24,7 +25,7 @@ public class DefaultCallApplicationTest : TestBase
     public readonly IFixture _fixture;
     private readonly IOrderRepository _orderRepository;
 
-    public DefaultCallApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, XingTangCallApplication defaultCallApplication, IOrderVisitRepository orderVisitRepository, IRepository<CallNative> callNativeRepository, IOrderRepository orderRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor)
+    public DefaultCallApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, XingTangCallApplication defaultCallApplication, IOrderVisitRepository orderVisitRepository, IRepository<CallNative> callNativeRepository, IOrderRepository orderRepository, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount)
     {
         _fixture = new Fixture();
         _defaultCallApplication = defaultCallApplication;

+ 106 - 0
src/Hotline.Application.Tests/Application/IndustryApplicationTest.cs

@@ -0,0 +1,106 @@
+using Hotline.Api.Controllers;
+using AutoFixture;
+using Hotline.Application.Snapshot;
+using Hotline.Identity.Accounts;
+using Hotline.Identity.Roles;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Users;
+using Mapster;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using XF.Domain.Repository;
+using Hotline.Settings;
+
+namespace Hotline.Application.Tests.Application;
+public class IndustryApplicationTest : TestBase
+{
+    private readonly IIndustryApplication _industryApplication;
+    private readonly IIndustryRepository _industryRepository;
+    private readonly ISystemOrganizeRepository _systemOrganizeRepository;
+
+    public IndustryApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdIdentiyService, IThirdAccountRepository thirdAccountRepository, IIndustryApplication industryApplication, IIndustryRepository industryRepository, ISystemOrganizeRepository systemOrganizeRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdIdentiyService, thirdAccountRepository)
+    {
+        _industryApplication = industryApplication;
+        _industryRepository = industryRepository;
+        _systemOrganizeRepository = systemOrganizeRepository;
+    }
+
+    [Fact]
+    public async Task UpdateIndustry_Test()
+    {
+        var industryItems = _industryApplication.GetIndustres(new IndustryListInDto(null, null)).ToList();
+        industryItems.NotNullOrEmpty().ShouldBeTrue("行业集合为空");
+        var industry = industryItems.First().Adapt<UpdateIndustryInDto>();
+        var item = await _industryRepository.GetAsync(industry.Id);
+        industry.ForeachClassProperties(async (industry, property, name, value) =>
+        {
+            if (name.ToUpper() == "ID") return true;
+            if (value is String)
+                industry.GetType().GetProperty(name).SetValue(industry, value + DateTime.Now.ToString("ss"));
+            return true;
+        });
+        var orgs = await _systemOrganizeRepository.GetOrgEnabled();
+        industry.ApproveOrgId = orgs.First().Key;
+        industry.ApproveOrgName = null;
+        await _industryApplication.UpdateIndustryAsync(industry, CancellationToken.None);
+        var updateIndustry = await _industryApplication.GetIndustryDetailAsync(item.Id);
+        updateIndustry.ForeachClassProperties(async (industry, property, name, value) =>
+        {
+            industry.GetType().GetProperty(name).GetValue(industry).ShouldBe(value);
+            return true;
+        });
+        await _industryApplication.UpdateIndustryAsync(item.Adapt<UpdateIndustryInDto>(), CancellationToken.None);
+    }
+
+    [Fact]
+    public async Task IndustryCase_Test()
+    {
+        var industry = await _industryApplication.GetIndustres(new IndustryListInDto(null, null)).ToListAsync();
+        var industryCase = new AddIndustryCaseDto
+        {
+            IndustryId = industry.First().Id,
+            Name = "单元测试" + DateTime.Now.ToString("ss"),
+            CitizenReadPackAmount = 10,
+            GuiderReadPackAmount = 10,
+            IsEnable = true,
+            DisplayOrder = 1
+        };
+        var caseId = await _industryApplication.AddIndustryCaseAsync(industryCase);
+        var items = await _industryApplication.GetIndustryCaseItems(new IndustryCaseItemInDto(null, null)).ToListAsync();
+        items.Count.ShouldNotBe(0);
+        items.Any(m => m.Id.IsNullOrEmpty()).ShouldBeFalse();
+        items.Any(m => m.Name.IsNullOrEmpty()).ShouldBeFalse();
+
+        var caseEntity = await _industryApplication.GetIndustryCaseAsync(caseId);
+        var updateDto = caseEntity.Adapt<UpdateIndustryCaseDto>();
+        updateDto.DisplayOrder = 2;
+        await _industryApplication.UpdateIndustryCaseAsync(updateDto);
+        var caseEntityUpdate = await _industryApplication.GetIndustryCaseAsync(caseId);
+        caseEntityUpdate.DisplayOrder.ShouldBe(updateDto.DisplayOrder);
+        caseEntityUpdate.Name.ShouldBe(updateDto.Name);
+    }
+
+    /// <summary>
+    /// 新增行业短信模板
+    /// 行业短信模板集合
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task SMSTemplate_Test()
+    {
+        var industryItems = await _industryApplication.GetIndustres(new IndustryListInDto(null, null)).ToListAsync();
+        var industry = industryItems.First();
+        var inDto = _fixture.Create<AddSnapshotSMSTemplateInDto>();
+        inDto.IndustryId = industry.Id;
+        var smsId = await _industryApplication.AddSMSTemplateAsync(inDto);
+        var items = await _industryApplication.GetSMSTemplates(new SnapshotSMSTemplateItemsInDto(null)).ToListAsync();
+        items.Count.ShouldNotBe(0);
+        var sms = items.Where(m => m.Id == smsId).First();
+        sms.Content.ShouldBe(inDto.Content);
+        sms.IndustryName.ShouldNotBeNullOrEmpty();
+        sms.IsEnable.ShouldBe(inDto.IsEnable);
+    }
+}

+ 50 - 0
src/Hotline.Application.Tests/Application/InviteCodeApplicationTest.cs

@@ -0,0 +1,50 @@
+using Hotline.Api.Controllers;
+using Hotline.Application.Snapshot;
+using Hotline.Identity.Accounts;
+using Hotline.Identity.Roles;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Users;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Tests.Application;
+public class InviteCodeApplicationTest : TestBase
+{
+    private readonly IInviteCodeApplication _inviteCodeApplication;
+
+    public InviteCodeApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdIdentiyService, IThirdAccountRepository thirdAccountRepository, IInviteCodeApplication inviteCodeApplication) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdIdentiyService, thirdAccountRepository)
+    {
+        _inviteCodeApplication = inviteCodeApplication;
+    }
+
+    [Fact]
+    public async Task InviteCode_Test()
+    {
+        var inDto = new AddInviteCodeInDto
+        {
+            OrgName = "测试部门" + DateTime.Now.ToString("ss"),
+            BeginCode = 100,
+            EndCode = 200,
+        };
+
+        await _inviteCodeApplication.AddInviteCodeAsync(inDto);
+
+        var items = _inviteCodeApplication.GetInviteCodeItems().ToList();
+        items.Count.ShouldNotBe(0);
+
+        var statics = await _inviteCodeApplication.GetInviteCodeStatisticAsync(new GetInviteCodeStatisticInDto 
+        {
+            StartTime = DateTime.Now.AddDays(-1),
+            EndTime = DateTime.Now
+        }).ToListAsync();
+        statics.Count.ShouldNotBe(0);
+    }
+}

+ 2 - 1
src/Hotline.Application.Tests/Application/KnowApplicationTest.cs

@@ -6,6 +6,7 @@ using Hotline.KnowledgeBase;
 using Hotline.KnowledgeBase.Notifies;
 using Hotline.Share.Dtos.Knowledge;
 using Hotline.Share.Tools;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using Mapster;
 using MediatR;
@@ -31,7 +32,7 @@ public class KnowApplicationTest : TestBase
     private readonly IRepository<KnowledgeWord> _knowledgeWordRepository;
     private readonly IRepository<KnowledgeHotWord> _knowledgeHotWordRepository;
 
-    public KnowApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IKnowApplication knowApplication, IRepository<KnowledgeRelationType> knowledgeRelationTypeRepository, IMediator mediator, IRepository<KnowledgeBase.Knowledge> knowledgeRepository, IKnowledgeDomainService knowledgeDomainService, IRepository<KnowledgeWord> knowledgeWordRepository, IRepository<KnowledgeHotWord> knowledgeHotWordRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor)
+    public KnowApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IKnowApplication knowApplication, IRepository<KnowledgeRelationType> knowledgeRelationTypeRepository, IMediator mediator, IRepository<KnowledgeBase.Knowledge> knowledgeRepository, IKnowledgeDomainService knowledgeDomainService, IRepository<KnowledgeWord> knowledgeWordRepository, IRepository<KnowledgeHotWord> knowledgeHotWordRepository, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount)
     {
         _knowApplication = knowApplication;
         _knowledgeRelationTypeRepository = knowledgeRelationTypeRepository;

+ 76 - 0
src/Hotline.Application.Tests/Application/OrderSnapshotApplicationTest.cs

@@ -0,0 +1,76 @@
+using Hotline.Api.Controllers;
+using Hotline.Application.Snapshot;
+using Hotline.Application.Tests.Mock;
+using Hotline.Caching.Interfaces;
+using Hotline.Identity.Accounts;
+using Hotline.Identity.Roles;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Requests;
+using Hotline.Share.Tools;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Users;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Tests.Application;
+public class OrderSnapshotApplicationTest : TestBase
+{
+    private readonly OrderServiceMock _orderServiceMock;
+    private readonly ISystemDicDataCacheManager _systemDicDataCacheManager;
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
+    private readonly IOrderSnapshotApplication _orderSnapshotApplication;
+    public OrderSnapshotApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdIdentiyService, IThirdAccountRepository thirdAccountRepository, OrderServiceMock orderServiceMock, ISystemDicDataCacheManager systemDicDataCacheManager, IOrderSnapshotRepository orderSnapshotRepository, IOrderSnapshotApplication orderSnapshotApplication) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdIdentiyService, thirdAccountRepository)
+    {
+        _orderServiceMock = orderServiceMock;
+        _systemDicDataCacheManager = systemDicDataCacheManager;
+        _orderSnapshotRepository = orderSnapshotRepository;
+        _orderSnapshotApplication = orderSnapshotApplication;
+    }
+
+    /// <summary>
+    /// 随手拍办理流程
+    /// 工单标注 : 验证数据是否保存正确
+    /// 获取已标注集合: 验证是否有已标注的数据
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task SnapshotWrokflow_Test()
+    {
+        var snapshotLabels = _systemDicDataCacheManager.SnapshotOrderLabel;
+        var inputLable = snapshotLabels.Where(m => m.DicDataValue == "bss").ToList();
+        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin)
+            .办理到工单标注(SetZuoXi)
+            .办理到派单员(SetZuoXi, false)
+            .办理到归档(SetZuoXi)
+            .发布工单(SetZuoXi, inputLable.Select(m => new Kv { Key = m.DicDataValue, Value = m.DicDataName}).ToList())
+            .GetCreateResult();
+        order.Id.ShouldNotBeNull();
+
+        var snapshot = await _orderSnapshotRepository.GetAsync(order.Id);
+        // 验证工单标注是否生效
+        snapshot.ShouldNotBeNull();
+        snapshot.IsTruthDepartment.ShouldBe(false);
+        snapshot.LabelName.ShouldBe(string.Join('|', inputLable.Select(m => m.DicDataName)));
+        snapshot.Labels.ShouldNotBeNull();
+        snapshot.Labels.Count.ShouldBe(inputLable.Count);
+
+        snapshot.IsSafetyDepartment.ShouldBe(false);
+
+        var labelLog = await _orderSnapshotApplication.GetLabelOrderSnapshotLogItems(new LabelOrderSnapshotLogItemsInDto()).ToPagedListAsync(new PagedRequest());
+        labelLog.Items.Any(m => m.Title.IsNullOrEmpty()).ShouldBe(false);
+        labelLog.Items.Any(m => m.SourceChannel.IsNullOrEmpty()).ShouldBe(false);
+
+        var sigedItems = await _orderSnapshotApplication.GetLabeledOrderSnapshotItems(new LabeledOrderSnapshotItemsInDto()).ToListAsync();
+        sigedItems.Any(m => m.OrderId == order.Id).ShouldBe(true);
+    }
+}
+

+ 92 - 0
src/Hotline.Application.Tests/Application/RedPackApplicationTest.cs

@@ -0,0 +1,92 @@
+using Hotline.Api.Controllers;
+using Hotline.Application.Snapshot;
+using Hotline.Identity.Accounts;
+using Hotline.Identity.Roles;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Enums.Snapshot;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Users;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Tests.Application;
+public class RedPackApplicationTest : TestBase
+{
+    private readonly IRedPackApplication _redPackApplication;
+    private readonly IRedPackRecordRepository _redPackRecordRepository;
+
+    public RedPackApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdIdentiyService, IThirdAccountRepository thirdAccountRepository, IRedPackApplication redPackApplication, IRedPackRecordRepository redPackRecordRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdIdentiyService, thirdAccountRepository)
+    {
+        _redPackApplication = redPackApplication;
+        _redPackRecordRepository = redPackRecordRepository;
+    }
+
+    /// <summary>
+    /// 获取审核集合
+    /// 获取审核短信模板
+    /// 审核通过
+    /// 获取红包记录
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task AuditRedPackAudit_Test()
+    {
+        var items = await _redPackApplication.GetRedPackAuditItems(new SnapshotOrderAuditItemsInDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 0)).ToListAsync();
+        var audit = items.First();
+        var smsTemplate = await _redPackApplication.GetRedPackAuditSMSTemplateAsync(new GetRedPackAuditSMSTemplateInDto(audit.OrderId, ESnapshotSMSStatus.Agree));
+        var inDto = new UpdateRedPackAuditInDto
+        {
+            Status = ESnapshotSMSStatus.Agree,
+            Opinion = "单元测试" + DateTime.Now.ToShortDateString(),
+            SMSTemplateId = smsTemplate.First().Id,
+            IsSendSms = true,
+            RedPackAuditId = audit.Id,
+        };
+        await _redPackApplication.AuditRedPackAuditAsync(inDto);
+        var suInDto = new UpdateRedPackRecordInDto
+        {
+            RedPackAuditId = audit.Id,
+            Name = "单元测试Name",
+            BankCardNo = "单元测试银行号",
+            OpenBank = "单元测试开户行",
+            ReplenishAmount = 100.01,
+            ReplenishTime = DateTime.Now,
+            ReplenishRemark = "单元测试补发备注",
+            IsSendSMS = false,
+            ReplenishType = "%15",
+            ReplenishTypeId = "1",
+        };
+        await _redPackApplication.UpdateRedPackRecordAsync(suInDto);
+
+        items = await _redPackApplication.GetRedPackAuditItems(new SnapshotOrderAuditItemsInDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 1)).ToListAsync();
+        items.Any(m => m.Id == audit.Id).ShouldBeTrue();
+
+        var record = await _redPackRecordRepository.Queryable()
+            .Where(m => m.OrderId == audit.OrderId)
+            .FirstAsync();
+        record.ShouldNotBeNull();
+        var recordItems = await _redPackApplication.GetRedPackRecordItems(new SnapshotRedPackRecordItemsInDto { Status =2}).ToListAsync();
+        recordItems.Count.ShouldNotBe(0);
+
+        var redPackRecord = recordItems.First();
+        var redPackRecordEntity = await _redPackRecordRepository.GetAsync(redPackRecord.Id);
+        redPackRecordEntity.PickupStatus = ERedPackPickupStatus.Received;
+        redPackRecordEntity.DistributionState = EReadPackSendStatus.Successful;
+        redPackRecordEntity.ReceiveTime = DateTime.Now;
+        await _redPackRecordRepository.UpdateAsync(redPackRecordEntity);
+
+        var sendRecordItems = await _redPackApplication.GetRedPackRecordDetail(new SnapshotRedPackRecordSendInDto
+        {
+            IsReceive = true,
+        }).ToListAsync();
+        sendRecordItems.Count.ShouldNotBe(0);
+    }
+}

+ 55 - 8
src/Hotline.Application.Tests/Application/SnapshotApplicationMockTest.cs

@@ -1,8 +1,11 @@
 using System.Threading;
 using System.Threading.Tasks;
+using DotNetCore.CAP;
 using Hotline.Application.Snapshot;
 using Hotline.Caching.Interfaces;
+using Hotline.EventBus;
 using Hotline.File;
+using Hotline.FlowEngine.Workflows;
 using Hotline.Orders;
 using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Settings;
@@ -10,6 +13,7 @@ using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Dtos.Snapshot;
 using Hotline.Share.Enums.Snapshot;
 using Hotline.Snapshot;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using Mapster;
 using Moq;
@@ -22,36 +26,60 @@ namespace Hotline.Application.Tests.Snapshot
     public class SnapshotApplicationTest
     {
         private readonly Mock<IThirdIdentiyService> _thirdLoginServiceMock;
-        private readonly Mock<IRepository<Industry>> _industryRepositoryMock;
-        private readonly Mock<IRepository<Article.Bulletin>> _bulletinRepositoryMock;
+        private readonly Mock<IIndustryRepository> _industryRepositoryMock;
+        private readonly Mock<ISnapshotBulletinRepository> _bulletinRepositoryMock;
         private readonly Mock<ISessionContext> _sessionContextMock;
         private readonly Mock<IRepository<RedPackRecord>> _redPackRecordRepositoryMock;
         private readonly Mock<IRepository<Order>> _orderRepositoryMock;
         private readonly Mock<IThirdAccountRepository> _thirdAccountRepositoryMock;
-        private readonly Mock<IRepository<OrderSnapshot>> _orderSnapshotRepositoryMock;
+        private readonly Mock<IOrderSnapshotRepository> _orderSnapshotRepositoryMock;
         private readonly Mock<ISystemSettingCacheManager> _systemSettingCacheManagerMock;
         private readonly Mock<ISystemAreaDomainService> _systemAreaDomainServiceMock;
         private readonly Mock<IFileRepository> _fileRepositoryMock;
         private readonly Mock<ISystemDicDataCacheManager> _systemDicDataCacheManagerMock;
+        private readonly Mock<ISnapshotOrderPublishRepository> _snapshotOrderPublishRepositoryMock;
+        private readonly Mock<IRepository<WorkflowTrace>> _workflowTraceRepositoryMock;
+        private readonly Mock<IPractitionerRepository> _practitionerRepositoryMock;
+        private readonly Mock<IRepository<SystemArea>> _systemAreaRepositoryMock;
+        private readonly Mock<IVolunteerRepository> _volunteerRepositoryMock;
+        private readonly Mock<IVolunteerReportRepository> _volunteerReportRepositoryMock;
+        private readonly Mock<ISystemLogRepository> _systemLogMock;
+        private readonly Mock<IGuiderSystemService> _guiderSystemServiceMock;
+        private readonly Mock<ICapPublisher> _capPublisherMock;
+        private readonly Mock<Publisher> _publisherMock;
+        private readonly Mock<IGuiderInfoRepository> _guiderInfoRepositoryMock;
+        private readonly Mock<IFileDomainService> _fileDomainServiceMock;
 
         private readonly DefaultSnapshotApplication _snapshotApplication;
 
         public SnapshotApplicationTest()
         {
             _thirdLoginServiceMock = new Mock<IThirdIdentiyService>();
-            _industryRepositoryMock = new Mock<IRepository<Industry>>();
-            _bulletinRepositoryMock = new Mock<IRepository<Article.Bulletin>>();
+            _industryRepositoryMock = new Mock<IIndustryRepository>();
+            _bulletinRepositoryMock = new Mock<ISnapshotBulletinRepository>();
             _sessionContextMock = new Mock<ISessionContext>();
             _redPackRecordRepositoryMock = new Mock<IRepository<RedPackRecord>>();
             _orderRepositoryMock = new Mock<IRepository<Order>>();
             _thirdAccountRepositoryMock = new Mock<IThirdAccountRepository>();
-            _orderSnapshotRepositoryMock = new Mock<IRepository<OrderSnapshot>>();
+            _orderSnapshotRepositoryMock = new Mock<IOrderSnapshotRepository>();
             _systemSettingCacheManagerMock = new Mock<ISystemSettingCacheManager>();
             _systemAreaDomainServiceMock = new Mock<ISystemAreaDomainService>();
             _fileRepositoryMock = new Mock<IFileRepository>();
             _systemDicDataCacheManagerMock = new Mock<ISystemDicDataCacheManager>();
+            _snapshotOrderPublishRepositoryMock = new Mock<ISnapshotOrderPublishRepository>();
+            _workflowTraceRepositoryMock = new Mock<IRepository<WorkflowTrace>>();
+            _practitionerRepositoryMock = new Mock<IPractitionerRepository>();
+            _systemAreaRepositoryMock = new Mock<IRepository<SystemArea>>();
+            _volunteerRepositoryMock = new Mock<IVolunteerRepository>();
+            _volunteerReportRepositoryMock = new Mock<IVolunteerReportRepository>();
+            _systemLogMock = new Mock<ISystemLogRepository>();
+            _guiderSystemServiceMock = new Mock<IGuiderSystemService>();
+            _capPublisherMock = new Mock<ICapPublisher>();
+            _publisherMock = new Mock<Publisher>();
+            _guiderInfoRepositoryMock = new Mock<IGuiderInfoRepository>();
+            _fileDomainServiceMock = new Mock<IFileDomainService>();
 
-            _snapshotApplication = new DefaultSnapshotApplication(
+        _snapshotApplication = new DefaultSnapshotApplication(
                 _thirdLoginServiceMock.Object,
                 _industryRepositoryMock.Object,
                 _bulletinRepositoryMock.Object,
@@ -63,7 +91,26 @@ namespace Hotline.Application.Tests.Snapshot
                 _systemSettingCacheManagerMock.Object,
                 _systemAreaDomainServiceMock.Object,
                 _fileRepositoryMock.Object,
-                _systemDicDataCacheManagerMock.Object
+                _systemDicDataCacheManagerMock.Object,
+                _snapshotOrderPublishRepositoryMock.Object,
+                _workflowTraceRepositoryMock.Object,
+                _practitionerRepositoryMock.Object,
+                _systemAreaRepositoryMock.Object,
+                _volunteerRepositoryMock.Object,
+                _volunteerReportRepositoryMock.Object,
+                _systemLogMock.Object,
+                _guiderSystemServiceMock.Object,
+                _capPublisherMock.Object,
+                _publisherMock.Object,
+                _guiderInfoRepositoryMock.Object,
+                _fileDomainServiceMock.Object,
+                null,
+                null,
+                null,
+                null,
+                null,
+                null,
+                null
             );
         }
 

+ 355 - 51
src/Hotline.Application.Tests/Application/SnapshotApplicationTest.cs

@@ -1,20 +1,30 @@
-using DocumentFormat.OpenXml.Wordprocessing;
+using AutoFixture;
+using DocumentFormat.OpenXml.Wordprocessing;
 using Hotline.Api.Controllers;
 using Hotline.Application.Identity;
 using Hotline.Application.Snapshot;
+using Hotline.Application.Tests.Mock;
+using Hotline.Caching.Interfaces;
 using Hotline.File;
 using Hotline.Identity.Accounts;
 using Hotline.Identity.Roles;
+using Hotline.Orders;
+using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Share.Dtos.Article;
 using Hotline.Share.Dtos.Snapshot;
 using Hotline.Share.Enums;
+using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Snapshot;
 using Hotline.Share.Tools;
 using Hotline.Snapshot;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Snapshot.Notifications;
 using Hotline.Users;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.DependencyInjection;
 using Shouldly;
+using System;
+using XF.Domain.Authentications;
 using XF.Domain.Repository;
 using XF.Utility.EnumExtensions;
 
@@ -27,8 +37,18 @@ public class SnapshotApplicationTest : TestBase
     private readonly IIndustryApplication _industryApplication;
     private readonly IIndustryRepository _industryRepository;
     private readonly IFileRepository _fileRepository;
+    private readonly OrderServiceMock _orderServiceMock;
+    private readonly IOrderRepository _orderRepository;
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
+    private readonly ISessionContext _sessionContext;
+    private readonly IGuiderSystemService _guiderSystemService;
+    private readonly ISystemSettingCacheManager _systemSettingCacheManager;
+    private readonly ICommunityInfoRepository _communityInfoRepository;
+    private readonly IIndustryLogRepository _industryLogRepository;
+    private readonly IRedPackApplication _redPackApplication;
+    private readonly IOrderSnapshotApplication _orderSnapshotApplication;
 
-    public SnapshotApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, ISnapshotApplication snapshotApplication, IIdentityAppService identityAppService, IRepository<RedPackRecord> redPackRecordRepository, IIndustryApplication industryApplication, IIndustryRepository industryRepository, IFileRepository fileRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor)
+    public SnapshotApplicationTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, ISnapshotApplication snapshotApplication, IIdentityAppService identityAppService, IRepository<RedPackRecord> redPackRecordRepository, IIndustryApplication industryApplication, IIndustryRepository industryRepository, IFileRepository fileRepository, OrderServiceMock orderServiceMock, IOrderRepository orderRepository, IOrderSnapshotRepository orderSnapshotRepository, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount, ISessionContext sessionContext, IGuiderSystemService guiderSystemService, ISystemSettingCacheManager systemSettingCacheManager, ICommunityInfoRepository communityInfoRepository, IIndustryLogRepository industryLogRepository, IRedPackApplication redPackApplication, IOrderSnapshotApplication orderSnapshotApplication) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount)
     {
         _snapshotApplication = snapshotApplication;
         _identityAppService = identityAppService;
@@ -36,6 +56,38 @@ public class SnapshotApplicationTest : TestBase
         _industryApplication = industryApplication;
         _industryRepository = industryRepository;
         _fileRepository = fileRepository;
+        _orderServiceMock = orderServiceMock;
+        _orderRepository = orderRepository;
+        _orderSnapshotRepository = orderSnapshotRepository;
+        SetWeiXin();
+        _sessionContext = sessionContext;
+        _guiderSystemService = guiderSystemService;
+        _systemSettingCacheManager = systemSettingCacheManager;
+        _communityInfoRepository = communityInfoRepository;
+        _industryLogRepository = industryLogRepository;
+        _redPackApplication = redPackApplication;
+        _orderSnapshotApplication = orderSnapshotApplication;
+    }
+
+    /// <summary>
+    /// 随手拍公开集合
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task GetOrderSnapshotPublishItemsAsync()
+    {
+        var items = await _orderSnapshotApplication.GetOrderSnapshotPublishItems(new GetOrderSnapshotPublishItemsInDto()).ToPageListAsync(0, 10);
+        items.NotNullOrEmpty().ShouldBeTrue();
+        items = await _orderSnapshotApplication.GetOrderSnapshotPublishItems(new GetOrderSnapshotPublishItemsInDto
+        {
+            OrderStatus = EOrderStatus.Filed
+        }).ToPageListAsync(0, 10);
+        items.NotNullOrEmpty().ShouldBeTrue();
+        items = await _orderSnapshotApplication.GetOrderSnapshotPublishItems(new GetOrderSnapshotPublishItemsInDto
+        {
+            IsPublished = true
+        }).ToPageListAsync(0, 10);
+        items.NotNullOrEmpty().ShouldBeTrue();
     }
 
     [Fact]
@@ -46,6 +98,106 @@ public class SnapshotApplicationTest : TestBase
         result.Industrys.First().DisplayOrder.ShouldBe(1, "排序异常");
     }
 
+    [Fact]
+    public async Task GetBulletionPopup_Test()
+    {
+        var item = await _snapshotApplication.GetBulletionPopupAsync(CancellationToken.None);
+        //item.ShouldNotBeNull();
+    }
+
+
+    /// <summary>
+    /// 添加随手拍公告
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task AddBulletin_Test()
+    {
+        var industry = await _industryRepository.Queryable()
+            .Where(m => m.BulletinTypeGuideId != null && m.BulletinTypeGuideName != null)
+            .OrderBy(m => m.DisplayOrder)
+            .FirstAsync();
+        if (industry == null)
+        {
+            return;
+        }
+        var inDto = new AddSnapshotBulletinInDto
+        {
+            Title = "单元测试" + DateTime.Now.ToLongDateTimeString(),
+            Content = "测试内容" + DateTime.Now.ToLongDateTimeString(),
+            BulletinTypeId = industry.BulletinTypeGuideId!,
+            BulletinTypeName = industry.BulletinTypeGuideName!
+        };
+        var bulletinId = await _snapshotApplication.AddBulletinAsync(inDto);
+        inDto = new AddSnapshotBulletinInDto
+        {
+            Title = "单元测试" + DateTime.Now.ToLongDateTimeString(),
+            Content = "测试内容" + DateTime.Now.ToLongDateTimeString(),
+            BulletinTypeId = industry.BulletinTypePublicityId!,
+            BulletinTypeName = industry.BulletinTypePublicityName!
+        };
+        bulletinId = await _snapshotApplication.AddBulletinAsync(inDto);
+        await _snapshotApplication.AuditBulletinAsync(new ExamineBulletinDto { Id = bulletinId, IsPass = true, Reason = "测试审核通过" });
+
+        var items = await _snapshotApplication.GetBulletinsAsync(new BulletinInDto { IndustryId = industry.Id }, CancellationToken.None);
+        items.Count.ShouldNotBe(0, "公告数量为0");
+    }
+
+    /// <summary>
+    /// 获取公开工单集合
+    /// 发布到公开工单
+    /// 审核通过公开工单
+    /// 获取特提参数
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task PublishOrder_Test()
+    {
+        var industry = await _industryRepository.Queryable()
+            .Where(m => m.IndustryType == EIndustryType.Clue)
+            .OrderBy(m => m.DisplayOrder)
+            .FirstAsync();
+
+        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin)
+            .办理到一级部门(SetZuoXi)
+            .办理到二级部门(Set一级部门)
+            .办理一级部门汇总(Set二级部门)
+            .办理到归档(Set一级部门)
+            .GetCreateResult();
+        SetZuoXi();
+        var auditDetail = await _orderSnapshotApplication.GetOrderSnapshotPublishDetailAsync(order.Id);
+        var orderEntity = await _orderRepository.GetAsync(order.Id);
+        var orderSnapshot = await _orderSnapshotRepository.GetAsync(order.Id);
+        var inDto = new AddSnapshotOrderPublishInDto
+        {
+            ArrangeContent = auditDetail.Content,
+            ArrangeOpinion = auditDetail.FileOpinion,
+            ArrangeTitle = auditDetail.Title,
+            OrderId = auditDetail.Id,
+            ArrangeAddress = auditDetail.FullAddress,
+            HandleTime = DateTime.Now
+        };
+        var auditId = await _orderSnapshotApplication.AddOrderPublishAsync(inDto, CancellationToken.None);
+        var items = await _snapshotApplication.GetOrderPublishAsync(new OrderPublishInDto { IndustryId = industry.Id }, CancellationToken.None);
+        items.Any(m => m.No == order.No).ShouldBeFalse();
+        await _orderSnapshotApplication.UpdateOrderSnapshotPublishStatusAsync(new UpdateOrderSnapshotPublishStatusInDto { Id = auditId, Status = EOrderSnapshotPublishStatus.Agree });
+
+        items = await _snapshotApplication.GetOrderPublishAsync(new OrderPublishInDto
+        {
+            IndustryId = industry.Id
+        }, CancellationToken.None);
+        items.Any(m => m.No == order.No).ShouldBeTrue();
+
+        var orderPublishDetail = await _snapshotApplication.GetOrderPublishDetailAsync(items.OrderByDescending(m => m.HandleTime).First().Id, CancellationToken.None);
+        orderPublishDetail.ShouldNotBeNull();
+        orderPublishDetail.Workflow.Any(m => m.Name.IsNullOrEmpty()).ShouldBeFalse();
+        await _snapshotApplication.AddRedPardAsync(order.Id, CancellationToken.None);
+        var guiderItems = await _redPackApplication
+            .GetRedPackGuiderAuditItems(new SnapshotOrderGuiderAuditItemsInDto(null, null, -1))
+            .ToListAsync();
+        guiderItems.Count.ShouldNotBe(0);
+    }
+
     [Fact]
     public async Task GetBulletins_Test()
     {
@@ -54,7 +206,7 @@ public class SnapshotApplicationTest : TestBase
         {
             IndustryId = homePage.Industrys.First(m => m.Name == "文化旅游").Id,
         };
-        var items = await _snapshotApplication.GetBulletinsAsync(inDto);
+        var items = await _snapshotApplication.GetBulletinsAsync(inDto, CancellationToken.None);
         items.ShouldNotBeNull();
         items.Any().ShouldBe(true, "公告数据为空");
         items.Any(m => m.Title.IsNullOrEmpty()).ShouldBe(false, "标题错误");
@@ -62,18 +214,43 @@ public class SnapshotApplicationTest : TestBase
         items.Any(m => m.Id.IsNullOrEmpty()).ShouldBe(false, "Id错误");
     }
 
+    /// <summary>
+    /// 获取小程序个人中心
+    /// </summary>
+    /// <returns></returns>
     [Fact]
     public async Task GetSnapshotUserInfo_Test()
     {
+        await _identityAppService.GetThredTokenAsync(new ThirdTokenInDto());
         var result = await _snapshotApplication.GetSnapshotUserInfoAsync();
         result.ShouldNotBeNull();
         result.PhoneNumber.ShouldNotBeNullOrEmpty();
+        result.PhoneNumberMask.Contains("***").ShouldBeTrue();
     }
 
+    /// <summary>
+    /// 根据OpenId刷新token
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task RefreshTokenAsync()
+    {
+        var token = await _identityAppService.GetThredTokenAsync(new ThirdTokenInDto());
+        var newToken = await _identityAppService.RefreshTokenAsync(token.OpenId);
+        newToken.ShouldNotBeNull();
+        newToken.OpenId.ShouldBe(token.OpenId);
+        newToken.PhoneNumber.ShouldNotBeNullOrEmpty();
+    }
+
+    /// <summary>
+    /// 请求第三方token
+    /// </summary>
+    /// <returns></returns>
     [Fact]
     public async Task GetThirdToken_Test()
     {
         var result = await _identityAppService.GetThredTokenAsync(new ThirdTokenInDto { LoginCode = "0c3Adhll2zDMBe413rnl2KvEym2AdhlH" });
+        result.PhoneNumber.ShouldNotBeNullOrEmpty();
     }
 
     [Theory]
@@ -83,12 +260,12 @@ public class SnapshotApplicationTest : TestBase
     {
         var dto = new OrderInDto();
         dto.KeyWords = key;
-        var page = await _snapshotApplication.GetSnapshotOrdersAsync(dto);
-        page.Total.ShouldNotBe(0);
-        page.Items.FirstOrDefault()?.IndustryName.ShouldNotBeNullOrEmpty();
-        page.Items.FirstOrDefault()?.OrderNo.ShouldNotBeNullOrEmpty();
-        page.Items.FirstOrDefault()?.StatusText.ShouldNotBeNullOrEmpty();
-        page.Items.FirstOrDefault()?.Area.ShouldNotBeNullOrEmpty();
+        var page = await _snapshotApplication.GetSnapshotOrdersAsync(dto, CancellationToken.None);
+        page.Count.ShouldNotBe(0);
+        page.FirstOrDefault()?.IndustryName.ShouldNotBeNullOrEmpty();
+        page.FirstOrDefault()?.OrderNo.ShouldNotBeNullOrEmpty();
+        page.FirstOrDefault()?.StatusText.ShouldNotBeNullOrEmpty();
+        page.FirstOrDefault()?.Area.ShouldNotBeNullOrEmpty();
     }
 
     [Theory]
@@ -99,73 +276,138 @@ public class SnapshotApplicationTest : TestBase
     public async Task SnapshotOrderStatus_Test(EOrderQueryStatus status, int count)
     {
         var dto = new OrderInDto { Status = status };
-        var page = await _snapshotApplication.GetSnapshotOrdersAsync(dto);
-        page.Total.ShouldNotBe(0, $"状态:{status.GetDescription()} 数据为空");
-        page.Total.ShouldBe(count, $"状态:{status.GetDescription()} 数据条数错误");
-        page.Items.FirstOrDefault()?.IndustryName.ShouldNotBeNullOrEmpty();
-        page.Items.FirstOrDefault()?.OrderNo.ShouldNotBeNullOrEmpty();
-        page.Items.FirstOrDefault()?.StatusText.ShouldNotBeNullOrEmpty();
-        page.Items.FirstOrDefault()?.Area.ShouldNotBeNullOrEmpty();
-
-        dto.PageIndex = 2;
-        page = await _snapshotApplication.GetSnapshotOrdersAsync(dto);
-        page.Items.Count.ShouldBe(0);
+        var page = await _snapshotApplication.GetSnapshotOrdersAsync(dto, CancellationToken.None);
+        page.Count.ShouldNotBe(0, $"状态:{status.GetDescription()} 数据为空");
     }
 
+    /// <summary>
+    /// 获取工单详情
+    /// </summary>
+    /// <returns></returns>
     [Fact]
     public async Task GetSnapshotOrderDetail_Test()
     {
-        var page = await _snapshotApplication.GetSnapshotOrdersAsync(new OrderInDto());
-        var id = page.Items.First().Id;
-        var detail = await _snapshotApplication.GetSnapshotOrderDetailAsync(id);
+        var page = await _snapshotApplication.GetSnapshotOrdersAsync(new OrderInDto(), CancellationToken.None);
+        var id = page.First().Id;
+        var detail = await _snapshotApplication.GetSnapshotOrderDetailAsync(id, CancellationToken.None);
         detail.Id.ShouldBe(id);
-        detail.IndustryName.ShouldNotBeNull();
+        detail.Title.ShouldNotBeNullOrEmpty();
+        detail.Opinion.ShouldNotBeNullOrEmpty();
+        detail.Content.ShouldNotBeNullOrEmpty();
     }
 
+    /// <summary>
+    /// 红包列表记录
+    /// </summary>
+    /// <param name="count"></param>
+    /// <param name="exp"></param>
+    /// <returns></returns>
     [Theory]
     [InlineData(2, 2)]
     [InlineData(12, 12)]
     public async Task GetRedPackDateAsync(int count, int exp)
     {
-        var items = await _snapshotApplication.GetRedPackDateAsync(new RedPackDateInDto { Count = count });
-        items.Count.ShouldNotBe(0, "0数据");
-        items.Count.ShouldBe(exp, $"应该:{exp}, 实际 {items.Count}");
+        var items = await _snapshotApplication.GetRedPackDateAsync(new RedPackDateInDto { }, CancellationToken.None);
     }
 
-    [Theory]
-    [InlineData(ERedPackPickupStatus.Unreceived)]
-    [InlineData(ERedPackPickupStatus.Received)]
-    public async Task GetRedPacksAsync(ERedPackPickupStatus status)
+    /// <summary>
+    /// 红包总额
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task GetRedPackReceivedTotal_Test()
     {
-        var page = await _snapshotApplication.GetRedPacksAsync(new RedPacksInDto { Status = status });
-        page.Total.ShouldNotBe(0, "数据不应该为空");
+        var amount = await _snapshotApplication.GetRedPackReceivedTotalAsync(CancellationToken.None);
     }
 
+    /// <summary>
+    /// 添加志愿者上报信息
+    /// </summary>
+    /// <returns></returns>
     [Fact]
-    public async Task GetBulletinsDetail_Test()
+    public async Task AddVolunteerReport_Test()
     {
-        var detail = await _snapshotApplication.GetBulletinsDetailAsync("08dc788f-20f4-4bf1-83d3-b5a8a4f395b0");
-        detail.Id.ShouldNotBeNullOrEmpty();
-        detail.Title.ShouldNotBeNullOrEmpty();
-        detail.Content.ShouldNotBeNullOrEmpty();
+        await _snapshotApplication.AddVolunteerAsync(new AddVolunteerInDto { Name = _sessionContext.UserName, PhoneNumber = _sessionContext.Phone }, CancellationToken.None);
+        var inDto = _fixture.Create<AddVolunteerReportInDto>();
+        inDto.JobType = "电焊";
+        inDto.PhoneNumber = "13999989" + DateTime.Now.ToString("ss");
+        inDto.Name = "单元测试" + DateTime.Now.ToString("ss");
+        foreach (var item in inDto.Files)
+        {
+            item.FileName = DateTime.Now.ToShortTimeString() + "文件.doc";
+        }
+
+        var result = await _snapshotApplication.AddVolunteerReportAsync(inDto, CancellationToken.None);
+        result.Id.ShouldNotBeNull();
     }
 
+    /// <summary>
+    /// 保存用户邀请码
+    /// </summary>
+    /// <returns></returns>
     [Fact]
-    public async Task InitRedPackDataAsync()
+    public async Task SaveInvitationCode_Test()
     {
-        for (int i = 0;i < 12;i++)
+        var code = new Random().Next(100, 200).ToString();
+        try
         {
-            var now = DateTime.Now;
-            var entity = new RedPackRecord
-            {
-                OrderId = "111111111",
-                Amount = 10 * 1000,
-                CreationTime = new DateTime(2024, i + 1, 02, now.Hour, now.Minute, now.Second),
-                WXOpenId = "测试生成的OpenId",
-                PickupStatus = ERedPackPickupStatus.Received,
-            };
-            await _redPackRecordRepository.AddAsync(entity);
+            await _thirdAccountRepository.Updateable()
+                .SetColumns(m => m.InvitationCode, null)
+                .Where(m => m.OpenId == _sessionContext.OpenId)
+                .ExecuteCommandAsync();
+            await _snapshotApplication.SaveInvitationCodeAsync(new SaveInvitationCodeInDto { InvitationCode = code });
+            var third = await _thirdAccountRepository.GetByOpenIdAsync(_sessionContext.OpenId);
+            third.InvitationCode.ShouldBe(code);
         }
+        catch (Exception e)
+        {
+            var msg = e.Message;
+            throw;
+        }
+    }
+
+    /// <summary>
+    /// 获取志愿者集合
+    /// 获取志愿者详情
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task GetPractitionerItems_Test()
+    {
+        var items = await _snapshotApplication.GetPractitionerItemsAsync(new PractitionerItemInDto { AreaId = "510399" }, CancellationToken.None);
+        items.Count.ShouldNotBe(0);
+
+        var item = await _snapshotApplication.GetPractitionerDetailAsync(items.First().Id, CancellationToken.None);
+        item.Street.ShouldNotBeNullOrEmpty();
+        item.Name.ShouldNotBeNullOrEmpty();
+        item.SystemAreaName.ShouldNotBeNullOrEmpty();
+        item.SystemAreaName.ShouldNotBeNullOrEmpty();
+        item.Gender.ShouldNotBe(EGender.Unknown);
+        item.GenderTxt.ShouldNotBeNullOrEmpty();
+        item.PhoneNumber.ShouldNotBeNullOrEmpty();
+    }
+
+    /// <summary>
+    /// 统计红包数据
+    /// </summary>
+    /// <param name="status"></param>
+    /// <returns></returns>
+    [Theory]
+    [InlineData(ERedPackPickupStatus.Unreceived)]
+    [InlineData(ERedPackPickupStatus.Received)]
+    public async Task GetRedPacksAsync(ERedPackPickupStatus status)
+    {
+        var page = await _snapshotApplication.GetRedPacksAsync(new RedPacksInDto { Status = status }, CancellationToken.None);
+        //page.Count.ShouldNotBe(0, "数据不应该为空");
+    }
+
+    [Fact]
+    public async Task GetBulletinsDetail_Test()
+    {
+        var detail = await _snapshotApplication.GetBulletinsDetailAsync("08dc788f-20f4-4bf1-83d3-b5a8a4f395b0");
+        //detail.Id.ShouldNotBeNullOrEmpty();
+        //detail.Title.ShouldNotBeNullOrEmpty();
+        //detail.Content.ShouldNotBeNullOrEmpty();
     }
 
     /// <summary>
@@ -214,7 +456,7 @@ public class SnapshotApplicationTest : TestBase
                 file.Key.ShouldBe(industryId);
             }
             pageDto.Workplace.ShouldNotBeNull();
-            pageDto.WorkArea.ShouldNotBeNull();
+            pageDto.WorkplaceName.ShouldNotBeNull();
         }
         catch (Exception e)
         {
@@ -226,4 +468,66 @@ public class SnapshotApplicationTest : TestBase
             await _fileRepository.Removeable().Where(m => m.Id == pageDto.Files.First().Id).ExecuteCommandAsync();
         }
     }
+
+    /// <summary>
+    /// 上报线索
+    /// 推送网格员
+    /// 网格员回复
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task Snapshot_Test()
+    {
+        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin)
+            .GetCreateResult();
+        await _snapshotApplication.PostOrderGuiderSystemAsync(order.Id, CancellationToken.None);
+        var orderSnapshot = await _orderSnapshotRepository.GetAsync(order.Id);
+        orderSnapshot.IndustryName = "修改行业名称";
+        await _orderSnapshotRepository.UpdateAsync(orderSnapshot);
+        var industryLog = await _industryLogRepository.Queryable()
+            .Where(m => m.OrderId == order.Id)
+            .FirstAsync();
+        industryLog.ShouldNotBeNull();
+        industryLog.IndustryName.ShouldBe("修改行业名称");
+        await _orderSnapshotRepository.Updateable()
+            .SetColumns(m => m.IndustryName, industryLog.OldIndustryName)
+            .Where(m => m.Id == order.Id)
+            .ExecuteCommandAsync();
+        orderSnapshot = await _orderSnapshotRepository.GetAsync(order.Id);
+        var replyDto = new GuiderSystemInDto
+        {
+            ReplyCode = order.No,
+            AppealNumber = orderSnapshot.NetworkENumber,
+            ReplyDate = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss"),
+            ReplyUserName = "18181552753@zgsg",
+            ReplyBMName = "瓦市村村民委员会",
+            ReplyResultType = "2",
+            ReplyISTrue = "1",
+            IsRepeat = "0",
+            IsHiddenDanger = "1",
+            MemberName = "许利洪",
+            MemberMobile = "18181552753",
+            ReplyContent = "到现场查实,存在安全隐患",
+            OrgId = "4828",
+            OrgName = "瓦市村民委员会" + DateTime.Now.ToString("ss"),
+            OrgFullName = "四川省/自贡市/沿滩区/永安镇/瓦市村村民委员会" + DateTime.Now.ToString("ss"),
+            DepartmentNo = "510311106206",
+            ParentOrgId = "4821",
+            ReplyFileList = new List<string>
+            {
+                "http://10.0.188.11:1234/tqOssManager/getObjectByUri/sichuan/scgrid/jpg/2024/12/5/095020318625.jpg"
+            }
+        };
+        await _snapshotApplication.SaveGuiderSystemReplyAsync(replyDto, CancellationToken.None);
+        var orderReply = await _orderSnapshotRepository.GetByNetworkENumberAsync(replyDto.AppealNumber);
+        orderReply.IsDanger.ShouldBe(true);
+        orderReply.MemberMobile.ShouldBe(replyDto.MemberMobile);
+        orderReply.MemberName.ShouldBe(replyDto.MemberName);
+        orderReply.NetworkRemark.ShouldBe(replyDto.ReplyContent);
+        orderReply.ReplyDate.Value.ToString("yyyy-MM-dd hh:mm:ss").ShouldBe(replyDto.ReplyDate);
+        var community = await _communityInfoRepository.GetAsync(replyDto.OrgId);
+        community.ShouldNotBeNull();
+        community.Name.ShouldBe(replyDto.OrgName);
+        community.FullName.ShouldBe(replyDto.OrgFullName);
+    }
 }

+ 13 - 6
src/Hotline.Application.Tests/Application/SystemSettingCacheManagerTest.cs

@@ -4,6 +4,7 @@ using Hotline.Caching.Services;
 using Hotline.Identity.Accounts;
 using Hotline.Identity.Roles;
 using Hotline.Settings;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.DependencyInjection;
@@ -22,7 +23,7 @@ public class SystemSettingCacheManagerTest : TestBase
     private readonly ISystemDicDataCacheManager _systemDicDataCacheManager;
     private readonly IRepository<SystemSetting> _systemSettingRepository;
 
-    public SystemSettingCacheManagerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, ISystemSettingCacheManager systemSettingCacheManager, ISystemDicDataCacheManager systemDicDataCacheManager, IRepository<SystemSetting> systemSettingRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor)
+    public SystemSettingCacheManagerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, ISystemSettingCacheManager systemSettingCacheManager, ISystemDicDataCacheManager systemDicDataCacheManager, IRepository<SystemSetting> systemSettingRepository, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount)
     {
         _systemSettingCacheManager = systemSettingCacheManager;
         _systemDicDataCacheManager = systemDicDataCacheManager;
@@ -34,8 +35,8 @@ public class SystemSettingCacheManagerTest : TestBase
     {
         var dd = DateTime.Parse("11/19/2024 18:08:00");
         _systemSettingCacheManager.CallSyncUnPushDateTime.ShouldBe(DateTime.Parse("2024/11/19 18:08:00"));
-        var result = _systemSettingCacheManager.CancelPublishOrderEnabled;
-        result.ShouldBeTrue();
+        //var result = _systemSettingCacheManager.CancelPublishOrderEnabled;
+        //result.ShouldBeTrue();
         var seconds = _systemSettingCacheManager.VisitCallDelaySecond;
         seconds.ShouldBe(60);
 
@@ -50,15 +51,21 @@ public class SystemSettingCacheManagerTest : TestBase
         _systemSettingCacheManager.WxOpenAppSecret.ShouldNotBeNull();
         _systemSettingCacheManager.VisitCallDelaySecond.ShouldNotBe(0);
         _systemSettingCacheManager.AutomaticPublishOrder.ShouldBe(true);
-        _systemSettingCacheManager.CancelPublishOrderEnabled.ShouldBe(true);
+        //_systemSettingCacheManager.CancelPublishOrderEnabled.ShouldBe(true);
 
+        _systemDicDataCacheManager.RemoveSysDicDataCache(SysDicTypeConsts.Workplace);
         var workplace = _systemDicDataCacheManager.Workplace;
         workplace.ShouldNotBeNull();
         workplace.Count.ShouldNotBe(0);
 
-        _systemDicDataCacheManager.RemoveSysDicDataCache(SysDicTypeConsts.WorkArea);
-        var workArea = _systemDicDataCacheManager.WorkArea;
+        _systemDicDataCacheManager.RemoveSysDicDataCache(SysDicTypeConsts.WorkplaceName);
+        var workArea = _systemDicDataCacheManager.WorkplaceName;
         workArea.ShouldNotBeNull();
         workArea.Count.ShouldNotBe(0);
+
+        _systemDicDataCacheManager.RemoveSysDicDataCache(SysDicTypeConsts.SnapshotBulletinType);
+        var snapshotBulletinType = _systemDicDataCacheManager.SnapshotBulletinType;
+        snapshotBulletinType.ShouldNotBeNull();
+        snapshotBulletinType.Count.ShouldNotBe(0);
     }
 }

+ 40 - 0
src/Hotline.Application.Tests/Application/ThirdIdentifyApplicationTest.cs

@@ -0,0 +1,40 @@
+using NETCore.Encrypt.Internal;
+using NETCore.Encrypt;
+using Org.BouncyCastle.Asn1.IsisMtt.X509;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NETCore.Encrypt.Extensions;
+using Hotline.Share.Tools;
+using System.Security.Cryptography;
+
+namespace Hotline.Application.Tests.Application;
+public class ThirdIdentifyApplicationTest
+{
+    [Fact]
+    public async Task ThirdSystem_Test()
+    {
+        var appId = "companyName";
+        var secret = "4x1q6YCWLDnHkpLTCWMwx3XQF7bA5QAd";
+
+
+        var iv = Guid.NewGuid().ToString().Substring(0, 16);
+
+
+        var unixTimespan = DateTimeOffset.Now.ToUnixTimeSeconds();
+        var strString = appId + unixTimespan;
+        var entrypted = EncryptProvider.AESEncrypt(strString, secret, iv);
+        var token = appId + iv + entrypted;
+
+        var decrypted = EncryptProvider.AESDecrypt(entrypted, secret, iv);
+        Console.WriteLine(decrypted);
+
+
+                                                        //"4x1q6YCWLDnHkpLTCWMwx3XQF7bA5QAd   WD7MEjbCySsniwKz
+        entrypted = EncryptProvider.AESEncrypt(strString, "NBdabUfdsabwB7382fdsab18v321udab");
+        decrypted = EncryptProvider.AESDecrypt(entrypted, "NBdabUfdsabwB7382fdsab18v321udab");
+        Console.WriteLine(decrypted);
+    }
+}

+ 9 - 10
src/Hotline.Application.Tests/Controller/DefaultSessionContext.cs

@@ -30,15 +30,14 @@ public class DefaultSessionContext : ISessionContext, IScopeDependency
     }
     public HttpContext? HttpContext { get => GetContext(); set => _content = value; }
 
-
-    public string? OpenId { get; init; }
+    public string? OpenId { get { return _contextAccessor.HttpContext.User.FindFirstValue(AppClaimTypes.OpenId); } init { } }
 
     /// <summary>
     /// Id of current tenant or null for host
     /// </summary>
     public string? UserId
     {
-        get { return _contextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); }
+        get { return _contextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); }
         init { }
     }
 
@@ -49,7 +48,7 @@ public class DefaultSessionContext : ISessionContext, IScopeDependency
     public string RequiredUserId => _contextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
     public string? UserName
     {
-        get { return _contextAccessor.HttpContext.User.FindFirstValue(AppClaimTypes.UserDisplayName); }
+        get { return _contextAccessor.HttpContext?.User.FindFirstValue(AppClaimTypes.UserDisplayName); }
         init { }
     }
 
@@ -64,26 +63,26 @@ public class DefaultSessionContext : ISessionContext, IScopeDependency
     /// </summary>
     public string[] Roles
     {
-        get { return _contextAccessor.HttpContext.User.Claims.Where(d => d.Type == ClaimTypes.Role).Select(d => d.Value).ToArray(); }
+        get { return _contextAccessor.HttpContext?.User.Claims.Where(d => d.Type == ClaimTypes.Role).Select(d => d.Value).ToArray(); }
         init { }
     }
 
     public string? OrgId
     {
-        get { return _contextAccessor.HttpContext.User.FindFirstValue(AppClaimTypes.DepartmentId); }
+        get { return _contextAccessor.HttpContext?.User.FindFirstValue(AppClaimTypes.DepartmentId); }
         init { }
     }
 
-    public string RequiredOrgId => _contextAccessor.HttpContext.User.FindFirstValue(AppClaimTypes.DepartmentId);
+    public string RequiredOrgId => _contextAccessor.HttpContext?.User.FindFirstValue(AppClaimTypes.DepartmentId);
     public string? OrgName
     {
-        get { return _contextAccessor.HttpContext.User.FindFirstValue(AppClaimTypes.DepartmentName); }
+        get { return _contextAccessor.HttpContext?.User.FindFirstValue(AppClaimTypes.DepartmentName); }
         init { }
     }
 
     public int OrgLevel
     {
-        get { return _contextAccessor.HttpContext.User.FindIntValue(AppClaimTypes.DepartmentLevel); }
+        get { return _contextAccessor.HttpContext?.User.FindIntValue(AppClaimTypes.DepartmentLevel) ?? 0; }
         init { }
     }
 
@@ -110,7 +109,7 @@ public class DefaultSessionContext : ISessionContext, IScopeDependency
 
     public string? AreaId
     {
-        get { return _contextAccessor.HttpContext.User.FindFirstValue(AppClaimTypes.AreaId); }
+        get { return _contextAccessor.HttpContext?.User.FindFirstValue(AppClaimTypes.AreaId); }
         init { }
     }
     public string? ClientId

+ 37 - 0
src/Hotline.Application.Tests/Controller/IndustryControllerTest.cs

@@ -0,0 +1,37 @@
+using Hotline.Api.Controllers;
+using Hotline.Api.Controllers.Snapshot;
+using Hotline.Identity.Accounts;
+using Hotline.Identity.Roles;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Users;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Tests.Controller;
+public class IndustryControllerTest : TestBase
+{
+    private readonly IndustryController _industryController;
+    public IndustryControllerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdIdentiyService, IThirdAccountRepository thirdAccountRepository, IndustryController industryController) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdIdentiyService, thirdAccountRepository)
+    {
+        _industryController = industryController;
+        _industryController.ControllerContext = new ControllerContext
+        {
+            HttpContext = new DefaultHttpContext()
+        };
+    }
+
+    [Fact]
+    public async Task GetBaseData_Test()
+    {
+        var result = await _industryController.GetBaseData();
+        result.Count.ShouldNotBe(0);
+    }
+}

+ 2 - 1
src/Hotline.Application.Tests/Controller/KnowledgeControllerTest.cs

@@ -8,6 +8,7 @@ using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Knowledge;
 using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.KnowledgeBase;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -27,7 +28,7 @@ public class KnowledgeControllerTest : TestBase
     private readonly KnowledgeController _knowledgeController;
     private readonly IRepository<KnowledgeBase.Knowledge> _knowledgeRepository;
 
-    public KnowledgeControllerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, KnowledgeServiceMock knowledgeServiceMock, KnowledgeController knowledgeController, IRepository<KnowledgeBase.Knowledge> knowledgeRepository, IHttpContextAccessor httpContextAccessor) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor)
+    public KnowledgeControllerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, KnowledgeServiceMock knowledgeServiceMock, KnowledgeController knowledgeController, IRepository<KnowledgeBase.Knowledge> knowledgeRepository, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount)
     {
         _knowledgeServiceMock = knowledgeServiceMock;
         _knowledgeController = knowledgeController;

+ 72 - 3
src/Hotline.Application.Tests/Controller/OrderControllerTest.cs

@@ -15,6 +15,7 @@ using Hotline.Identity.Accounts;
 using Hotline.Identity.Roles;
 using Hotline.Orders;
 using Hotline.Repository.SqlSugar;
+using Hotline.Repository.SqlSugar.Snapshot;
 using Hotline.Settings;
 using Hotline.Settings.Hotspots;
 using Hotline.Share.Dtos;
@@ -23,6 +24,7 @@ using Hotline.Share.Dtos.File;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Dtos.Order.Publish;
+using Hotline.Share.Dtos.Snapshot;
 using Hotline.Share.Dtos.TrCallCenter;
 using Hotline.Share.Dtos.Users;
 using Hotline.Share.Enums.CallCenter;
@@ -30,6 +32,7 @@ using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Settings;
 using Hotline.Share.Tools;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using Mapster;
 using MediatR;
@@ -67,10 +70,23 @@ public class OrderControllerTest : TestBase
     private readonly IRepository<CallidRelation> _callIdRelationRepository;
     private readonly XingTangCallApplication _defaultCallApplication;
     private readonly ISqlSugarClient _capSqlClient;
+    private readonly IIndustryRepository _industryRepository;
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
     private readonly ISystemLogRepository _systemLogRepository;
     private readonly IOrderVisitDomainService _orderVisitDomainService;
 
-    public OrderControllerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IRepository<Hotspot> hotspotRepository, OrderController orderController, IOrderRepository orderRepository, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, OrderServiceMock orderServiceMock, IRepository<OrderPublish> orderPublishRepository, INotificationHandler<EndWorkflowNotify> orderPublishEndWorkflowHandler, IOrderVisitRepository orderVisitRepository, IRepository<SystemSetting> systemSettingRepository, ISystemSettingCacheManager systemSettingCacheManager, IRepository<CallNative> callNativeRepository, IRepository<CallidRelation> callIdRelationRepository, XingTangCallApplication defaultCallApplication, ISugarUnitOfWork<CapDbContext> capDbContext, IHttpContextAccessor httpContextAccessor, ISystemLogRepository systemLogRepository, IOrderVisitDomainService orderVisitDomainService, IRepository<OrderVisitDetail> orderVisitDetailRepository) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor)
+    public OrderControllerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository,
+        UserController userController, IRepository<Hotspot> hotspotRepository, OrderController orderController, 
+        IOrderRepository orderRepository, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, 
+        OrderServiceMock orderServiceMock, IRepository<OrderPublish> orderPublishRepository,
+        INotificationHandler<EndWorkflowNotify> orderPublishEndWorkflowHandler,
+        IOrderVisitRepository orderVisitRepository, IRepository<SystemSetting> systemSettingRepository,
+        ISystemSettingCacheManager systemSettingCacheManager, IRepository<CallNative> callNativeRepository,
+        IRepository<CallidRelation> callIdRelationRepository, XingTangCallApplication defaultCallApplication,
+        ISugarUnitOfWork<CapDbContext> capDbContext, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdService,
+        IThirdAccountRepository thirdAccount, IIndustryRepository industryRepository, IOrderSnapshotRepository orderSnapshotRepository,
+        ISystemLogRepository systemLogRepository, IOrderVisitDomainService orderVisitDomainService, IRepository<OrderVisitDetail> orderVisitDetailRepository)
+        : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount)
     {
         _hotspotRepository = hotspotRepository;
         _orderController = orderController;
@@ -89,6 +105,8 @@ public class OrderControllerTest : TestBase
         _callIdRelationRepository = callIdRelationRepository;
         _defaultCallApplication = defaultCallApplication;
         _capSqlClient = capDbContext.Db;
+        _industryRepository = industryRepository;
+        _orderSnapshotRepository = orderSnapshotRepository;
         _systemLogRepository = systemLogRepository;
         _orderVisitDomainService = orderVisitDomainService;
         _orderVisitDetailRepository = orderVisitDetailRepository;
@@ -110,7 +128,7 @@ public class OrderControllerTest : TestBase
             .With(m => m.Id, Ulid.NewUlid().ToString())
             .With(m => m.CallNo, callNo)
             .With(m => m.Direction, ECallDirection.In)
-            .With(m => m.IsDeleted , false)
+            .With(m => m.IsDeleted, false)
             .Create();
         await _callNativeRepository.AddAsync(inDto);
 
@@ -119,7 +137,7 @@ public class OrderControllerTest : TestBase
             .With(m => m.Direction, ECallDirection.Out)
             .With(m => m.CallNo, callNo)
             .With(m => m.AudioFile, string.Empty)
-            .With(m => m.IsDeleted , false)
+            .With(m => m.IsDeleted, false)
             .Create();
         await _callNativeRepository.AddAsync(inDto2);
         var callOrder = new CallidRelation
@@ -218,6 +236,36 @@ public class OrderControllerTest : TestBase
         order.ShouldNotBeNull();
     }
 
+    /// <summary>
+    /// 创建随手拍工单
+    /// 验证随手拍工单是否能正常创建
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task OrderSnapshot_Test()
+    {
+        var industryItems = await _industryRepository.Queryable() .Select(d => new { d.Id, d.Name, }) .ToListAsync();
+        var industry = industryItems.First();
+
+        SetZuoXi();
+        var order = _orderServiceMock.CreateOrder(industryId:industry.Id, industryName: industry.Name)
+            .办理到派单员()
+            .办理到归档(SetPaiDanYuan, data =>
+            {
+                data.IsEvasive = true;
+                data.IsInactively = true;
+            })
+            .GetCreateResult();
+        var orderEntity = await _orderRepository.GetAsync(order.Id);
+        orderEntity.ShouldNotBeNull();
+
+        var snapshot = await _orderSnapshotRepository.GetAsync(m => m.Id == order.Id);
+        snapshot.ShouldNotBeNull();
+        snapshot.IndustryId.ShouldBe(industry.Id);
+        snapshot.IndustryName.ShouldBe(industry.Name);
+    }
+
+
     /// <summary>
     /// 验证中心直办工单归档后 自动发布
     /// 是否推诿, 是否不积极
@@ -380,4 +428,25 @@ public class OrderControllerTest : TestBase
         published.ShouldNotBeNull();
         published.IsDeleted.ShouldBeFalse();
     }
+
+    /// <summary>
+    /// 工单批量标注
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task OrderSignBath_Test()
+    {
+        var inDto = new OrderSignBathInDto
+        {
+            IsSafetyDepartment = true,
+            Remark = "单元测试标注",
+            OrderIds = new List<string>()
+        };
+        inDto.OrderIds.Add(_orderServiceMock.CreateSnapshotOrder(SetWeiXin).办理到工单标注(SetZuoXi).GetCreateResult().Id);
+        inDto.OrderIds.Add(_orderServiceMock.CreateSnapshotOrder(SetWeiXin).办理到工单标注(SetZuoXi).GetCreateResult().Id);
+        Set班长();
+        await _orderController.GetNextStepsWithRecommend(inDto.OrderIds.First());
+        var result = await _orderController.OrderSignBathAsync(inDto);
+        result.Contains("标注完成").ShouldBe(true);
+    }
 }

+ 54 - 3
src/Hotline.Application.Tests/Controller/SnapshotControllerTest.cs

@@ -1,9 +1,16 @@
-using Hotline.Api.Controllers;
+using AutoFixture;
+using Hotline.Api.Controllers;
 using Hotline.Api.Controllers.Snapshot;
 using Hotline.Identity.Accounts;
 using Hotline.Identity.Roles;
+using Hotline.Orders;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Enums.Snapshot;
+using Hotline.Share.Tools;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using Shouldly;
 using System;
@@ -17,13 +24,23 @@ namespace Hotline.Application.Tests.Controller;
 public class SnapshotControllerTest : TestBase
 {
     private readonly SnapshotController _snapshotController;
-    public SnapshotControllerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, SnapshotController snapshotController) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor)
+    private readonly IOrderRepository _orderRepository;
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
+    private readonly IIndustryRepository _industryRepository;
+    public SnapshotControllerTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, SnapshotController snapshotController, IOrderRepository orderRepository, IOrderSnapshotRepository orderSnapshotRepository, IIndustryRepository industryRepository, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount)
     {
         _snapshotController = snapshotController;
+        _snapshotController.ControllerContext = new ControllerContext
+        {
+            HttpContext = new DefaultHttpContext()
+        };
+        _orderRepository = orderRepository;
+        _orderSnapshotRepository = orderSnapshotRepository;
+        _industryRepository = industryRepository;
     }
 
     [Fact]
-    public async Task GetAreaTreeTest()
+    public async Task GetAreaTree_Test()
     {
         var result = await _snapshotController.GetAreaTreeAsync();
         result.ShouldNotBeNull();
@@ -35,4 +52,38 @@ public class SnapshotControllerTest : TestBase
         zzz.ShouldNotBeNull();
     }
 
+    /// <summary>
+    /// 测试创建随手拍工单
+    /// </summary>
+    /// <returns></returns>
+    [Fact]
+    public async Task AddOrder_Test()
+    {
+        var homePage = await _snapshotController.GetHomePageAsync();
+        var industry = homePage.Industrys.Where(m => m.IndustryType == EIndustryType.Declare).FirstOrDefault();
+        var pageBase = await _snapshotController.GetIndustryBaseAsync(industry.Id);
+
+        var inDto = _fixture.Create<AddSnapshotOrderInDto>();
+        inDto.Street = "单元测试街道" + DateTime.Now.ToLongDateTimeString();
+        inDto.IndustryId = industry.Id;
+        inDto.Town = "仙市镇";
+        inDto.County = "沿滩区";
+        inDto.Description = "单元测试添加的时间描述";
+        inDto.IsSecret = false;
+        //inDto.JobType = 
+        foreach (var item in inDto.Files)
+        {
+            item.FileName = DateTime.Now.ToShortTimeString() + "文件.doc";
+        }
+
+        var order = await _snapshotController.AddOrderAsync(inDto);
+        var orderEntity = await _orderRepository.GetAsync(order.Id);
+        orderEntity.ShouldNotBeNull();
+        orderEntity.Latitude.ShouldBe(inDto.Latitude);
+        orderEntity.Longitude.ShouldBe(inDto.Longitude);
+        orderEntity.Title.ShouldNotBeNullOrEmpty();
+        orderEntity.Content.ShouldNotBeNullOrEmpty();
+        var orderSnapshotEntity = await _orderSnapshotRepository.GetAsync(order.Id);
+        orderSnapshotEntity.ShouldNotBeNull();
+    }
 }

+ 2 - 1
src/Hotline.Application.Tests/Domain/OrderVisitDomainServiceTest.cs

@@ -11,6 +11,7 @@ using Hotline.Share.Dtos.Push;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Push;
 using Hotline.Share.Tools;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using Mapster;
 using Microsoft.AspNetCore.Http;
@@ -28,7 +29,7 @@ public class OrderVisitDomainServiceTest : TestBase
     private readonly IOrderRepository _orderRepository;
     private readonly OrderServiceMock _orderServiceMock;
 
-    public OrderVisitDomainServiceTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IOrderVisitDomainService orderVisitDomainService, IOrderVisitRepository orderVisitRepository, IRepository<OrderVisitDetail> orderVisitDetailRepository, Publisher publisher, IOrderRepository orderRepository, OrderServiceMock orderServiceMock, IHttpContextAccessor httpContextAccessor) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor)
+    public OrderVisitDomainServiceTest(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IOrderVisitDomainService orderVisitDomainService, IOrderVisitRepository orderVisitRepository, IRepository<OrderVisitDetail> orderVisitDetailRepository, Publisher publisher, IOrderRepository orderRepository, OrderServiceMock orderServiceMock, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdService, IThirdAccountRepository thirdAccount) : base(accountRepository, roleRepository, userController, scopeFactory, userRepository, httpContextAccessor, thirdService, thirdAccount)
     {
         _orderVisitDomainService = orderVisitDomainService;
         _orderVisitRepository = orderVisitRepository;

+ 2 - 1
src/Hotline.Application.Tests/Hotline.Application.Tests.csproj

@@ -26,6 +26,7 @@
     <PackageReference Include="coverlet.collector" Version="3.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.20" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
+    <PackageReference Include="NETCore.Encrypt" Version="2.1.1" />
     <PackageReference Include="Senparc.Weixin" Version="6.19.1" />
     <PackageReference Include="Senparc.Weixin.AspNet" Version="1.3.1" />
     <PackageReference Include="Senparc.Weixin.WxOpen" Version="3.20.1" />
@@ -41,8 +42,8 @@
     <ProjectReference Include="..\Hotline.Api\Hotline.Api.csproj" />
     <ProjectReference Include="..\Hotline.Application\Hotline.Application.csproj" />
     <ProjectReference Include="..\Hotline.Repository.SqlSugar\Hotline.Repository.SqlSugar.csproj" />
+    <ProjectReference Include="..\TianQue.Sdk\TianQue.Sdk.csproj" />
     <ProjectReference Include="..\Tr.Sdk\Tr.Sdk.csproj" />
-    <ProjectReference Include="..\Hotline.WeChat\Hotline.WeChat.csproj" />
     <ProjectReference Include="..\XF.Domain.Repository\XF.Domain.Repository.csproj" />
     <ProjectReference Include="..\XF.Domain\XF.Domain.csproj" />
   </ItemGroup>

+ 3 - 0
src/Hotline.Application.Tests/Infrastructure/TestSettingConstants.cs

@@ -7,11 +7,14 @@ using System.Threading.Tasks;
 namespace Hotline.Application.Tests.Infrastructure;
 public static class TestSettingConstants
 {
+    public const string WeiXinAccountName = "UnitTestWeiXin";
     public const string ZuoXiAccountName = "UnitTestZuoXi";
     public const string PaiDanYuanAccountName = "UnitTestPDY";
     public const string FirstOrgAccountName = "cs";
     public const string SecondOrgAccountName = "cs21";
     public const string BanZhangAccountName = "UnitTestBZ";
+
+    public const string GuiderAccountName = "UnitTestGuider";
 }
 
 public static class TestSessionConstants

+ 50 - 0
src/Hotline.Application.Tests/Infrastructure/TianQueTest.cs

@@ -0,0 +1,50 @@
+using Hotline.File;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using TianQue.Sdk;
+
+namespace Hotline.Application.Tests.Infrastructure;
+public class TianQueTest
+{
+    private readonly IFileDomainService _fileDomainService;
+
+    public TianQueTest(IFileDomainService fileDomainService)
+    {
+        _fileDomainService = fileDomainService;
+    }
+
+    [Fact]
+    public void Test_GenerateNonce()
+    {
+        // Arrange
+        var nonce = SignUtils.GenerateNonce();
+
+        // Assert
+        Assert.NotNull(nonce);
+        Assert.Equal(64, nonce.Length);
+        // b55dfdedba900437d486e70e5fb78ed50afaeb910a2346f16ef03af656f8bb0b
+    }
+
+    [Fact]
+    public async Task PostAcceptInfo_Test()
+    {
+        // Arrange
+        //var tiqnQueService = new TiqnQueService();
+
+        //// Act
+        //var result = await tiqnQueService.PostAcceptInfo();
+
+        //// Assert
+        //Assert.Equal("ok", result);
+    }
+
+    [Fact]
+    public async Task GetFile_Test()
+    { 
+        //await _fileDomainService.GetNetworkFileAsync("http://10.0.188.11:1234/tqOssManager/getObjectByUri/sichuan/scgrid/jpg/2024/12/5/095020318625.jpg", "");
+    }
+
+}

+ 24 - 0
src/Hotline.Application.Tests/Infrastructure/WeiXinTest.cs

@@ -0,0 +1,24 @@
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.WeChat;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Application.Tests.Infrastructure;
+public class WeiXinTest
+{
+    private readonly WeChatService _weChatService;
+
+    public WeiXinTest(WeChatService weChatService)
+    {
+        _weChatService = weChatService;
+    }
+
+    // [Fact]
+    public async Task GetPhoneNumber_Test()
+    {
+        var result = await _weChatService.GetPhoneNumberAsync(new ThirdTokenDto { AppId = "12333", Secret = "4444444" });
+    }
+}

+ 19 - 0
src/Hotline.Application.Tests/Mock/Interfaces/IOrderServiceStartWorkflow.cs

@@ -0,0 +1,19 @@
+using Hotline.Application.Tests.Dto;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Application.Tests.Mock.Interfaces;
+
+
+public interface IOrderServiceStartWorkflow
+{
+    OrderServiceMock orderServiceMock { set;}
+
+    OrderServiceMock 办理到派单员(Action action = null);
+    OrderServiceMock 办理到工单标注(Action action = null);
+    CreateOrderOutDto GetCreateResult();
+    OrderServiceMock 办理到一级部门(Action action = null);
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 97 - 19
src/Hotline.Application.Tests/Mock/OrderServiceMock.cs


+ 145 - 0
src/Hotline.Application.Tests/Mock/OrderServiceStartWorkflow.cs

@@ -0,0 +1,145 @@
+using AutoFixture;
+using Hotline.Api.Controllers;
+using Hotline.Api.Controllers.Snapshot;
+using Hotline.Application.Tests.Dto;
+using Hotline.Application.Tests.Mock.Interfaces;
+using Hotline.Share.Dtos.FlowEngine;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.FlowEngine;
+using Hotline.Share.Tools;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Authentications;
+
+namespace Hotline.Application.Tests.Mock;
+public class OrderServiceStartWorkflow : IOrderServiceStartWorkflow
+{
+    public readonly IFixture _fixture;
+    public readonly ISessionContext _sessionContext;
+    private readonly OrderController _orderController;
+    private readonly SnapshotController _snapshotController;
+    public OrderServiceMock _orderServiceMock;
+
+    public OrderServiceStartWorkflow(ISessionContext sessionContext, OrderController orderController, SnapshotController snapshotController)
+    {
+        _orderController = orderController;
+        _orderController.ControllerContext = new ControllerContext
+        {
+            HttpContext = new DefaultHttpContext()
+        };
+        _snapshotController = snapshotController;
+        _snapshotController.ControllerContext = new ControllerContext
+        {
+            HttpContext = new DefaultHttpContext()
+        };
+        _fixture = new Fixture();
+
+
+        _sessionContext = sessionContext;
+    }
+
+    public OrderServiceMock orderServiceMock { set => _orderServiceMock = value; }
+
+    public CreateOrderOutDto GetCreateResult()
+    {
+        return _orderServiceMock.CreateOrderOutDto;
+    }
+
+    public OrderServiceMock 办理到一级部门(Action action = null)
+    {
+        action?.Invoke();
+        var stepNextInfo = _orderController.GetFlowStartOptions(_orderServiceMock.CreateOrderOutDto.Id).GetAwaiter().GetResult().ToJson().FromJson<NextStepsDto<NextStepOption>>();
+        var stepInfo = stepNextInfo.Steps.FirstOrDefault(m => m.Value == "一级部门");
+        var stepOrg = stepInfo.Items.FirstOrDefault(m => m.OrgName == "测试部门");
+
+        var handleDto = new StartWorkflowDto<OrderHandleFlowDto>
+        {
+            Data = new OrderHandleFlowDto
+            {
+                OrderId = _orderServiceMock.CreateOrderOutDto.Id,
+            },
+            Workflow = new BasicWorkflowDto
+            {
+                NextHandlers = [stepOrg],
+                NextStepCode = stepInfo.Key,
+                NextStepName = stepInfo.Value,
+                Opinion = "办理意见",
+                BackToCountersignEnd = false,
+                IsSms = false,
+                IsForwarded = false,
+                HandlerType = EHandlerType.OrgLevel,
+                BusinessType = EBusinessType.Department,
+            }
+        };
+        _orderController.StartFlow(handleDto).GetAwaiter().GetResult();
+        return _orderServiceMock;
+    }
+
+    public OrderServiceMock 办理到工单标注(Action action = null)
+    {
+        action?.Invoke();
+        var stepNextInfo = _orderController.GetFlowStartOptions(_orderServiceMock.CreateOrderOutDto.Id).GetAwaiter().GetResult().ToJson().FromJson<NextStepsDto<NextStepOption>>();
+        var stepInfo = stepNextInfo.Steps.FirstOrDefault(m => m.Value == "工单标记");
+        var stepOrg = stepInfo.Items.FirstOrDefault(m => m.Username == "单元测试班长");
+
+        var handleDto = new StartWorkflowDto<OrderHandleFlowDto>
+        {
+            Data = new OrderHandleFlowDto
+            {
+                OrderId = _orderServiceMock.CreateOrderOutDto.Id,
+                
+            },
+            Workflow = new BasicWorkflowDto
+            {
+                NextHandlers = [stepOrg],
+                NextStepCode = stepInfo.Key,
+                NextStepName = stepInfo.Value,
+                Opinion = "办理到派单组意见",
+                BackToCountersignEnd = false,
+                IsSms = false,
+                IsForwarded = false,
+                HandlerType = EHandlerType.OrgLevel,
+                BusinessType = EBusinessType.Send,
+                FlowDirection = EFlowDirection.CenterToCenter,
+            }
+        };
+        _orderController.StartFlow(handleDto).GetAwaiter().GetResult();
+        return _orderServiceMock;
+    }
+
+    public OrderServiceMock 办理到派单员(Action action = null)
+    {
+        action?.Invoke();
+        var stepNextInfo = _orderController.GetFlowStartOptions(_orderServiceMock.CreateOrderOutDto.Id).GetAwaiter().GetResult().ToJson().FromJson<NextStepsDto<NextStepOption>>();
+        var stepInfo = stepNextInfo.Steps.FirstOrDefault(m => m.Value == "派单组");
+        var stepOrg = stepInfo.Items.FirstOrDefault(m => m.Username == "单元测试派单员");
+
+        var handleDto = new StartWorkflowDto<OrderHandleFlowDto>
+        {
+            Data = new OrderHandleFlowDto
+            {
+                OrderId = _orderServiceMock.CreateOrderOutDto.Id,
+            },
+            Workflow = new BasicWorkflowDto
+            {
+                NextHandlers = [stepOrg],
+                NextStepCode = stepInfo.Key,
+                NextStepName = stepInfo.Value,
+                Opinion = "办理到派单组意见",
+                BackToCountersignEnd = false,
+                IsSms = false,
+                IsForwarded = false,
+                HandlerType = EHandlerType.OrgLevel,
+                BusinessType = EBusinessType.Send,
+                FlowDirection = EFlowDirection.CenterToCenter,
+            }
+        };
+        _orderController.StartFlow(handleDto).GetAwaiter().GetResult();
+        return _orderServiceMock;
+    }
+}

+ 6 - 2
src/Hotline.Application.Tests/Mock/ThirdTestService.cs

@@ -1,19 +1,23 @@
 using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Tools;
 using Hotline.Users;
+using XF.Domain.Dependency;
 
 namespace Hotline.Application.Tests.Mock;
-public class ThirdTestService : IThirdIdentiyService
+public class ThirdTestService : IThirdIdentiyService, IScopeDependency
 {
     public async Task<ThirdPhoneOutDto> GetPhoneNumberAsync(ThirdTokenDto dto)
     {
         return new ThirdPhoneOutDto
         {
-            PhoneNumber = "13800138000"
+            PhoneNumber = "138001389877"
         };
     }
 
     public async Task<ThirdTokenOutDto> GetTokenAsync(ThirdTokenDto dto)
     {
+        //var resultString = "{\"SessionKey\":\"letVB1m+8ZYsMjo8PxhwUw==\",\"OpenId\":\"oHBwj4_8hQG1Q00HyTO3d47RLuAA\"}";
+        //return resultString.FromJson<ThirdTokenOutDto>();
         return new ThirdTokenOutDto
         {
             SessionKey = "sessionKeyfjdklsafjdskla",

+ 0 - 10
src/Hotline.Application.Tests/Repository/FWMQRepositoryTest.cs

@@ -1,10 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Hotline.Application.Tests.Repository;
-internal class FWMQRepositoryTest
-{
-}

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

@@ -59,6 +59,11 @@ using Hotline.Application.Tests.SqlSuger;
 using Microsoft.AspNetCore.Http;
 using Hotline.WeChat;
 using Hotline.Api.Controllers.Snapshot;
+using Hotline.Snapshot.Interfaces;
+using TianQue.Sdk;
+using Hotline.Snapshot.Notifications;
+using Hotline.File;
+using Hotline.Application.Tests.Mock.Interfaces;
 using Hotline.Orders.DatabaseEventHandler;
 using Hotline.Orders;
 using XF.Domain.Repository.Events;
@@ -82,7 +87,7 @@ public class Startup
             {
                 config.AddJsonFile(JsonFile);
             })
-            .UseStartup<AspNetCoreStartup>()) ;
+            .UseStartup<AspNetCoreStartup>());
     }
 
     private class AspNetCoreStartup
@@ -94,7 +99,7 @@ public class Startup
 
         public IConfiguration Configuration { get; }
 
-        public void ConfigureServices(IServiceCollection services) 
+        public void ConfigureServices(IServiceCollection services)
         {
             AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
             var configuration = Configuration;
@@ -105,11 +110,11 @@ public class Startup
             var callCenterConfigurationSection = configuration.GetRequiredSection(nameof(CallCenterConfiguration));
             var callCenterConfiguration = callCenterConfigurationSection.Get<CallCenterConfiguration>();
 
-			services.Configure<AppConfiguration>(d => appConfigurationSection.Bind(d));
+            services.Configure<AppConfiguration>(d => appConfigurationSection.Bind(d));
             services.Configure<IdentityConfiguration>(d => configuration.GetSection(nameof(IdentityConfiguration)).Bind(d));
             services.Configure<CityBaseConfiguration>(d => configuration.GetSection(nameof(CityBaseConfiguration)).Bind(d));
 
-			services.RegisterMapper();
+            services.RegisterMapper();
             //services.AddControllers()
             //    .AddControllersAsServices();
 
@@ -117,8 +122,6 @@ public class Startup
             services.AddSqlSugar(configuration);
             services.AddCAPDb(configuration);
 
-            // application services
-            services.AddScoped<ISnapshotApplication, ZiGongSnapshotApplication>();
 
             //mq
             services.AddTestMq(configuration);
@@ -161,6 +164,7 @@ public class Startup
 
             //services.AddScoped<IThirdAccountRepository, ThirdAccountRepository>();
             services.AddApplication();
+            services.AddScoped<IOrderServiceStartWorkflow, OrderServiceStartWorkflow>();
             services.AddScoped<IExpireTimeSupplier, DaySupplier>();
             services.AddScoped<IExpireTimeSupplier, WorkDaySupplier>();
             services.AddScoped<IExpireTimeSupplier, HourSupplier>();
@@ -173,6 +177,7 @@ public class Startup
             services.AddScoped<IMediator, MediatorMock>();
             services.AddScoped<IExportApplication, ExportApplication>();
             services.AddScoped<OrderController>();
+            services.AddScoped<IndustryController>();
             services.AddScoped<UserController>();
             services.AddScoped<SnapshotController>();
             services.AddScoped<KnowledgeController>();
@@ -185,7 +190,10 @@ public class Startup
             services.AddScoped<OrderServiceMock>();
             services.AddScoped<KnowledgeServiceMock>();
             services.AddScoped<XingTangCallsSyncJob>();
+            services.AddScoped<IFileDomainService, FileDomainService>();
             services.AddXingTangDb(callCenterConfiguration.XingTang);
+            services.AddScoped<IGuiderSystemService, TiqnQueService>();
+
             //ServiceLocator.Instance = services.BuildServiceProvider();
         }
 

+ 39 - 18
src/Hotline.Application.Tests/TestBase.cs

@@ -7,6 +7,7 @@ using Hotline.Settings;
 using Hotline.Share.Dtos.Users;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.User;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using IdentityModel;
 using Microsoft.AspNetCore.Http;
@@ -26,10 +27,14 @@ public class TestBase
     public readonly IFixture _fixture;
     private readonly IServiceScopeFactory _scopeFactory;
     public readonly IHttpContextAccessor _httpContextAccessor;
+    public readonly IThirdIdentiyService _thirdIdentiyService;
+    public readonly IThirdAccountRepository _thirdAccountRepository;
 
-    public TestBase(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor)
-    {
 
+    public TestBase(IAccountRepository accountRepository, IRepository<Role> roleRepository, UserController userController, IServiceScopeFactory scopeFactory, IRepository<User> userRepository, IHttpContextAccessor httpContextAccessor, IThirdIdentiyService thirdIdentiyService, IThirdAccountRepository thirdAccountRepository)
+    {
+        _thirdAccountRepository = thirdAccountRepository;
+        _thirdIdentiyService = thirdIdentiyService;
         _fixture = new Fixture();
         _accountRepository = accountRepository;
         _roleRepository = roleRepository;
@@ -47,6 +52,7 @@ public class TestBase
             httpContextAccessor.HttpContext = new DefaultHttpContext();
             SetZuoXi();
         }
+
     }
 
     public void SetPaiDanYuan()
@@ -74,18 +80,29 @@ public class TestBase
         SetOperator("坐席", "市民热线服务中心", "单元测试坐席", "001", "13408389849", EUserType.Seat, TestSettingConstants.ZuoXiAccountName);
     }
 
+    public void SetWeiXin()
+    {
+        SetOperator("微信用户", "", "", "", "138001389877", EUserType.Normal, TestSettingConstants.WeiXinAccountName);
+    }
+
+    public void Set网格员()
+    {
+        SetOperator("网格员", "市民热线服务中心", "单元测试网格员", "001", "13408389849", EUserType.Normal, TestSettingConstants.GuiderAccountName);
+    }
+
     private void SetOperator(string displayName, string fullOrgName, string name, string orgId, string phoneNo, EUserType userType, string userName)
     {
         var account = _accountRepository.GetExtAsync(
             d => d.UserName == userName,
             d => d.Includes(x => x.Roles)).GetAwaiter().GetResult();
 
-        if (account == null)
+        var roleId = _roleRepository.Queryable()
+                       .Where(m => m.DisplayName == displayName)
+                       .Select(m => m.Id)
+                       .First();
+
+        if (account == null && roleId != null)
         {
-            var roleId = _roleRepository.Queryable()
-                .Where(m => m.DisplayName == displayName)
-                .Select(m => m.Id)
-                .First();
             var newUser = new AddUserDto
             {
                 FullOrgName = fullOrgName,
@@ -98,25 +115,29 @@ public class TestBase
                 UserName = userName
             };
             var accountId = _userController.Add(newUser).GetAwaiter().GetResult();
-            // TestSessionConstants.UserId = accountId;
             account = _accountRepository.GetExtAsync(
                 d => d.UserName == userName,
                 d => d.Includes(x => x.Roles)).GetAwaiter().GetResult();
         }
-        var user = _userRepository.GetAsync(account.Id).GetAwaiter().GetResult();
+        var user = _userRepository.Queryable()
+            .Includes(d => d.Organization)
+            .FirstAsync(d => d.Id == account.Id).GetAwaiter().GetResult();
+        var third = _thirdIdentiyService.GetTokenAsync(new Share.Dtos.Snapshot.ThirdTokenDto()).GetAwaiter().GetResult();
+        var thirdAccount = _thirdAccountRepository.Get(d => d.OpenId == third.OpenId);
 
-        List<Claim> userClaims = [ 
-            new(JwtClaimTypes.Subject, account.Id),
-            new(JwtClaimTypes.PhoneNumber, account.PhoneNo ?? string.Empty),
+        List<Claim> userClaims = [
+            new(JwtClaimTypes.Subject, account.Id ?? thirdAccount.Id),
+            new(JwtClaimTypes.PhoneNumber, account.PhoneNo ?? thirdAccount.PhoneNumber),
             new(ClaimTypes.NameIdentifier, user.Id),
             new(AppClaimTypes.UserDisplayName, account.Name),
-            new(AppClaimTypes.DepartmentId, user.OrgId ?? string.Empty), 
-            new(AppClaimTypes.DepartmentIsCenter, user.Organization?.IsCenter.ToString() ?? false.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.DepartmentId, user.OrgId ?? string.Empty),
+            new(AppClaimTypes.DepartmentIsCenter, user.Organization?.IsCenter.ToString() ?? false.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),
+            new(AppClaimTypes.OpenId, thirdAccount?.OpenId ?? string.Empty),
         ];
         ClaimsIdentity identity = new ClaimsIdentity(userClaims);
         var principal = new ClaimsPrincipal(identity);

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

@@ -70,7 +70,6 @@
     "ConnectionStrings": {
         "Hotline": "PORT=5432;DATABASE=hotline_dev;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;",
         "CAP": "PORT=5432;DATABASE=fwmq;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;Search Path=cap"
-        //"Hotline": "PORT=5432;DATABASE=hotline;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;"
     },
     "Cache": {
         "Host": "110.188.24.182",

+ 19 - 4
src/Hotline.Application/Bigscreen/DataScreenRefreshService.cs

@@ -14,10 +14,12 @@ using System.Text;
 using System.Threading.Tasks;
 using Hotline.Settings;
 using XF.Domain.Constants;
+using XF.Domain.Repository;
+using Hotline.JudicialManagement;
 
 namespace Hotline.Application.Bigscreen
 {
-    public class DataScreenRefreshService: BackgroundService
+    public class DataScreenRefreshService : BackgroundService
     {
         private readonly IServiceScopeFactory _serviceScopeFactory;
 
@@ -33,7 +35,7 @@ namespace Hotline.Application.Bigscreen
             var _orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
             var _mapper = scope.ServiceProvider.GetRequiredService<IMapper>();
             var systemSettingCacheManager = scope.ServiceProvider.GetRequiredService<ISystemSettingCacheManager>();
-           
+            var _orderSecondaryHandlingRepository = scope.ServiceProvider.GetRequiredService<IRepository<OrderSecondaryHandling>>();
             int times = int.Parse(systemSettingCacheManager.GetSetting(SettingConstants.BsDataStateChangedTimes)?.SettingValue[0]);
             while (!stoppingToken.IsCancellationRequested)
             {
@@ -95,7 +97,7 @@ namespace Hotline.Application.Bigscreen
 
                     await realtimeService.OrderCountStatisticsAsync(dto, stoppingToken);
                 }
-                catch{}
+                catch { }
 
                 try
                 {
@@ -110,7 +112,20 @@ namespace Hotline.Application.Bigscreen
 
                     await realtimeService.OrderHandlingDetailAsync(orderlist, stoppingToken);
                 }
-                catch{}
+                catch { }
+
+                try
+                {
+                    var list = await _orderSecondaryHandlingRepository.Queryable().Includes(x => x.Order)
+                              .Where(x => x.CreationTime.Date == DateTime.Now.Date)
+                              .OrderByDescending(x => x.CreationTime)
+                              .Take(50)
+                             .ToListAsync();
+                    var orderlist = _mapper.Map<List<OrderSecondaryHandlingDto>>(list);
+
+                    await realtimeService.OrderSecondaryHandlingDetailAsync(orderlist, stoppingToken);
+                }
+                catch { }
 
                 await Task.Delay(times);
             }

+ 16 - 7
src/Hotline.Application/CallCenter/DefaultCallApplication.cs

@@ -299,6 +299,8 @@ public abstract class DefaultCallApplication : ICallApplication
             .WhereIF(!string.IsNullOrEmpty(dto.StaffNo), d => d.StaffNo == dto.StaffNo)
             .WhereIF(!string.IsNullOrEmpty(dto.GroupId), d => d.GroupId == dto.GroupId)
             .WhereIF(dto.OperateState != null, d => d.OperateState == dto.OperateState)
+            .WhereIF(dto.OperateTimeStart.HasValue, x => x.OperateTime >= dto.OperateTimeStart)
+            .WhereIF(dto.OperateTimeEnd.HasValue, x => x.OperateTime <= dto.OperateTimeEnd)
             .OrderByDescending(d => d.Id)
             .ToFixedListAsync(dto, cancellationToken);
     }
@@ -454,12 +456,19 @@ public abstract class DefaultCallApplication : ICallApplication
     {
         if (dto.CallId.IsNullOrEmpty())
         {
-            var log = await _systemLogRepository.Queryable()
-                .Where(m => m.IpUrl == dto.Id && m.Name == "回访外呼已经接通")
-                .FirstAsync(cancellationToken);
-            if (log is null || log.Remark.IsNullOrEmpty()) return null;
-            var callRemark = log.Remark.FromJson<CallRemarkDto>();
-            dto.CallId = callRemark.CallId;
+            try
+            {
+                var log = await _systemLogRepository.Queryable()
+                    .Where(m => m.IpUrl == dto.Id && m.Name == "回访外呼已经接通")
+                    .FirstAsync(cancellationToken);
+                if (log is null || log.Remark.IsNullOrEmpty()) return null;
+                var callRemark = log.Remark.FromJson<CallRemarkDto>();
+                dto.CallId = callRemark.CallId;
+            }
+            catch (Exception e)
+            { 
+                _logger.LogError($"PublishVisitRelevanceCallIdAsync: {e.ToJson()}");
+            }
         }
 
         var seconds = _systemSettingCacheManager.VisitCallDelaySecond;
@@ -600,7 +609,7 @@ public abstract class DefaultCallApplication : ICallApplication
             TrCallRecordDto = call.Adapt<TrCallDto>()
         }, cancellationToken: cancellationToken);
         var msg = $"原CallId: {orderCall.CallId}, 更新CallId: {call.Id}";
-        _systemLogRepository.Add("延迟更新工单通话", orderId, msg,status:1, ipUrl: orderCall.CallId);
+        _systemLogRepository.Add("延迟更新工单通话", orderId, msg, status: 1, ipUrl: orderCall.CallId);
         return msg + "(完成推省上)";
     }
 

+ 6 - 6
src/Hotline.Application/CallCenter/ICallApplication.cs

@@ -183,17 +183,17 @@ namespace Hotline.Application.CallCenter
         /// <returns></returns>
         Task<EVoiceEvaluate> GetReplyVoiceOrDefaultByOrderIdAsync(string orderId);
 
+        /// <summary>
+        /// 保存回访详情时发送延迟消息同步通话记录
+        /// 如果回访通话记录有多条, 需要关联通话时长最长的那条
+        /// </summary>
+        Task OrderVisitRelevanceCallIdAsync(VisitDto dto, CancellationToken cancellationToken);
+
         /// <summary>
         /// 根据 OrderId 返回用户电话评价枚举
         /// </summary>
         /// <param name="orderId"></param>
         /// <returns></returns>
         Task<ESeatEvaluate> GetSeatDefaultByOrderIdAsync(string orderId);
-
-        /// <summary>
-        /// 保存回访详情时发送延迟消息同步通话记录
-        /// 如果回访通话记录有多条, 需要关联通话时长最长的那条
-        /// </summary>
-        Task OrderVisitRelevanceCallIdAsync(VisitDto dto, CancellationToken cancellationToken);
     }
 }

+ 690 - 0
src/Hotline.Application/Caselibrary/CaseApplication.cs

@@ -0,0 +1,690 @@
+using Hotline.Settings.Hotspots;
+using Hotline.Share.Dtos.Knowledge;
+using Hotline.Share.Dtos.Caselibrary;
+using Hotline.Share.Enums.Caselibrary;
+using MapsterMapper;
+using XF.Domain.Authentications;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+using Hotline.Repository.SqlSugar.Extensions;
+using Hotline.SeedData;
+using Hotline.File;
+using Hotline.Application.Bulletin;
+using Hotline.Share.Tools;
+using Hotline.Application.Tools;
+using SqlSugar;
+using Hotline.CaseLibrary;
+using Hotline.Orders;
+using Hotline.KnowledgeBase;
+using Hotline.Share.Enums.Planlibrary;
+using System.Numerics;
+using Hotline.Share.Dtos.Planlibrary;
+using Hotline.Planlibrary;
+
+namespace Hotline.Application.Caselibrary
+{
+    /// <summary>
+    /// 案例库处理
+    /// </summary>
+    public class CaseApplication : ICaseApplication, IScopeDependency
+    {
+
+        #region 注册
+
+        private readonly IRepository<CaseList> _caseListRepository;                           //案例库列表
+        private readonly IRepository<CaseType> _caseTypeRepository;                           //案例库分类管理
+        private readonly IRepository<CaseTypeOrg> _caseTypeOrgRepository;                     //案例库分类关联机构
+        private readonly IRepository<CaseCollect> _caseCollectRepository;                     //案例库收藏评分
+        private readonly IRepository<Order> _orderRepository;                                 //工单
+        private readonly IRepository<Hotline.KnowledgeBase.Knowledge> _knowledgeRepository;   //知识库
+        private readonly ISessionContext _sessionContext;
+        private readonly IMapper _mapper;
+        private readonly IRepository<Hotspot> _hotspotTypeRepository;
+        private readonly IFileRepository _fileRepository;
+        private readonly IBulletinApplication _bulletinApplication;
+
+        public CaseApplication(
+            IRepository<CaseList> caseListRepository,
+            IRepository<CaseType> caseTypeRepository,
+            IRepository<CaseTypeOrg> caseTypeOrgRepository,
+            IRepository<CaseCollect> caseCollectRepository,
+            IRepository<Order> orderRepository,
+            IRepository<Hotline.KnowledgeBase.Knowledge> knowledgeRepository,
+            ISessionContext sessionContext,
+            IMapper mapper,
+            IRepository<Hotspot> hotspotTypeRepository,
+            IFileRepository fileRepository,
+            IBulletinApplication bulletinApplication)
+        {
+            _caseListRepository = caseListRepository;
+            _caseTypeRepository = caseTypeRepository;
+            _caseTypeOrgRepository = caseTypeOrgRepository;
+            _caseCollectRepository = caseCollectRepository;
+            _orderRepository = orderRepository;
+            _knowledgeRepository = knowledgeRepository;
+            _sessionContext = sessionContext;
+            _mapper = mapper;
+            _hotspotTypeRepository = hotspotTypeRepository;
+            _fileRepository = fileRepository;
+            _bulletinApplication = bulletinApplication;
+        }
+
+        #endregion
+
+        #region 案例库类型管理
+
+        #region 案例库类型 - 新增
+
+        /// <summary>
+        /// 案例库类型 - 新增
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<string> AddTypeAsync(AddCaseTypeDto dto, CancellationToken cancellationToken)
+        {
+            var sandard = await _caseTypeRepository.GetAsync(p => p.ParentId == dto.ParentId && p.Name == dto.Name && p.IsDeleted == false, cancellationToken);
+            if (sandard is not null)
+                throw UserFriendlyException.SameMessage("当前层级已存在相同名称的分类!");
+
+            var type = _mapper.Map<CaseType>(dto);
+            type.InitId();
+            type.IsEnable = true;
+            //获取分类名称全称
+            string FullName = await GetFullName(type.ParentId);
+            //处理全称,如果为第一级直接用全称,否则获取全称后拼接名称
+            type.SpliceName = string.IsNullOrEmpty(FullName) ? dto.Name : FullName + "-" + dto.Name;
+            var id = await _caseTypeRepository.AddAsync(type, cancellationToken);
+            if (dto.TypeOrgDtos != null && dto.TypeOrgDtos.Any())
+            {
+                List<CaseTypeOrg> orgs = _mapper.Map<List<CaseTypeOrg>>(dto.TypeOrgDtos);
+                orgs.ForEach(x => x.TypeId = type.Id);
+                await _caseTypeOrgRepository.AddRangeAsync(orgs, cancellationToken);
+            }
+            return id;
+        }
+
+        #endregion
+
+        #region 案例库类型 - 编辑
+
+        /// <summary>
+        /// 案例库类型 - 编辑
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task UpdateTypeAsync(UpdateCaseTypeDto dto, CancellationToken cancellationToken)
+        {
+            //查询原有数据
+            var type = await _caseTypeRepository.GetAsync(dto.Id, cancellationToken);
+            if (type is null)
+                throw UserFriendlyException.SameMessage("编辑失败!");
+            bool result = type.Name != dto.Name || type.ParentId != dto.ParentId;
+            //是否更改分类名称或者层级
+
+            //转换
+            _mapper.Map(dto, type);
+            //如果更改了名称或者修改了层级,则修改全称,未更改不修改
+            if (result)
+            {
+                string FullName = await GetFullName(type.ParentId);//获取分类名称全称
+                type.SpliceName = string.IsNullOrEmpty(FullName) ? dto.Name : FullName + "-" + dto.Name;//处理全称,如果为第一级直接用全称,否则获取全称后拼接名称
+            }
+
+            //修改数据
+            await _caseTypeRepository.UpdateAsync(type, cancellationToken);
+
+            //如果修改了名称,对应修改子分类全称
+            if (result)
+                await UpdateChildNode(type.Id);
+
+            // 修改关联机构
+            await _caseTypeOrgRepository.RemoveAsync(x => x.TypeId == type.Id, false, cancellationToken);
+            if (dto.TypeOrgDtos != null && dto.TypeOrgDtos.Any())
+            {
+                List<CaseTypeOrg> orgs = _mapper.Map<List<CaseTypeOrg>>(dto.TypeOrgDtos);
+                orgs.ForEach(x => x.TypeId = type.Id);
+                await _caseTypeOrgRepository.AddRangeAsync(orgs, cancellationToken);
+            }
+        }
+
+        #endregion
+
+        #region 案例库类型 - 删除
+
+        /// <summary>
+        /// 案例库类型 - 删除
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task RemoveTypeAsync(string Id, CancellationToken cancellationToken)
+        {
+            //查询数据是否存在
+            var sandard = await _caseTypeRepository.GetAsync(p => p.Id == Id && p.IsDeleted == false, cancellationToken);
+            if (sandard is null)
+                throw UserFriendlyException.SameMessage("分类不存在!");
+
+            //查询是否有子级分类
+            var checkChild = await _caseTypeRepository.CountAsync(p => p.ParentId == Id && p.IsDeleted == false, cancellationToken);
+            if (checkChild > 0)
+                throw UserFriendlyException.SameMessage("存在子级分类!");
+
+            //查询是否有案例分类
+            var checkKnowledge = await _caseListRepository.CountAsync(p => p.CaseTypes.Any(t => t.Id == Id), cancellationToken);
+            if (checkKnowledge > 0)
+                throw UserFriendlyException.SameMessage("分类存在案例!");
+
+            //删除操作
+            await _caseTypeRepository.RemoveAsync(sandard, true, cancellationToken);
+        }
+
+        #endregion
+
+        #endregion
+
+        #region 案例库管理
+
+        #region 案例库 - 列表
+
+        /// <summary>
+        /// 案例库 - 列表
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <returns></returns>
+        public async Task<(int, IList<CaseDataDto>)> QueryAllCaseListAsync(CaseListDto pagedDto, CancellationToken cancellationToken)
+        {
+            var typeSpliceName = string.Empty;
+
+            if (!string.IsNullOrEmpty(pagedDto.CaseTypeID))
+            {
+                var type = await _caseTypeRepository.GetAsync(x => x.Id == pagedDto.CaseTypeID);
+                typeSpliceName = type?.SpliceName;
+            }
+
+            //单表分页
+            var (total, temp) = await _caseListRepository.Queryable()
+                .Includes(x => x.CaseTypes)
+                .Includes(x => x.ExaminMan)
+                .Includes(x => x.Order)
+                .Includes(x => x.Knowledge)
+                .Where(x => x.IsDeleted == false)
+
+                .WhereIF(pagedDto.IsPopular == true, x => x.IsPopular == true)
+
+                .WhereIF(OrgSeedData.CenterId != pagedDto.CreateOrgId && !string.IsNullOrEmpty(pagedDto.CreateOrgId), x => x.CreatorOrgId != null && x.CreatorOrgId.StartsWith(pagedDto.CreateOrgId!))
+
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.Title) &&
+                          string.IsNullOrEmpty(pagedDto.Keywords) &&
+                          string.IsNullOrEmpty(pagedDto.Abstract), x => x.Title.Contains(pagedDto.Title!))
+
+                .WhereIF(string.IsNullOrEmpty(pagedDto.Title) &&
+                        !string.IsNullOrEmpty(pagedDto.Keywords) &&
+                         string.IsNullOrEmpty(pagedDto.Abstract), x => x.Keywords.Contains(pagedDto.Keywords!))
+
+                .WhereIF(string.IsNullOrEmpty(pagedDto.Title) &&
+                         string.IsNullOrEmpty(pagedDto.Keywords) &&
+                        !string.IsNullOrEmpty(pagedDto.Abstract), x => x.Abstract.Contains(pagedDto.Abstract!))
+
+                .WhereIF(!string.IsNullOrEmpty(pagedDto.Title) &&
+                         !string.IsNullOrEmpty(pagedDto.Keywords) &&
+                         !string.IsNullOrEmpty(pagedDto.Abstract)
+                                                                , x => x.Title.Contains(pagedDto.Title!) ||
+                                                                  x.Keywords!.Contains(pagedDto.Keywords!) ||
+                                                                  x.Abstract!.Contains(pagedDto.Abstract!))
+
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == ECaseStatus.OnShelf, x => x.Status == ECaseStatus.OnShelf)
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == ECaseStatus.OffShelf, x => x.Status == ECaseStatus.OffShelf)
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == ECaseStatus.Auditing, x => x.Status == ECaseStatus.Auditing)
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == ECaseStatus.NewDrafts, x =>
+                                                                                               (x.Status == ECaseStatus.Drafts && x.CreatorId == _sessionContext.UserId) ||
+                                                                                               (x.Status == ECaseStatus.Revert && x.CreatorId == _sessionContext.UserId) ||
+                                                                                               (x.Status == ECaseStatus.NewDrafts && x.CreatorId == _sessionContext.UserId))
+                .WhereIF(pagedDto.Status.HasValue && pagedDto.Status == ECaseStatus.All, x => (x.Status == ECaseStatus.OnShelf ||
+                                                                                               x.Status == ECaseStatus.OffShelf ||
+                                                                                               x.Status == ECaseStatus.Auditing) ||
+                                                                                              (x.Status == ECaseStatus.Drafts && x.CreatorId == _sessionContext.UserId) ||
+                                                                                              (x.Status == ECaseStatus.Revert && x.CreatorId == _sessionContext.UserId) ||
+                                                                                              (x.Status == ECaseStatus.NewDrafts && x.CreatorId == _sessionContext.UserId))
+
+                .WhereIF(!string.IsNullOrEmpty(typeSpliceName), x => x.CaseTypes.Any(t => t.SpliceName.StartsWith(typeSpliceName)))
+
+                .WhereIF(pagedDto.CreationTimeStart.HasValue, x => x.CreationTime >= pagedDto.CreationTimeStart)
+                .WhereIF(pagedDto.CreationTimeEnd.HasValue, x => x.CreationTime <= pagedDto.CreationTimeEnd)
+
+                .WhereIF(pagedDto.OnShelfTimeStart.HasValue, x => x.OnShelfTime >= pagedDto.OnShelfTimeStart)
+                .WhereIF(pagedDto.OnShelfTimeEnd.HasValue, x => x.OnShelfTime <= pagedDto.OnShelfTimeEnd)
+
+                .WhereIF(pagedDto.OffShelfTimeStart.HasValue, x => x.OffShelfTime >= pagedDto.OffShelfTimeStart)
+                .WhereIF(pagedDto.OffShelfTimeEnd.HasValue, x => x.OffShelfTime <= pagedDto.OffShelfTimeEnd)
+
+                .WhereIF(pagedDto.UpdateTimeStart.HasValue, x => x.UpdateTime >= pagedDto.UpdateTimeStart)
+                .WhereIF(pagedDto.UpdateTimeEnd.HasValue, x => x.UpdateTime <= pagedDto.UpdateTimeEnd)
+
+                .WhereIF(pagedDto.ExaminTimeStart.HasValue, x => x.ExaminTime >= pagedDto.ExaminTimeStart)
+                .WhereIF(pagedDto.ExaminTimeEnd.HasValue, x => x.ExaminTime <= pagedDto.ExaminTimeEnd)
+
+                .OrderByIF(string.IsNullOrEmpty(pagedDto.SortField), x => x.CreationTime, OrderByType.Desc)
+                .OrderByIF(pagedDto is { SortField: "pageView" }, x => x.PageView, OrderByType.Desc)         //阅读量
+                .OrderByIF(pagedDto is { SortField: "score" }, x => x.Score, OrderByType.Desc)               //评分
+                .OrderByIF(pagedDto is { SortField: "creationTime" }, x => x.CreationTime, OrderByType.Desc) //创建时间
+                                                                                                             //.Select(x => new CaseDataDto
+                                                                                                             //{
+                                                                                                             //    Id = x.Id.SelectAll(),
+                                                                                                             //    OrderTitle = x.Order.Title,
+                                                                                                             //    KnowledgeTitle = x.Knowledge.Title
+                                                                                                             //})
+                .ToPagedListAsync(pagedDto.PageIndex, pagedDto.PageSize, cancellationToken);
+
+            return (total, _mapper.Map<IList<CaseDataDto>>(temp));
+            //return (total, temp);
+        }
+
+        #endregion
+
+        #region 案例库 - 新增
+
+        /// <summary>
+        /// 新增
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<string> AddCaseAsync(AddCaseListDto dto, CancellationToken cancellationToken)
+        {
+            var cList = _mapper.Map<CaseList>(dto);
+
+            var any = await _caseListRepository.Queryable().Where(x => x.Status == ECaseStatus.OnShelf && x.Title == dto.Title).AnyAsync();
+            if (any)
+                throw UserFriendlyException.SameMessage("当前案例标题存在重复标题!");
+
+            cList.InitId();
+
+            if (dto.Files != null && dto.Files.Count > 0)
+                cList.FileJson = await _fileRepository.AddFileAsync(dto.Files, cList.Id, "", cancellationToken);
+
+            if (dto.CaseTypes.Any())
+            {
+                cList.CaseTypes = dto.CaseTypes.Select(d => new CaseType
+                {
+                    Id = d.Id,
+                    Name = d.Name,
+                    SpliceName = d.SpliceName
+                }).ToList();
+            }
+
+            await _caseListRepository.AddNav(cList)
+                                     .Include(d => d.CaseTypes)
+                                     .ExecuteCommandAsync();
+
+            return cList.Id;
+        }
+
+        #endregion
+
+        #region 案例库 - 修改
+
+        /// <summary>
+        /// 修改
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task UpdateCaseAsync(UpdateCaseListDto dto, CancellationToken cancellationToken)
+        {
+            var Case = await _caseListRepository.GetAsync(dto.Id);
+
+            if (Case == null)
+                throw UserFriendlyException.SameMessage("案例库查询失败");
+
+            if ((Case.Status == ECaseStatus.OnShelf || Case.Status == ECaseStatus.Auditing) && (Case.ExpiredTime.HasValue && Case.ExpiredTime.Value > DateTime.Now))
+                throw UserFriendlyException.SameMessage("案例库数据不可修改");
+
+            var any = await _caseListRepository.Queryable().Where(x => x.Status == ECaseStatus.OnShelf && x.Title == dto.Title && x.Id != dto.Id).AnyAsync();
+            if (any)
+                throw UserFriendlyException.SameMessage("当前案例标题存在重复标题!");
+
+            if (dto.ApplyStatus == ECaseApplyStatus.Delete)
+            {
+                Case.Status = (ECaseStatus)dto.Status;
+                Case.ApplyStatus = (ECaseApplyStatus)dto.ApplyStatus;
+                Case.Id = dto.Id;
+                Case.ApplyReason = dto.ApplyReason;
+            }
+            else
+            {
+                _mapper.Map(dto, Case);
+
+                if (dto.Files != null && dto.Files.Count > 0)
+                    Case.FileJson = await _fileRepository.AddFileAsync(dto.Files, Case.Id, "", cancellationToken);
+                else
+                    Case.FileJson = new List<Share.Dtos.File.FileJson>();
+            }
+
+
+            if (dto.ApplyStatus != ECaseApplyStatus.Delete)
+            {
+                if (dto.CaseTypes.Any())
+                {
+                    Case.CaseTypes = dto.CaseTypes.Select(d => new CaseType
+                    {
+                        Id = d.Id
+                    }).ToList();
+
+                    await _caseListRepository.UpdateNav(Case)
+                        .Include(d => d.CaseTypes, new UpdateNavOptions
+                        {
+                            ManyToManyIsUpdateA = true
+                        })
+                        .ExecuteCommandAsync();
+                }
+                else
+                {
+                    await _caseListRepository.UpdateAsync(Case, cancellationToken);
+                }
+            }
+            else
+            {
+                await _caseListRepository.UpdateAsync(Case, cancellationToken);
+            }
+        }
+
+        #endregion
+
+        #region 案例库 - 删除
+
+        /// <summary>
+        /// 删除
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task RemoveCaseAsync(UpdateCaseListDto dto, CancellationToken cancellationToken)
+        {
+            var Case = await _caseListRepository.GetAsync(dto.Id);
+
+            if (Case == null)
+                throw UserFriendlyException.SameMessage("案例库查询失败");
+
+            _mapper.Map(dto, Case);
+
+            Case.IsDeleted = true;
+
+            await _caseListRepository.UpdateNullAsync(Case, cancellationToken);
+        }
+
+        #endregion
+
+        #region 案例库 - 审核
+
+        /// <summary>
+        /// 审核
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task AuditCaseAsync(UpdateCaseListDto dto, CancellationToken cancellationToken)
+        {
+            var Case = await _caseListRepository.GetAsync(dto.Id);
+
+            if (Case == null)
+                throw UserFriendlyException.SameMessage("案例库查询失败");
+
+            Case.Status = (ECaseStatus)dto.Status!;
+            Case.ApplyStatus = (ECaseApplyStatus)dto.ApplyStatus!;
+            Case.ExaminTime = dto.ExaminTime;
+            Case.ExaminManId = dto.ExaminManId;
+            Case.ExaminOrganizeId = dto.ExaminOrganizeId;
+            Case.ExaminOpinion = dto.ExaminOpinion;
+            Case.UpdateTime = dto.UpdateTime;
+            Case.OnShelfTime = dto.OnShelfTime;
+            Case.OffShelfTime = dto.OffShelfTime;
+
+            if (Case.ApplyStatus == ECaseApplyStatus.Delete)
+            {
+                Case.IsDeleted = true;
+            }
+
+            await _caseListRepository.UpdateNullAsync(Case, cancellationToken);
+        }
+
+        #endregion
+
+        #region 案例库 - 详情
+
+        /// <summary>
+        /// 详情
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <param name="IsAddPv">默认不增加,false不增加,true增加浏览量</param>
+        /// <returns></returns>
+        public async Task<CaseInfoDto> GetCaseAsync(string Id, bool? IsAddPv, CancellationToken cancellationToken)
+        {
+            var Case = await _caseListRepository.Queryable()
+                .Includes(x => x.CaseTypes)
+                .Includes(x => x.Order)
+                .Includes(x => x.Knowledge)
+                .Where(x => x.Id == Id).FirstAsync();
+            if (Case == null)
+                throw UserFriendlyException.SameMessage("案例库查询失败");
+            ;
+            //转化
+            var caseInfoDto = _mapper.Map<CaseInfoDto>(Case);
+
+            if (Case != null)
+            {
+                if (!string.IsNullOrEmpty(Case.Abstract))
+                    caseInfoDto.Abstract = _bulletinApplication.GetSiteUrls(Case.Abstract);
+                if (!string.IsNullOrEmpty(Case.Describe))
+                    caseInfoDto.Describe = _bulletinApplication.GetSiteUrls(Case.Describe);
+                if (!string.IsNullOrEmpty(Case.Result))
+                    caseInfoDto.Result = _bulletinApplication.GetSiteUrls(Case.Result);
+                if (!string.IsNullOrEmpty(Case.Reason))
+                    caseInfoDto.Reason = _bulletinApplication.GetSiteUrls(Case.Reason);
+            }
+
+            // 分类
+            //var relationType = await _caseRelationTypeRepository.QueryAsync(x => x.CaseId == Id && x.CreatorId == _sessionContext.UserId);
+            //if (relationType != null)
+            //{
+            //    caseInfoDto.CaseTypes = _mapper.Map<List<CaseRelationTypeDto>>(relationType);
+            //}
+
+            // 收藏
+            var collect = await _caseCollectRepository.GetAsync(x => x.CaseId == Id && x.CreatorId == _sessionContext.UserId);
+            if (collect != null)
+                caseInfoDto.Collect = _mapper.Map<CaseCollectDto>(collect);
+
+            // 附件
+            if (caseInfoDto.FileJson != null && caseInfoDto.FileJson.Any())
+            {
+                var ids = caseInfoDto.FileJson.Select(x => x.Id).ToList();
+                caseInfoDto.Files = await _fileRepository.GetFilesAsync(ids, cancellationToken);
+            }
+
+            // 更新浏览量
+            if (IsAddPv == true)
+            {
+                //修改浏览量
+                Case.PageView++;
+                //修改点击量
+                await _caseListRepository.UpdateAsync(Case, cancellationToken);
+            }
+            return caseInfoDto;
+        }
+
+        #endregion
+
+        #region 案例库 - 批量导出
+
+        /// <summary>
+        /// 案例库 - 批量导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<Dictionary<string, Stream>> CaseInfoListExportAsync(CaseInfoExportDto dto, CancellationToken cancellationToken)
+        {
+            var streamList = new Dictionary<string, Stream>();
+            var knowList = await _caseListRepository.Queryable()
+                .Where(m => dto.Ids.Contains(m.Id))
+                .Select(m => new { m.Title, m.Result })
+                .ToListAsync(cancellationToken);
+
+            var tasks = knowList.Select(async item =>
+            {
+                var stream = await Task.Run(() => item.Result.HtmlToStream(dto.FileType), cancellationToken);
+                return new KeyValuePair<string, Stream>(
+                    item.Title + dto.FileType.GetFileExtension(),
+                    stream
+                );
+            });
+
+            var results = await Task.WhenAll(tasks);
+
+            foreach (var kvp in results)
+            {
+                if (!streamList.ContainsKey(kvp.Key))
+                {
+                    streamList.Add(kvp.Key, kvp.Value);
+                }
+            }
+
+            return streamList;
+        }
+
+        #endregion
+
+        #endregion
+
+        #region 私有方法
+
+        #region 查询所有子级
+
+        /// <summary>
+        /// 查询所有子级
+        /// </summary>
+        /// <param name="treeDatas">分类数据</param>
+        /// <param name="ID">需要查询哪级下面的分类</param>
+        /// <param name="checkId">选中的数据ID</param>
+        /// <returns></returns>
+        private List<TreeListDto> GetChildren(List<CaseType> treeDatas, string ID, string? checkId)
+        {
+            List<TreeListDto> nodeList = new();
+            //根据ID查询子级
+            var children = treeDatas.Where(q => q.ParentId == ID);
+            foreach (var dr in children)
+            {
+                //组装数据
+                TreeListDto node = new()
+                {
+                    name = dr.Name,
+                    ParentID = dr.ParentId,
+                    value = dr.Id,
+                    IsEnable = dr.IsEnable
+                };
+                //是否选中
+                if (!string.IsNullOrEmpty(checkId) && checkId != Guid.Empty.ToString() && checkId == dr.Id)
+                    node.selected = true;
+                //子级数据赋值
+                node.children = GetChildren(treeDatas, node.value, checkId);
+                //添加数据
+                nodeList.Add(node);
+            }
+            return nodeList;
+        }
+
+        #endregion
+
+        #region  获取全称
+
+        /// <summary>
+        /// 获取全称
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        private async Task<string> GetFullName(string? Id)
+        {
+            //获取全部父级名称
+            var list = await GetParentNode(Id);
+            //倒叙
+            list.Reverse();
+            //拆分
+            return string.Join("-", list.ToArray());
+        }
+
+        /// <summary>
+        /// 查询父级名称
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        private async Task<List<string>> GetParentNode(string? Id)
+        {
+            List<string> list = new();
+            //查询父级数据
+            var type = await _caseTypeRepository.GetAsync(p => p.Id == Id);
+            if (type != null)
+            {
+                //添加名称
+                list.Add(type.Name);
+                list.AddRange(await GetParentNode(type.ParentId));
+            }
+            return list;
+        }
+
+        #endregion
+
+        #region 修改子级分类全称
+
+        /// <summary>
+        /// 修改子级分类全称
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        private async Task UpdateChildNode(string Id)
+        {
+            //查询子分类
+            var list = await GetChildNode(Id);
+            if (list is not null && list.Count > 0)
+            {
+                foreach (var item in list)
+                {
+                    //获取全称
+                    string FullName = await GetFullName(item.ParentId);
+                    item.SpliceName = string.IsNullOrEmpty(FullName) ? item.Name : FullName + "-" + item.Name;
+                    //修改全称
+                    await _caseTypeRepository.UpdateAsync(item);
+                }
+            }
+        }
+
+        /// <summary>
+        /// 查询子级节点数据
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        private async Task<List<CaseType>> GetChildNode(string Id)
+        {
+            List<CaseType> list = new();
+            //查询数据
+            var typelist = await _caseTypeRepository.QueryAsync(p => p.ParentId == Id);
+            if (typelist != null)
+            {
+                //处理数据
+                foreach (var item in typelist)
+                {
+                    list.Add(item);
+                    list.AddRange(await GetChildNode(item.Id));
+                }
+            }
+            return list;
+        }
+
+        #endregion
+
+        #endregion
+
+    }
+}

+ 90 - 0
src/Hotline.Application/Caselibrary/ICaseApplication.cs

@@ -0,0 +1,90 @@
+using Hotline.Share.Dtos.Caselibrary;
+
+namespace Hotline.Application.Caselibrary
+{
+    public interface ICaseApplication
+    {
+
+        #region 案例库类型管理
+
+        /// <summary>
+        ///案例库类型 - 新增
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task<string> AddTypeAsync(AddCaseTypeDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        ///案例库类型 - 编辑
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task UpdateTypeAsync(UpdateCaseTypeDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 案例库类型 - 删除
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <returns></returns>
+        Task RemoveTypeAsync(string Id, CancellationToken cancellationToken);
+
+        #endregion
+
+        #region 案例库管理
+
+        /// <summary>
+        /// 案例库 - 列表
+        /// </summary>
+        /// <param name="pagedDto"></param>
+        /// <returns></returns>
+        Task<(int, IList<CaseDataDto>)> QueryAllCaseListAsync(CaseListDto pagedDto, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 案例库 - 新增
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        Task<string> AddCaseAsync(AddCaseListDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        ///案例库类型 - 编辑
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task UpdateCaseAsync(UpdateCaseListDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        ///案例库类型 - 删除
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task RemoveCaseAsync(UpdateCaseListDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        ///案例库类型 - 下架审核
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task AuditCaseAsync(UpdateCaseListDto dto, CancellationToken cancellationToken);
+
+        /// <summary>
+        ///案例库类型 - 详情
+        /// </summary>
+        /// <param name="Id"></param>
+        /// <param name="IsAddPv">默认不增加,false不增加,true增加浏览量</param>
+        /// <returns></returns>
+        Task<CaseInfoDto> GetCaseAsync(string Id, bool? IsAddPv, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 案例库类型 - 批量导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        Task<Dictionary<string, Stream>> CaseInfoListExportAsync(CaseInfoExportDto dto, CancellationToken cancellationToken);
+
+        #endregion
+
+    }
+}

+ 20 - 0
src/Hotline.Application/ExportExcel/ExportApplication.cs

@@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.DependencyInjection;
 using MiniExcelLibs;
+using NPOI.HPSF;
 using System.Net.Http;
 using System.Reflection;
 using XF.Domain.Dependency;
@@ -67,6 +68,25 @@ namespace Hotline.Application.ExportExcel
             return GetExcelStream(dto, items, func).GetExcelFile(fileName);
         }
 
+        public FileStreamResult GetExcelFile(Type typeT, Type typeD, object dto, IList<object> items, string fileName)
+        {
+            var columnInfos = typeD.GetProperty("ColumnInfos")?.GetValue(dto) as List<ColumnInfo>;
+            if (columnInfos == null)
+            {
+                throw new ArgumentException("ColumnInfos not found in dto");
+            }
+
+            dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(columnInfos);
+
+            var dtos = items
+                .Select(item => _mapper.Map(item, typeT, dynamicClass))
+                .Cast<object>()
+                .ToList();
+
+            return ExcelHelper.CreateStream(dtos).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);

+ 2 - 0
src/Hotline.Application/ExportExcel/IExportApplication.cs

@@ -26,6 +26,8 @@ namespace Hotline.Application.ExportExcel
 
         FileStreamResult GetExcelFile<T, D>(ExportExcelDto<D> dto, IList<T> items,string fileName, Func<IList<T>, T>? func = null);
 
+        FileStreamResult GetExcelFile(Type typeT, Type typeD, object dto, IList<object> items, string fileName);
+
         /// <summary>
         /// 导入数据
         /// </summary>

+ 13 - 1
src/Hotline.Application/FlowEngine/WorkflowApplication.cs

@@ -482,6 +482,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 CurrentStepType = startStepDefine.StepType,
                 CurrentHandlerType = startStepDefine.HandlerType,
                 CurrentStepBusinessType = startStepDefine.BusinessType,
+                CurrentTag = startStepDefine.Tag ?? string.Empty,
                 Steps = new List<NextStepOption> { nextStepOption }
             };
         }
@@ -497,6 +498,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             CurrentStepType = startStepDefine.StepType,
             CurrentHandlerType = startStepDefine.HandlerType,
             CurrentStepBusinessType = startStepDefine.BusinessType,
+            CurrentTag = startStepDefine.Tag ?? string.Empty,
             TimeTypeOptions = EnumExts.GetDescriptions<ETimeType>().ToList(),
             Steps = await GetConfigStepsAsync(definition.FlowType, startStepDefine.StepType, startStepDefine.BusinessType,
                 firstStepDefines, cancellationToken)
@@ -572,6 +574,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 StepType = stepDefine.StepType,
                 BusinessType = stepDefine.BusinessType,
                 HandlerType = stepDefine.HandlerType,
+                Tag = stepDefine.Tag ?? string.Empty,
                 OrgLevel = stepDefine.HandlerType is EHandlerType.OrgLevel ? orgLevel : null,
             };
             var orgs = await _organizeRepository.Queryable()
@@ -611,6 +614,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             CurrentStepBusinessType = currentStep.BusinessType,
             CurrentStepType = currentStep.StepType,
             CurrentHandlerType = currentStep.HandlerType,
+            CurrentTag = currentStep.Tag ?? string.Empty,
             TimeTypeOptions = EnumExts.GetDescriptions<ETimeType>().ToList(),
             IsMainHandlerShow = workflow.WorkflowDefinition.IsMainHandlerShow,
             StepId = currentStep.Id,
@@ -857,7 +861,9 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             NextStepOption nextStepOption;
 
             //汇总节点只能选择对应节点办理对象
-            if (workflow.FlowType is EFlowType.Handle && stepDefine.StepType is EStepType.Summary)
+            if (workflow.FlowType is EFlowType.Handle 
+                && stepDefine.StepType is EStepType.Summary
+                && stepDefine.BusinessType is EBusinessType.Seat or EBusinessType.Send)
             {
                 var handler = _workflowDomainService.GetSummaryTargetFlowStepHandler(workflow, stepDefine.SummaryTargetCode);
 
@@ -869,6 +875,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                     BusinessType = stepDefine.BusinessType,
                     //HandlerType = stepDefine.HandlerType,
                     HandlerType = EHandlerType.AssignedUser, //指定办理人(业务需求)
+                    Tag = stepDefine.Tag ?? string.Empty,
                     Items = new List<FlowStepHandler> { handler } //handlers
                 };
             }
@@ -901,6 +908,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
                 StepType = stepDefine.StepType,
                 BusinessType = stepDefine.BusinessType,
                 HandlerType = stepDefine.HandlerType,
+                Tag = stepDefine.Tag ?? string.Empty,
                 Items = handlers
             };
         }
@@ -1045,6 +1053,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             StepType = stepDefine.StepType,
             BusinessType = stepDefine.BusinessType,
             HandlerType = stepDefine.HandlerType,
+            Tag = stepDefine.Tag ?? string.Empty,
             OrgLevel = stepDefine.HandlerType is EHandlerType.OrgLevel ? orgLevel : null,
             Items = handlers
         };
@@ -1081,6 +1090,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             BusinessType = prevStep.BusinessType,
             //HandlerType = prevStep.HandlerType,
             HandlerType = EHandlerType.AssignedUser, //指定办理人(业务需求)
+            Tag = prevStep.Tag ?? string.Empty,
             Items = new List<FlowStepHandler> { handler } //handlers //new List<Kv> { new(prevStep.HandlerId, prevStep.HandlerName) },
         };
     }
@@ -1480,6 +1490,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             StepType = stepType,
             BusinessType = businessType,
             HandlerType = handlerType, //目前所有动态策略均属于部门等级
+            Tag = string.Empty,
             Items = items
         };
     }
@@ -1620,6 +1631,7 @@ public class WorkflowApplication : IWorkflowApplication, IScopeDependency
             StepType = stepType,
             BusinessType = businessType,
             HandlerType = EHandlerType.OrgLevel, //目前所有动态策略均属于部门等级
+            Tag = string.Empty,
             Items = items
         };
     }

+ 2 - 1
src/Hotline.Application/Handlers/FlowEngine/WorkflowEndHandler.cs

@@ -20,6 +20,7 @@ using Hotline.Share.Dtos.TrCallCenter;
 using Hotline.Share.Enums.CallCenter;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Tools;
+using Hotline.Snapshot.Notifications;
 using MapsterMapper;
 using MediatR;
 using Microsoft.Extensions.Logging;
@@ -230,7 +231,7 @@ public class WorkflowEndHandler : INotificationHandler<EndWorkflowNotify>
                     //这里需要判断是否是警情退回
                     orderFlowDto.IsNonPoliceReturn = notification.Dto.External == null ? false : notification.Dto.External.IsPoliceReturn;
                     await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderFiled, orderFlowDto, cancellationToken: cancellationToken);
-
+                    await _publisher.PublishAsync(new SnapshotOrderFiledNotification(order.Id), PublishStrategy.ParallelWhenAll, cancellationToken);
                     await _orderDomainService.OrderAutomaticPublishAsync(order, cancellationToken);
                     //try
                     //{

+ 32 - 13
src/Hotline.Application/Handlers/FlowEngine/WorkflowNextHandler.cs

@@ -1,6 +1,7 @@
 using DotNetCore.CAP;
 using Hotline.Application.JudicialManagement;
 using Hotline.Application.Quality;
+using Hotline.Application.Snapshot;
 using Hotline.Caching.Interfaces;
 using Hotline.Configurations;
 using Hotline.EventBus;
@@ -22,6 +23,8 @@ using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Push;
 using Hotline.Share.Enums.Quality;
 using Hotline.Share.Mq;
+using Hotline.Snapshot.Interfaces;
+using Hotline.Snapshot.Notifications;
 using Hotline.Users;
 using Mapster;
 using MapsterMapper;
@@ -31,6 +34,8 @@ using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 using XF.Domain.Authentications;
 using XF.Domain.Entities;
+using XF.Domain.Exceptions;
+using XF.Domain.Extensions;
 using XF.Domain.Repository;
 
 namespace Hotline.Application.Handlers.FlowEngine;
@@ -39,6 +44,7 @@ public class WorkflowNextHandler : INotificationHandler<NextStepNotify>
 {
     private readonly IOrderDomainService _orderDomainService;
     private readonly ICalcExpireTime _expireTime;
+    private readonly IOrderSnapshotRepository _orderSnapshotRepository;
     private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
     private readonly IOrderRepository _orderRepository;
     private readonly ICapPublisher _capPublisher;
@@ -68,7 +74,8 @@ public class WorkflowNextHandler : INotificationHandler<NextStepNotify>
         ISystemSettingCacheManager systemSettingCacheManager,
         Publisher publisher,
         IOptionsSnapshot<AppConfiguration> appOptions,
-        ICalcExpireTime expireTime)
+        ICalcExpireTime expireTime,
+        IOrderSnapshotRepository orderSnapshotRepository)
     {
         _orderDomainService = orderDomainService;
         _orderRepository = orderRepository;
@@ -85,6 +92,7 @@ public class WorkflowNextHandler : INotificationHandler<NextStepNotify>
         _publisher = publisher;
         _appOptions = appOptions;
         _expireTime = expireTime;
+        _orderSnapshotRepository = orderSnapshotRepository;
     }
 
     /// <summary>Handles a notification</summary>
@@ -100,12 +108,8 @@ public class WorkflowNextHandler : INotificationHandler<NextStepNotify>
             var data = notification.Dto;
             var assignInfo = notification.FlowAssignInfo;
 
-            var currentTag = string.IsNullOrEmpty(notification.Trace.Tag)
-                ? null
-                : System.Text.Json.JsonSerializer.Deserialize<DefinitionTag>(notification.Trace.Tag);
-            var nextTag = string.IsNullOrEmpty(notification.NextStepDefine.Tag)
-                ? null
-                : System.Text.Json.JsonSerializer.Deserialize<DefinitionTag>(notification.NextStepDefine.Tag);
+            var currentTag = notification.Trace.Tag.TryDeserialize<DefinitionTag>();
+            var nextTag = notification.NextStepDefine.Tag.TryDeserialize<DefinitionTag>();
 
             switch (workflow.ModuleCode)
             {
@@ -227,14 +231,29 @@ public class WorkflowNextHandler : INotificationHandler<NextStepNotify>
                         ExpiredTimeChanged = isCenterToOrg,
                         HandlerOrgLevel = notification.HandlerOrgId.CalcOrgLevel()
                     }, cancellationToken: cancellationToken);
+
                     if (data.FlowDirection is EFlowDirection.CenterToOrg)
                         await _qualityApplication.AddQualityAsync(EQualitySource.Send, order.Id, cancellationToken);
 
+                    //随手拍推送网格员
+                    bool.TryParse(
+                        _systemSettingCacheManager.GetSetting(SettingConstants.Snapshot)?.SettingValue[0],
+                        out bool isSnapshotEnable);
+                    if (isSnapshotEnable)
+                    {
+                        if (string.Compare(TagDefaults.Wanggeyuan, notification.NextStepDefine.Tag,
+                                StringComparison.OrdinalIgnoreCase) == 0)
+                        {
+                            await _publisher.PublishAsync(new PostGuiderSystemNotification(order.Id),
+                                PublishStrategy.ParallelWhenAll, cancellationToken);
+                        }
+                    }
+
                     break;
                 case WorkflowModuleConsts.KnowledgeAdd:
                 case WorkflowModuleConsts.KnowledgeUpdate:
                 case WorkflowModuleConsts.KnowledgeOffshelf:
-				case WorkflowModuleConsts.KnowledgeDelete:
+                case WorkflowModuleConsts.KnowledgeDelete:
                     //var knowledgeWork = await _knowledgeWorkFlowRepository.Queryable().Where(x => x.Id == workflow.ExternalId).FirstAsync(cancellationToken);
                     var knowledge = await _knowledgeRepository.Queryable().Where(x => x.Id == workflow.ExternalId).FirstAsync(cancellationToken);
                     knowledge.Flowed(workflow.FlowedUserIds, workflow.FlowedOrgIds, workflow.HandlerUsers, workflow.HandlerOrgs);
@@ -324,14 +343,14 @@ public class WorkflowNextHandler : INotificationHandler<NextStepNotify>
                                         orderDelay.DelayApplyType = EDelayApplyType.ProvinceApply;
                                         orderDelay.IsProDelay = true;
                                         await _orderDelayRepository.UpdateAsync(orderDelay);
-										//省件延期--以省审批前一个节点整理的延期意见为准推送省上 宜宾
-										if (_appOptions.Value.IsYiBin)
+                                        //省件延期--以省审批前一个节点整理的延期意见为准推送省上 宜宾
+                                        if (_appOptions.Value.IsYiBin)
                                         {
-	                                        orderDelay.DelayReason = notification.Dto.Opinion;
+                                            orderDelay.DelayReason = notification.Dto.Opinion;
                                         }
 
-										//推送
-										var publishOrderDelay = _mapper.Map<PublishOrderDelayDto>(orderDelay);
+                                        //推送
+                                        var publishOrderDelay = _mapper.Map<PublishOrderDelayDto>(orderDelay);
                                         await _capPublisher.PublishAsync(EventNames.HotlineOrderApplyDelay, publishOrderDelay,
                                             cancellationToken: cancellationToken);
 

+ 45 - 21
src/Hotline.Application/Handlers/FlowEngine/WorkflowPreviousHandler.cs

@@ -1,20 +1,24 @@
 using DotNetCore.CAP;
+using Hotline.Configurations;
 using Hotline.FlowEngine.Notifications;
 using Hotline.FlowEngine.WorkflowModules;
 using Hotline.FlowEngine.Workflows;
 using Hotline.Orders;
 using Hotline.Push.Notifies;
+using Hotline.Repository.SqlSugar.Orders;
+using Hotline.Settings.TimeLimitDomain;
 using Hotline.Settings.TimeLimits;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Dtos.Push;
 using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Push;
+using Hotline.Share.Enums.Settings;
 using Hotline.Users;
 using MapsterMapper;
 using MediatR;
-using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
 using XF.Domain.Authentications;
 using XF.Domain.Repository;
 
@@ -36,8 +40,11 @@ namespace Hotline.Application.Handlers.FlowEngine
         private readonly ISessionContext _sessionContext;
         private readonly IRepository<OrderScreenDetail> _orderScreenDetailRepository;
         private readonly IRepository<OrderTerminate> _orderTerminateRepository;
+        private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
+        private readonly ICalcExpireTime _expireTime;
+        private readonly IRepository<OrderVisitDetail> _orderVisitDetailRepository;
 
-		public WorkflowPreviousHandler(
+        public WorkflowPreviousHandler(
             IOrderDomainService orderDomainService,
             IOrderRepository orderRepository,
             IOrderScreenRepository orderScreenRepository,
@@ -51,8 +58,11 @@ namespace Hotline.Application.Handlers.FlowEngine
             IMediator mediator,
             ISessionContext sessionContext,
             IRepository<OrderScreenDetail> orderScreenDetailRepository,
-            IRepository<OrderTerminate> orderTerminateRepository
-			)
+            IRepository<OrderTerminate> orderTerminateRepository,
+            IOptionsSnapshot<AppConfiguration> appOptions,
+            ICalcExpireTime expireTime,
+             IRepository<OrderVisitDetail> orderVisitDetailRepository
+            )
         {
             _orderDomainService = orderDomainService;
             _orderRepository = orderRepository;
@@ -68,6 +78,9 @@ namespace Hotline.Application.Handlers.FlowEngine
             _sessionContext = sessionContext;
             _orderScreenDetailRepository = orderScreenDetailRepository;
             _orderTerminateRepository = orderTerminateRepository;
+            _appOptions = appOptions;
+            _expireTime = expireTime;
+            _orderVisitDetailRepository = orderVisitDetailRepository;
         }
 
         /// <summary>Handles a notification</summary>
@@ -170,16 +183,27 @@ namespace Hotline.Application.Handlers.FlowEngine
                         }
                         break;
                     case WorkflowModuleConsts.OrderScreen:
-	                    var workflowSteps = await _workflowDomainService.GetWorkflowAsync(workflow.Id,withSteps: true,cancellationToken:cancellationToken);
-						var screen = await _orderScreenRepository.GetAsync(workflow.ExternalId, cancellationToken);
+                        var workflowSteps = await _workflowDomainService.GetWorkflowAsync(workflow.Id, withSteps: true, cancellationToken: cancellationToken);
+                        var screen = await _orderScreenRepository.GetAsync(workflow.ExternalId, cancellationToken);
                         if (screen != null)
                         {
                             screen.Flowed(workflow.FlowedUserIds, workflow.FlowedOrgIds, workflow.HandlerUsers, workflow.HandlerOrgs);
                             screen.SendBackTime = DateTime.Now;
-                            screen.SendBackApply = workflowSteps.Steps.Any(x=>x is { Status: EWorkflowStepStatus.WaitForHandle or EWorkflowStepStatus.WaitForAccept, StepType: EStepType.Start,IsOrigin : true } );
+                            screen.SendBackApply = workflowSteps.Steps.Any(x => x is { Status: EWorkflowStepStatus.WaitForHandle or EWorkflowStepStatus.WaitForAccept, StepType: EStepType.Start, IsOrigin: true });
                             screen.Status = EScreenStatus.SendBack;
                             screen.SendBackNum++;
-							await _orderScreenRepository.UpdateAsync(screen, cancellationToken);
+                            await _orderScreenRepository.UpdateAsync(screen, cancellationToken);
+                            //自贡任务 220 修改甄别截止申请日期的计算方式
+                            //甄别截止申请日期=回访时间+2个工作日  ,若退回到申请人节点,甄别截止申请日期=退回时间+2个工作日
+                            if (screen.SendBackApply == true && _appOptions.Value.IsZiGong)
+                            {
+                                var orderVisitDetail = await _orderVisitDetailRepository.GetAsync(p => p.Id == screen.VisitDetailId, cancellationToken);
+                                if (orderVisitDetail != null)
+                                {
+                                    orderVisitDetail.ScreenByEndTime = (await _expireTime.CalcEndTime(DateTime.Now, DateTime.Now, ETimeType.WorkDay, 2, 0, 0)).EndTime;
+                                    await _orderVisitDetailRepository.UpdateAsync(orderVisitDetail, cancellationToken);
+                                }
+                            }
                         }
                         OrderScreenDetail detail = new OrderScreenDetail
                         {
@@ -192,22 +216,22 @@ namespace Hotline.Application.Handlers.FlowEngine
                     case WorkflowModuleConsts.KnowledgeUpdate:
                     case WorkflowModuleConsts.KnowledgeDelete:
                     case WorkflowModuleConsts.KnowledgeOffshelf:
-					case WorkflowModuleConsts.TelRestApply:
+                    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)
-		                    {
+                        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;
-				}
+                            }
+                            await _orderTerminateRepository.UpdateAsync(orderTerminate, cancellationToken);
+                        }
+                        break;
+                }
 
             }
             catch (Exception e)

+ 1 - 1
src/Hotline.Application/Hotline.Application.csproj

@@ -17,10 +17,10 @@
 
   <ItemGroup>
     <ProjectReference Include="..\Hotline.Ai.Jths\Hotline.Ai.Jths.csproj" />
+    <ProjectReference Include="..\Hotline.Ai.XingTang\Hotline.Ai.XingTang.csproj" />
     <ProjectReference Include="..\Hotline.Application.Contracts\Hotline.Application.Contracts.csproj" />
     <ProjectReference Include="..\Hotline.NewRock\Hotline.NewRock.csproj" />
     <ProjectReference Include="..\Hotline.Repository.SqlSugar\Hotline.Repository.SqlSugar.csproj" />
-    <ProjectReference Include="..\Hotline.WeChat\Hotline.WeChat.csproj" />
     <ProjectReference Include="..\Hotline.Wex\Hotline.Wex.csproj" />
     <ProjectReference Include="..\Hotline.XingTang\Hotline.XingTang.csproj" />
     <ProjectReference Include="..\Hotline.YbEnterprise.Sdk\Hotline.YbEnterprise.Sdk.csproj" />

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

@@ -21,6 +21,13 @@ namespace Hotline.Application.Identity
         /// <exception cref="UserFriendlyException"></exception>
         Task<TokenOutDto> GetThredTokenAsync(ThirdTokenInDto dto);
 
+        /// <summary>
+        /// 根据OpenId刷新令牌
+        /// </summary>
+        /// <param name="openId"></param>
+        /// <returns></returns>
+        Task<TokenOutDto> RefreshTokenAsync(string openId);
+
         Task<string> LoginAsync(LoginDto dto, CancellationToken cancellationToken);
 
         Task<string> OldToNewLoginAsync(HotlineLoginOldToNewDto dto, CancellationToken cancellationToken);

+ 49 - 5
src/Hotline.Application/Identity/IdentityAppService.cs

@@ -18,11 +18,13 @@ using Hotline.Share.Enums.Snapshot;
 using Hotline.Share.Enums.User;
 using Hotline.Share.Tools;
 using Hotline.Snapshot;
+using Hotline.Snapshot.Interfaces;
 using Hotline.Users;
 using IdentityModel;
 using Mapster;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.Extensions.Options;
+using SqlSugar;
 using XF.Domain.Authentications;
 using XF.Domain.Cache;
 using XF.Domain.Dependency;
@@ -48,7 +50,10 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
     private readonly IThirdIdentiyService _thirdIdentiyService;
     private readonly IThirdAccountRepository _thirdAccountRepository;
-    private readonly IRepository<GuiderInfo> _guiderInfoRepository;
+    private readonly IGuiderInfoRepository _guiderInfoRepository;
+    private readonly IVolunteerRepository _volunteerRepository;
+    private readonly ISystemLogRepository _systemLog;
+
 
     public IdentityAppService(
         IAccountRepository accountRepository,
@@ -65,7 +70,9 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
         IThirdAccountRepository thirdAccountRepository,
         ISessionContext sessionContext,
         IRepository<Citizen> citizenRepository,
-        IRepository<GuiderInfo> guiderInfoRepository)
+        IGuiderInfoRepository guiderInfoRepository,
+        IVolunteerRepository volunteerRepository,
+        ISystemLogRepository systemLog)
     {
         _accountRepository = accountRepository;
         _accountDomainService = accountDomainService;
@@ -82,6 +89,8 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
         _sessionContext = sessionContext;
         _citizenRepository = citizenRepository;
         _guiderInfoRepository = guiderInfoRepository;
+        _volunteerRepository = volunteerRepository;
+        _systemLog = systemLog;
     }
 
     public async Task<string> OldToNewLoginAsync(HotlineLoginOldToNewDto dto, CancellationToken cancellationToken)
@@ -317,16 +326,41 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
         }
         var thirdToken = await _thirdIdentiyService.GetTokenAsync(thirdDto);
         var phone = await _thirdIdentiyService.GetPhoneNumberAsync(thirdDto);
-        var thirdAccount = await _thirdAccountRepository.QueryByOpenIdAsync(thirdToken.OpenId);
+        var thirdAccount = await _thirdAccountRepository.GetByOpenIdAsync(thirdToken.OpenId);
 
         // 新用户注册
         if (thirdAccount is null)
         {
             thirdAccount = thirdToken.Adapt<ThirdAccount>();
+            thirdAccount.CitizenType = EReadPackUserType.Citizen;
             thirdAccount.PhoneNumber = phone.PhoneNumber;
+            var guider =  await _guiderInfoRepository.GetByPhoneNumberAsync(phone.PhoneNumber);
+            if (guider != null)
+            {
+                thirdAccount.CitizenType = EReadPackUserType.Guider;
+                thirdAccount.UserId = guider.Id;
+            }
+            else
+            {
+                var citizen = await _citizenRepository.Queryable().Where(m => m.PhoneNumber == phone.PhoneNumber).FirstAsync();
+                thirdAccount.UserId = citizen?.Id;
+            }
             thirdAccount.Id = await _thirdAccountRepository.AddAsync(thirdAccount);
         }
 
+        return await GetJwtToken(thirdAccount);
+    }
+
+    public async Task<TokenOutDto> RefreshTokenAsync(string openId)
+    {
+        var thirdAccount = await _thirdAccountRepository.GetByOpenIdAsync(openId)
+            ?? throw UserFriendlyException.SameMessage("未找到用户信息");
+
+        return await GetJwtToken(thirdAccount);
+    }
+
+    private async Task<TokenOutDto> GetJwtToken(ThirdAccount thirdAccount)
+    {
         var jwtOptions = _identityOptionsAccessor.Value.Jwt;
         var claims = new List<Claim>
         {
@@ -339,6 +373,16 @@ public class IdentityAppService : IIdentityAppService, IScopeDependency
         var expiredSeconds = jwtOptions.Expired <= 0 ? 3600 : jwtOptions.Expired;
         await _cacheAudience.SetAsync(audience.Id, audience, TimeSpan.FromSeconds(expiredSeconds));
         var token = _jwtSecurity.EncodeJwtToken(claims, audience.Ticket);
-        return new TokenOutDto(thirdAccount.CitizenType, token);
+        var isVolunteer = await _volunteerRepository.IsVolunteerAsync(thirdAccount.PhoneNumber);
+        return new TokenOutDto()
+        {
+            UserType = thirdAccount.CitizenType,
+            Token = token,
+            IsVolunteer = isVolunteer,
+            OpenId = thirdAccount.OpenId,
+            PhoneNumber = thirdAccount.PhoneNumber,
+            InvitationCode = thirdAccount.InvitationCode,
+            UserName = thirdAccount.UserName
+        };
     }
-  }
+}

+ 13 - 10
src/Hotline.Application/JudicialManagement/EnforcementApplication.cs

@@ -16,9 +16,9 @@ namespace Hotline.Application.JudicialManagement
     public class EnforcementApplication : IEnforcementApplication, IScopeDependency
     {
         private readonly IRepository<JudicialManagementOrders> _judicialManagementOrdersRepository;
-        private readonly IRepository<EnforcementOrdersHandler> _enforcementOrdersHandlerRepository;
         private readonly IMapper _mapper;
         private readonly ISessionContext _sessionContext;
+        private readonly IRepository<LawEnforcementAgencies> _lawEnforcementAgenciesRepository;
 
         /// <summary>
         /// 
@@ -27,16 +27,17 @@ namespace Hotline.Application.JudicialManagement
         /// <param name="enforcementOrdersHandlerRepository"></param>
         /// <param name="mapper"></param>
         /// <param name="sessionContext"></param>
+        /// <param name="lawEnforcementAgenciesRepository"></param>
         public EnforcementApplication(
            IRepository<JudicialManagementOrders> judicialManagementOrdersRepository,
-           IRepository<EnforcementOrdersHandler> enforcementOrdersHandlerRepository,
            IMapper mapper,
-           ISessionContext sessionContext)
+           ISessionContext sessionContext,
+           IRepository<LawEnforcementAgencies> lawEnforcementAgenciesRepository)
         {
             _judicialManagementOrdersRepository = judicialManagementOrdersRepository;
-            _enforcementOrdersHandlerRepository = enforcementOrdersHandlerRepository;
             _mapper = mapper;
             _sessionContext = sessionContext;
+            _lawEnforcementAgenciesRepository = lawEnforcementAgenciesRepository;
         }
 
         /// <summary>
@@ -62,7 +63,8 @@ namespace Hotline.Application.JudicialManagement
                    .WhereIF(dto.CreationTimeEnd.HasValue, d => d.CreationTime <= dto.CreationTimeEnd) //受理时间结束
                      .WhereIF(!string.IsNullOrEmpty(dto.FromName), d => d.FromName == dto.FromName) //来电人姓名
                      .WhereIF(!string.IsNullOrEmpty(dto.AreaCode), d => d.AreaCode == dto.AreaCode) //区域
-                     .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => SqlFunc.JsonListObjectAny(d.EnforcementOrdersHandler, "Value", dto.OrgCode))//执法部门
+                      .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => SqlFunc.JsonListObjectAny(d.EnforcementOrdersHandler, "Value", dto.OrgCode))//接办部门
+                       .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => SqlFunc.JsonListObjectAny(d.LawEnforcementAgencies, "Value", dto.LawEnforcementOrgCode))//执法部门
                      .OrderByDescending(d => d.CreationTime)
                     .MergeTable();
         }
@@ -97,9 +99,10 @@ namespace Hotline.Application.JudicialManagement
                       .WhereIF(!string.IsNullOrEmpty(dto.NameOrNo), d => d.AcceptorName.Contains(dto.NameOrNo!) || d.AcceptorStaffNo.Contains(dto.NameOrNo!)) //受理人/坐席
                       .WhereIF(dto.CreationTimeStart.HasValue, d => d.CreationTime >= dto.CreationTimeStart) //受理时间开始
                      .WhereIF(dto.CreationTimeEnd.HasValue, d => d.CreationTime <= dto.CreationTimeEnd) //受理时间结束
-                       .WhereIF(!string.IsNullOrEmpty(dto.FromName), d => d.FromName == dto.FromName) //来电人姓名
-                       .WhereIF(!string.IsNullOrEmpty(dto.AreaCode), d => d.AreaCode == dto.AreaCode) //区域
-                       .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => SqlFunc.JsonListObjectAny(d.EnforcementOrdersHandler, "Value", dto.OrgCode))//执法部门
+                     .WhereIF(!string.IsNullOrEmpty(dto.FromName), d => d.FromName == dto.FromName) //来电人姓名
+                     .WhereIF(!string.IsNullOrEmpty(dto.AreaCode), d => d.AreaCode == dto.AreaCode) //区域
+                     .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => SqlFunc.JsonListObjectAny(d.EnforcementOrdersHandler, "Value", dto.OrgCode))//接办部门
+                       .WhereIF(!string.IsNullOrEmpty(dto.OrgCode), d => SqlFunc.JsonListObjectAny(d.LawEnforcementAgencies, "Value", dto.LawEnforcementOrgCode))//执法部门
                        .OrderByDescending(d => d.CreationTime)
                       .MergeTable();
 
@@ -137,7 +140,7 @@ namespace Hotline.Application.JudicialManagement
         /// <returns></returns>
         public ISugarQueryable<EmDepartmentalProcessingStatisticsDto> GetDepartmentalProcessingStatisticsAsync(DateTime StartTime, DateTime EndTime)
         {
-            return _enforcementOrdersHandlerRepository.Queryable()
+            return _lawEnforcementAgenciesRepository.Queryable()
                    .LeftJoin<JudicialManagementOrders>((h, o) => h.OrderId == o.Id)
                    .Where((h, o) => o.CreationTime >= StartTime && o.CreationTime <= EndTime && o.IsItCounted == true)
                     .GroupBy((h, o) => new
@@ -164,7 +167,7 @@ namespace Hotline.Application.JudicialManagement
         /// <returns></returns>
         public ISugarQueryable<EnforcementOrderListDto> GetDepartmentalProcessingStatisticsOrderListAsync(QueryDepartmentalProcessingStatisticsDto dto)
         {
-            return _enforcementOrdersHandlerRepository.Queryable()
+            return _lawEnforcementAgenciesRepository.Queryable()
                  .LeftJoin<JudicialManagementOrders>((x, o) => x.OrderId == o.Id)
                  .Where((x, o) => o.CreationTime >= dto.StartTime && o.CreationTime <= dto.EndTime && o.Id != null && o.IsItCounted == true)
                  .WhereIF(!string.IsNullOrEmpty(dto.OrgCode) && dto.OrgCode == "001", (x, o) => x.OrgCode == dto.OrgCode)

+ 1 - 0
src/Hotline.Application/Knowledge/KnowApplication.cs

@@ -351,6 +351,7 @@ namespace Hotline.Application.Knowledge
                 .Includes(x => x.User)
                 .Includes(x => x.SystemOrganize)
                 .Includes(x => x.HotspotType)
+                .Includes(x => x.KnowledgeType)
                 .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))

+ 7 - 0
src/Hotline.Application/Mappers/MapperConfigs.cs

@@ -20,6 +20,7 @@ using Hotline.Share.Enums.Order;
 using Hotline.Snapshot;
 using Mapster;
 using XF.Domain.Entities;
+using Hotline.Share.Tools;
 
 namespace Hotline.Application.Mappers
 {
@@ -27,6 +28,12 @@ namespace Hotline.Application.Mappers
     {
         public void Register(TypeAdapterConfig config)
         {
+            config.ForType<SystemLog, SystemLogDto>()
+                .Map(dest => dest.ExecuteParam,
+                src => src.ExecuteParam == null 
+                ? null
+                : src.ExecuteParam.ToJson().FromJson<Dictionary<string, object>>());
+
             config.ForType<ExcelContent, Order>()
                 .Map(d => d.FirstVisitResult, x => x.VisitResult)
                 .IgnoreNullValues(true);

+ 0 - 2
src/Hotline.Application/Mappers/OrderMapperConfigs.cs

@@ -265,7 +265,5 @@ public class OrderMapperConfigs : IRegister
             .IgnoreIf((s, d) => s.OrgHandledAttitude == null, d => d.OrgHandledAttitude)
             .Map(d => d.OrgHandledAttitude, s => s.OrgHandledAttitude.Value)
             ;
-
-
     }
 }

+ 74 - 1
src/Hotline.Application/Mappers/SnapshotMapperConfigs.cs

@@ -1,4 +1,10 @@
-using Hotline.Share.Dtos.Snapshot;
+using AngleSharp.Text;
+using Hotline.Orders;
+using Hotline.Settings;
+using Hotline.Share.Dtos;
+using Hotline.Share.Dtos.Snapshot;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Tools;
 using Hotline.Snapshot;
 using Mapster;
 using System;
@@ -12,11 +18,78 @@ public class SnapshotMapperConfigs : IRegister
 {
     public void Register(TypeAdapterConfig config)
     {
+        config.ForType<AddSnapshotOrderInDto, OrderSnapshot>()
+           .Ignore(m => m.StartWorkTime)
+           .Ignore(m => m.EndWorkTime);
+
+        config.ForType<SystemDicData, Kv>()
+            .Map(m => m.Key, b => b.DicDataValue)
+            .Map(m => m.Value, b => b.DicDataName);
+
+        config.ForType<SystemArea, Kv>()
+            .Map(m => m.Key, n => n.Id)
+            .Map(m => m.Value, n => n.AreaName);
+
+        config.ForType< SnapshotBulletin, SnapshotBulletinDetailOutDto>()
+         .Map(m => m.BulletinTypeId, n => n.SnapshotBulletinTypeId)
+         .Map(m => m.BulletinTypeName, n => n.SnapshotBulletinTypeName);
+
+        config.ForType<UpdateSnapshotBulletinInDto, SnapshotBulletin>()
+            .Map(m => m.SnapshotBulletinTypeId, n => n.BulletinTypeId)
+            .Map(m => m.SnapshotBulletinTypeName, n => n.BulletinTypeName);
+
+        config.ForType<AddSnapshotBulletinInDto, SnapshotBulletin>()
+            .Map(m => m.SnapshotBulletinTypeId, n => n.BulletinTypeId)
+            .Map(m => m.SnapshotBulletinTypeName, n => n.BulletinTypeName);
+
+        config.ForType<GuiderSystemInDto, CommunityInfo>()
+            .Map(dest => dest.Name, src => src.OrgName)
+            .Map(dest => dest.ParentCode, src => src.ParentOrgId)
+            .Map(dest => dest.Id, src => src.OrgId)
+            .Map(dest => dest.FullName, src => src.OrgFullName)
+            .AfterMapping((dest, src) => 
+            {
+                src.Id = dest.OrgId;
+            });
+
         config.ForType<AddIndustryDto, Industry>();
         config.ForType<Hotline.File.File, IndustryFileDto>()
             .Map(m => m.AdditionId, n => n.Additions);
 
         config.ForType<IndustryFileDto, Hotline.File.File>()
             .Map(m => m.Additions, n => n.AdditionId);
+
+        config.ForType<SnapshotFileInDto, File.File>()
+            .Map(m => m.Additions, n => n.AdditionId);
+
+        config.ForType<AddSnapshotOrderInDto, Order>()
+            .Map(m => m.Contact, n => n.PhoneNumber)
+            .Map(m => m.FromName, n => n.Name)
+            .Map(m => m.FromPhone, n => n.PhoneNumber);
+
+        config.ForType<SnapshotOrderPublish, OrderPublishDetailOutDto>()
+            .Map(m => m.Title, n => n.ArrangeTitle)
+            .Map(m => m.Content, n => n.ArrangeContent)
+            .Map(m => m.Opinion, n => n.ArrangeOpinion);
+
+        config.ForType<Order, OrderPublishDetailOutDto>()
+            .Map(m => m.Opinion, n => n.ActualOpinion);
+
+        config.ForType<AddBatchPractitionerInDto, Practitioner>()
+            .Ignore(m => m.Gender);
+
+        config.ForType<GuiderSystemInDto, OrderSnapshot>()
+            .Map(m => m.ReplyDate, n => n.ReplyDate)
+            .Map(m => m.ReplyUserName, n => n.ReplyUserName)
+            .Map(m => m.ReplyBMName, n => n.ReplyBMName)
+            .Map(m => m.ReplyResultType, n => n.ReplyResultType)
+            .Map(m => m.IsTruth, n => n.ReplyISTrue == "1")
+            .Map(m => m.IsDeal, n => n.ReplyResultType == "2")
+            .Map(m => m.IsRepetition, n => n.IsRepeat == "1")
+            .Map(m => m.IsDanger, n => n.IsHiddenDanger == "1")
+            .Map(m => m.MemberName, n => n.MemberName)
+            .Map(m => m.MemberMobile, n => n.MemberMobile)
+            .Map(m => m.CommunityId, n => n.OrgId)
+            .Map(m => m.NetworkRemark, n => n.ReplyContent);
     }
 }

+ 1 - 1
src/Hotline.Application/Handlers/Order/AddOrderPushMessageNotifyHandler.cs → src/Hotline.Application/Orders/Handles/OrderHandler/AddOrderPushMessageNotifyHandler.cs

@@ -6,7 +6,7 @@ using Hotline.Share.Enums.Push;
 using MediatR;
 using Microsoft.Extensions.Logging;
 
-namespace Hotline.Application.Handlers.Order
+namespace Hotline.Application.Orders.Handles.Order
 {
     /// <summary>
     /// 新增工单发送短信

+ 1 - 1
src/Hotline.Application/Handlers/Order/GetOrderDetailNotifyHandler.cs → src/Hotline.Application/Orders/Handles/OrderHandler/GetOrderDetailNotifyHandler.cs

@@ -7,7 +7,7 @@ using Hotline.FlowEngine.Workflows;
 using Hotline.Orders.Notifications;
 using MediatR;
 
-namespace Hotline.Application.Handlers.Order
+namespace Hotline.Application.Orders.Handles.Order
 {
     public class GetOrderDetailNotifyHandler : INotificationHandler<GetOrderDetailNotify>
     {

+ 1 - 1
src/Hotline.Application/Handlers/Order/OrderRelateCallHandler.cs → src/Hotline.Application/Orders/Handles/OrderHandler/OrderRelateCallHandler.cs

@@ -12,7 +12,7 @@ using System.Threading.Tasks;
 using XF.Domain.Dependency;
 using XF.Domain.Repository;
 
-namespace Hotline.Application.Handlers.Order
+namespace Hotline.Application.Orders.Handles.Order
 {
     public class OrderRelateCallHandler : ICapSubscribe, ITransientDependency
     {

+ 1 - 1
src/Hotline.Application/Handlers/Order/OrderVisitSmsHandler.cs → src/Hotline.Application/Orders/Handles/OrderHandler/OrderVisitSmsHandler.cs

@@ -17,7 +17,7 @@ using MediatR;
 using XF.Domain.Dependency;
 using XF.Domain.Repository;
 
-namespace Hotline.Application.Handlers.Order;
+namespace Hotline.Application.Orders.Handles.Order;
 public class OrderVisitSmsHandler : INotificationHandler<ReceiveMessageNotify>
 {
     private readonly IOrderVisitDomainService _orderVisitDomainService;

+ 3 - 3
src/Hotline.Application/Handlers/Order/TranspondCityNotifyHandler.cs → src/Hotline.Application/Orders/Handles/OrderHandler/TranspondCityNotifyHandler.cs

@@ -13,9 +13,9 @@ using System;
 using Hotline.DI;
 using XF.Domain.Repository;
 
-namespace Hotline.Application.Handlers.Order
+namespace Hotline.Application.Orders.Handles.Order
 {
-   // [Injection(AppScopes = EAppScope.YiBin)]
+    // [Injection(AppScopes = EAppScope.YiBin)]
     public class TranspondCityNotifyHandler : INotificationHandler<OrderStartWorkflowNotify>
     {
         private readonly IMapper _mapper;
@@ -63,7 +63,7 @@ namespace Hotline.Application.Handlers.Order
                     if (order.Transpond.HasValue && order.Transpond.Value)
                     {
                         var orderDto = _mapper.Map<OrderDto>(order);
-                        await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderTranspondCity, orderDto);
+                        await _capPublisher.PublishAsync(Share.Mq.EventNames.HotlineOrderTranspondCity, orderDto);
                         //保存本地数据
                         TranspondCityRawData cityRawData = new TranspondCityRawData
                         {

+ 1 - 1
src/Hotline.Application/Handlers/Order/UpdateOrderPushMessageNotifyHandler.cs → src/Hotline.Application/Orders/Handles/OrderHandler/UpdateOrderPushMessageNotifyHandler.cs

@@ -6,7 +6,7 @@ using Hotline.Share.Enums.Push;
 using MediatR;
 using Microsoft.Extensions.Logging;
 
-namespace Hotline.Application.Handlers.Order
+namespace Hotline.Application.Orders.Handles.Order
 {
     public class UpdateOrderPushMessageNotifyHandler : INotificationHandler<UpdateOrderNotify>
     {

+ 9 - 9
src/Hotline.Application/Orders/OrderScreenHandler/OrderScreenEndWorkflowHandler.cs → src/Hotline.Application/Orders/Handles/OrderScreenHandler/OrderScreenEndWorkflowHandler.cs

@@ -1,25 +1,25 @@
 using DotNetCore.CAP;
+using Hotline.Configurations;
 using Hotline.FlowEngine.Notifications;
 using Hotline.FlowEngine.WorkflowModules;
 using Hotline.Orders;
+using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.Order;
 using Hotline.Share.Enums.Order;
 using MapsterMapper;
 using MediatR;
-using Hotline.Share.Dtos;
-using XF.Domain.Repository;
-using XF.Domain.Authentications;
-using Hotline.Configurations;
 using Microsoft.Extensions.Options;
+using XF.Domain.Authentications;
+using XF.Domain.Repository;
 
-namespace Hotline.Application.Orders.OrderScreenHandler;
+namespace Hotline.Application.Orders.Handles.OrderScreenHandler;
 public class OrderScreenEndWorkflowHandler : INotificationHandler<EndWorkflowNotify>
 {
     private readonly IOrderRepository _orderRepository;
     private readonly ICapPublisher _capPublisher;
     private readonly IMapper _mapper;
     private readonly IRepository<OrderVisitDetail> _orderVisitedDetailRepository;
-    private readonly IRepository<OrderScreen> _orderScreenRepository;
+    private readonly IRepository<Hotline.Orders.OrderScreen> _orderScreenRepository;
     private readonly IRepository<OrderVisit> _orderVisitRepository;
     private readonly ISessionContext _sessionContext;
     private readonly IRepository<OrderScreenDetail> _orderScreenDetailRepository;
@@ -30,7 +30,7 @@ public class OrderScreenEndWorkflowHandler : INotificationHandler<EndWorkflowNot
         ICapPublisher capPublisher,
         IMapper mapper,
         IRepository<OrderVisitDetail> orderVisitedDetailRepository,
-        IRepository<OrderScreen> orderScreenRepository,
+        IRepository<Hotline.Orders.OrderScreen> orderScreenRepository,
         IRepository<OrderVisit> orderVisitRepository,
         ISessionContext sessionContext,
         IRepository<OrderScreenDetail> orderScreenDetailRepository,
@@ -97,7 +97,7 @@ public class OrderScreenEndWorkflowHandler : INotificationHandler<EndWorkflowNot
                                 //获取回访明细
                                 var visitDe = visit.OrderVisitDetails.First(x => x.Id == screen.VisitDetailId);
                                 //推省上
-                                await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderScreenApplyed,
+                                await _capPublisher.PublishAsync(Share.Mq.EventNames.HotlineOrderScreenApplyed,
                                     new PublishVisitDto()
                                     {
                                         Order = _mapper.Map<OrderDto>(visit.Order),
@@ -113,7 +113,7 @@ public class OrderScreenEndWorkflowHandler : INotificationHandler<EndWorkflowNot
                                     });
 
                                 //推门户
-                                await _capPublisher.PublishAsync(Hotline.Share.Mq.EventNames.HotlineOrderVisitedWeb, new PublishVisitAllDto()
+                                await _capPublisher.PublishAsync(Share.Mq.EventNames.HotlineOrderVisitedWeb, new PublishVisitAllDto()
                                 {
                                     Id = visit.Id,
                                     Order = _mapper.Map<OrderDto>(visit.Order),

+ 101 - 0
src/Hotline.Application/Orders/Handles/OrderScreenHandler/OrderScreenNextWorkflowHandler.cs

@@ -0,0 +1,101 @@
+using DotNetCore.CAP;
+using Hotline.Configurations;
+using Hotline.FlowEngine.Notifications;
+using Hotline.FlowEngine.WorkflowModules;
+using Hotline.Orders;
+using Hotline.Share.Dtos.Order;
+using Hotline.Share.Enums.Order;
+using Hotline.Share.Mq;
+using MapsterMapper;
+using MediatR;
+using Microsoft.Extensions.Options;
+using XF.Domain.Authentications;
+using XF.Domain.Extensions;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Orders.Handles.OrderScreen;
+public class OrderScreenNextWorkflowHandler : INotificationHandler<NextStepNotify>
+{
+    private readonly ICapPublisher _capPublisher;
+    private readonly IMapper _mapper;
+    private readonly IOrderScreenRepository _orderScreenRepository;
+    private readonly ISessionContext _sessionContext;
+    private readonly IRepository<OrderScreenDetail> _orderScreenDetailRepository;
+    private readonly IOptionsSnapshot<AppConfiguration> _appOptions;
+
+    public OrderScreenNextWorkflowHandler(
+        ICapPublisher capPublisher,
+        IMapper mapper,
+        IOrderScreenRepository orderScreenRepository,
+        ISessionContext sessionContext,
+        IOptionsSnapshot<AppConfiguration> appOptions,
+        IRepository<OrderScreenDetail> orderScreenDetailRepository)
+    {
+        _capPublisher = capPublisher;
+        _mapper = mapper;
+        _orderScreenRepository = orderScreenRepository;
+        _sessionContext = sessionContext;
+        _orderScreenDetailRepository = orderScreenDetailRepository;
+        _appOptions = appOptions;
+    }
+
+    /// <summary>Handles a notification</summary>
+    /// <param name="notification">The notification</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    public async Task Handle(NextStepNotify notification, CancellationToken cancellationToken)
+    {
+        if (notification.Workflow.ModuleCode == WorkflowModuleConsts.OrderScreen)
+        {
+            var workflow = notification.Workflow;
+            var nextTag = notification.NextStepDefine.Tag.TryDeserialize<DefinitionTag>();
+            var screen = await _orderScreenRepository.Queryable().Includes(x => x.Order)
+                .Where(x => x.Id == workflow.ExternalId).FirstAsync(cancellationToken);
+            if (screen != null)
+            {
+                screen.Status = EScreenStatus.Approval;
+                screen.Flowed(workflow.FlowedUserIds, workflow.FlowedOrgIds, workflow.HandlerUsers, workflow.HandlerOrgs);
+                //如果下个节点是省审批,则修改为省甄别
+                if (nextTag is not null && nextTag.Type == TagDefaults.TagType.Org && nextTag.Value == TagDefaults.TagValue.Province)
+                    screen.IsProScreen = true;
+                await _orderScreenRepository.UpdateAsync(screen, cancellationToken);
+            }
+
+            if (nextTag is not null && nextTag.Type == TagDefaults.TagType.Org)
+            {
+                switch (nextTag.Value)
+                {
+                    case TagDefaults.TagValue.Province:
+                        if (screen != null)
+                        {
+                            var screenDto = _mapper.Map<OrderScreenListDto>(screen);
+                            if (screen.Order != null && screen.Order.Source == ESource.ProvinceStraight)
+                            {
+                                var screenOrderDto = _mapper.Map<OrderDto>(screen.Order);
+                                //省件甄别--以省审批前一个节点整理的甄别意见为准推送省上 宜宾
+                                if (_appOptions.Value.IsYiBin)
+                                {
+                                    screenDto.Content = notification.Dto.Opinion;
+                                    screenDto.Files = new List<Share.Dtos.File.FileDto>();
+                                }
+                                //推省上
+                                _capPublisher.Publish(EventNames.HotlineOrderScreenApply, new PublishScreenDto()
+                                {
+                                    Order = screenOrderDto,
+                                    Screen = screenDto,
+                                    ClientGuid = ""
+                                });
+                            }
+                        }
+
+                        break;
+                }
+            }
+            OrderScreenDetail detail = new OrderScreenDetail
+            {
+                ScreenId = screen.Id
+            };
+            detail.Audit(_sessionContext.UserId, _sessionContext.UserName, _sessionContext.OrgId, _sessionContext.OrgName, 1);
+            await _orderScreenDetailRepository.AddAsync(detail, cancellationToken);
+        }
+    }
+}

+ 37 - 0
src/Hotline.Application/Orders/Handles/OrderScreenHandler/OrderScreenStartWorkflowHandler.cs

@@ -0,0 +1,37 @@
+using Hotline.FlowEngine.Notifications;
+using Hotline.FlowEngine.WorkflowModules;
+using Hotline.Orders;
+using MediatR;
+
+namespace Hotline.Application.Orders.Handles.OrderScreen;
+
+public class OrderScreenStartWorkflowHandler : INotificationHandler<StartWorkflowNotify>
+{
+    private readonly IOrderScreenRepository _orderScreenRepository;
+
+    public OrderScreenStartWorkflowHandler(
+        IOrderScreenRepository orderScreenRepository
+    )
+    {
+        _orderScreenRepository = orderScreenRepository;
+    }
+
+    /// <summary>Handles a notification</summary>
+    /// <param name="notification">The notification</param>
+    /// <param name="cancellationToken">Cancellation token</param>
+    public async Task Handle(StartWorkflowNotify notification, CancellationToken cancellationToken)
+    {
+        if (notification.Workflow.ModuleCode == WorkflowModuleConsts.OrderScreen)
+        {
+            var workflow = notification.Workflow;
+            var screen = await _orderScreenRepository.Queryable().Includes(x => x.Order)
+                .Where(x => x.Id == workflow.ExternalId).FirstAsync(cancellationToken);
+            if (screen != null)
+            {
+                screen.WorkflowId = workflow.Id;
+                screen.Flowed(workflow.FlowedUserIds, workflow.FlowedOrgIds, workflow.HandlerUsers, workflow.HandlerOrgs);
+                await _orderScreenRepository.UpdateAsync(screen, cancellationToken);
+            }
+        }
+    }
+}

+ 63 - 0
src/Hotline.Application/Orders/Handles/SnapshotHandler/GuiderSystemTimeoutHandler.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.Application.Orders;
+using Hotline.Authentications;
+using Hotline.Caching.Interfaces;
+using Hotline.EventBus;
+using Hotline.FlowEngine.Notifications;
+using Hotline.FlowEngine.Workflows;
+using Hotline.Orders;
+using Hotline.Settings;
+using Hotline.Share.Dtos.FlowEngine;
+using Hotline.Share.Enums.FlowEngine;
+using Hotline.Share.Tools;
+using Hotline.Snapshot.Notifications;
+using MediatR;
+using XF.Domain.Exceptions;
+
+namespace Hotline.Application.Orders.Handles.Snapshot
+{
+    /// <summary>
+    /// 需求:坐席派给网格员的安全隐患工单若未推送成功超过4小时或者网格员超过4小时没回复,则自动流转到标注节点待标注列表
+    /// </summary>
+    public class GuiderSystemTimeoutHandler : INotificationHandler<GuiderSystemTimeOutBackNotification>
+    {
+        private readonly ISystemSettingCacheManager _systemSettingCacheManager;
+        private readonly IOrderApplication _orderApplication;
+        private readonly ISystemLogRepository _systemLogRepository;
+
+        public GuiderSystemTimeoutHandler(
+            ISystemSettingCacheManager systemSettingCacheManager,
+            IOrderApplication orderApplication
+,
+            ISystemLogRepository systemLogRepository)
+        {
+            _systemSettingCacheManager = systemSettingCacheManager;
+            _orderApplication = orderApplication;
+            _systemLogRepository = systemLogRepository;
+        }
+
+        /// <summary>Handles a notification</summary>
+        /// <param name="notification">The notification</param>
+        /// <param name="cancellationToken">Cancellation token</param>
+        public async Task Handle(GuiderSystemTimeOutBackNotification notification, CancellationToken cancellationToken)
+        {
+            try
+            {
+                if (_systemSettingCacheManager.Snapshot)
+                {
+                    await _orderApplication.HandleFromWanggeyuanToMaskAsync(notification.OrderId, cancellationToken);
+                }
+            }
+            catch (Exception e)
+            {
+                _systemLogRepository.Add("网格员超时未回复", notification.OrderId, "方法异常", status: 0, executeResult: e.ToJson());
+                throw;
+            }
+            _systemLogRepository.Add("网格员超时未回复", notification.OrderId, "收到事件", "", 1);
+        }
+    }
+}

+ 41 - 10
src/Hotline.Application/Orders/IOrderApplication.cs

@@ -2,6 +2,7 @@
 using Hotline.FlowEngine.Workflows;
 using Hotline.Orders;
 using Hotline.Settings;
+using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.DataSharing.PusherHotlineDto;
 using Hotline.Share.Dtos.FlowEngine;
 using Hotline.Share.Dtos.Order;
@@ -9,6 +10,7 @@ using Hotline.Share.Dtos.Order.Publish;
 using Hotline.Share.Enums.Settings;
 using Hotline.Share.Requests;
 using SqlSugar;
+using XF.Domain.Authentications;
 
 namespace Hotline.Application.Orders
 {
@@ -57,7 +59,7 @@ namespace Hotline.Application.Orders
         ISugarQueryable<Order> GetAboutToExpireAsync(AboutToExpireListDto dto);
 
         //Task<PagedDto<WorkflowOrderDto>> GetAboutToExpireNodeAsync(AboutToExpireListDto dto, CancellationToken cancellationToken);
-        Task OrderParticiple(string inputStr, string orderId, DateTime time, CancellationToken cancellationToken);
+        Task OrderParticiple(string inputStr, string orderId, string no, string title, DateTime time, CancellationToken cancellationToken);
         Task OrderSensitiveParticiple(string inputStr, string orderId, CancellationToken cancellationToken);
         /// <summary>
         /// 接收外部平台工单
@@ -330,11 +332,11 @@ namespace Hotline.Application.Orders
         ISugarQueryable<OrderVisitDetail> MayScreenList(MayScreenListDto dto);
 
 
-		ISugarQueryable<Order> QueryWaitedForSeat(QueryOrderWaitedDto dto);
+        ISugarQueryable<Order> QueryWaitedForSeat(QueryOrderWaitedDto dto);
 
 
-		ISugarQueryable<Order> QueryWaited(QueryOrderWaitedDto dto);
-        
+        ISugarQueryable<Order> QueryWaited(QueryOrderWaitedDto dto);
+
 
         /// <summary>
         /// 受理类型前10
@@ -356,12 +358,12 @@ namespace Hotline.Application.Orders
         /// <returns></returns>
         ISugarQueryable<OrderVisit> QueryOrderVisitList(QueryOrderVisitDto dto);
 
-		/// <summary>
-		/// 热点类型小类统计明细
-		/// </summary>
-		/// <param name="dto"></param>
-		/// <returns></returns>
-		ISugarQueryable<Order> HotspotStatisticsDetail(HotspotStatisticsRep dto);
+        /// <summary>
+        /// 热点类型小类统计明细
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<Order> HotspotStatisticsDetail(HotspotStatisticsRep dto);
 
         /// <summary>
         /// 坐席总体满意度分析
@@ -384,5 +386,34 @@ namespace Hotline.Application.Orders
         /// <returns></returns>
         ISugarQueryable<OrderVisitDetail> QuerySeatSatisfactionOrderVisitList(SeatSatisfactionOrderVisitRequest dto);
 
+        /// <summary>
+        /// 工单热词分析
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<OrderTsDetailsDto> QueryOrderTsDetailsList(PagedKeywordRequest dto);
+
+        /// <summary>
+        /// 知识库引用
+        /// </summary>
+        /// <param name="orderId"></param>
+        /// <param name="title"></param>
+        /// <param name="no"></param>
+        /// <param name="knowledgeQuote"></param>
+        /// <returns></returns>
+        Task AddKnowledgeQuote(string orderId, string title, string no, List<Kv> knowledgeQuote, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// 知识库引用
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<OrderTsDetailsDto> QueryKnowledgeQuoteList(PagedKeywordRequest dto);
+
+        /// <summary>
+        /// 将工单从网格员节点办理至工单标记节点
+        /// </summary>
+        /// <returns></returns>
+        Task HandleFromWanggeyuanToMaskAsync(string orderId, CancellationToken cancellation);
     }
 }

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels