qinchaoyue 7 месяцев назад
Родитель
Сommit
e193663b99
44 измененных файлов с 1711 добавлено и 128 удалено
  1. 1 0
      .gitignore
  2. 14 0
      Hotline.sln
  3. 6 4
      src/Hotline.Api/Controllers/OrderController.cs
  4. 7 0
      src/Hotline.Api/StartupExtensions.cs
  5. 27 0
      src/Hotline.Application.Tests/Application/ExpireTimeHandlerTest.cs
  6. 159 0
      src/Hotline.Application.Tests/Application/SnapshotApplicationTest.cs
  7. 54 0
      src/Hotline.Application.Tests/DefaultHttpContextAccessor.cs
  8. 79 0
      src/Hotline.Application.Tests/Domain/YiBinExpireTimeTest.cs
  9. 121 0
      src/Hotline.Application.Tests/Domain/ZiGongExpireTimeTest.cs
  10. 47 0
      src/Hotline.Application.Tests/Hotline.Application.Tests.csproj
  11. 2 0
      src/Hotline.Application.Tests/ReadME.md
  12. 201 0
      src/Hotline.Application.Tests/Startup.cs
  13. 174 0
      src/Hotline.Application.Tests/appsettings.Development.json
  14. 11 0
      src/Hotline.Application/Mappers/MapperConfigs.cs
  15. 28 0
      src/Hotline.Repository.SqlSugar/ExpireTime/TimeLimitSettingAttributeRepository.cs
  16. 24 0
      src/Hotline.Repository.SqlSugar/ExpireTime/TimeLimitSettingInventoryRepository.cs
  17. 21 0
      src/Hotline.Repository.SqlSugar/ExpireTime/TimeLimitSettingRepository.cs
  18. 6 0
      src/Hotline.Share/Dtos/Settings/IsFuzzyQueryAttribute.cs
  19. 6 0
      src/Hotline.Share/Dtos/Settings/NoCodeAttribute.cs
  20. 73 2
      src/Hotline.Share/Dtos/Settings/TimeConfig.cs
  21. 2 2
      src/Hotline.Share/Enums/Settings/ETimeType.cs
  22. 2 0
      src/Hotline.Share/Hotline.Share.csproj
  23. 29 0
      src/Hotline.Share/Tools/ObjectExtensions.cs
  24. 8 0
      src/Hotline.Share/Tools/ServiceLocator.cs
  25. 17 0
      src/Hotline.Share/Tools/TypeExtensions.cs
  26. 33 26
      src/Hotline/Orders/Order.cs
  27. 1 0
      src/Hotline/SeedData/TimeLimitSettingSeedData.cs
  28. 6 0
      src/Hotline/Settings/SystemDicData.cs
  29. 22 6
      src/Hotline/Settings/TimeLimitDomain/ExpireTimeHandler.cs
  30. 61 46
      src/Hotline/Settings/TimeLimitDomain/ExpireTimeLimitBase.cs
  31. 16 0
      src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/DaySupplier.cs
  32. 26 0
      src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/ExpireTimeFactory.cs
  33. 16 0
      src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/HourSupplier.cs
  34. 13 0
      src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/IExpireTimeSupplier.cs
  35. 120 0
      src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/WorkDaySupplier.cs
  36. 10 3
      src/Hotline/Settings/TimeLimitDomain/ICalcExpireTime.cs
  37. 7 3
      src/Hotline/Settings/TimeLimitDomain/IExpireTimeHandler.cs
  38. 15 0
      src/Hotline/Settings/TimeLimitDomain/Repository/ITimeLimitSettingAttributeRepository.cs
  39. 12 0
      src/Hotline/Settings/TimeLimitDomain/Repository/ITimeLimitSettingInventoryRepository.cs
  40. 12 0
      src/Hotline/Settings/TimeLimitDomain/Repository/ITimeLimitSettingRepository.cs
  41. 5 6
      src/Hotline/Settings/TimeLimitDomain/YiBinExpireTimeLimit.cs
  42. 114 0
      src/Hotline/Settings/TimeLimitDomain/ZiGongExpireTimeLimit.cs
  43. 102 29
      src/Hotline/Settings/TimeLimitSetting.cs
  44. 1 1
      src/XF.Domain/Dependency/DependencyInjectionExtensions.cs

+ 1 - 0
.gitignore

@@ -21,6 +21,7 @@
 [Rr]eleases/
 x64/
 x86/
+TestResult/
 [Aa][Rr][Mm]/
 [Aa][Rr][Mm]64/
 bld/

+ 14 - 0
Hotline.sln

@@ -51,6 +51,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hotline.YbEnterprise.Sdk",
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XingTang.Sdk", "src\XingTang.Sdk\XingTang.Sdk.csproj", "{CF2A8B80-FF4E-4291-B383-D735BB629F32}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hotline.XingTang", "src\Hotline.XingTang\Hotline.XingTang.csproj", "{9F99C272-5BC2-452C-9D97-BC756AF04669}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hotline.Application.Tests", "src\Hotline.Application.Tests\Hotline.Application.Tests.csproj", "{801A8807-F95E-428B-B8C3-3F9244B9E080}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -129,6 +133,14 @@ Global
 		{CF2A8B80-FF4E-4291-B383-D735BB629F32}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{CF2A8B80-FF4E-4291-B383-D735BB629F32}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{CF2A8B80-FF4E-4291-B383-D735BB629F32}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9F99C272-5BC2-452C-9D97-BC756AF04669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9F99C272-5BC2-452C-9D97-BC756AF04669}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9F99C272-5BC2-452C-9D97-BC756AF04669}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9F99C272-5BC2-452C-9D97-BC756AF04669}.Release|Any CPU.Build.0 = Release|Any CPU
+		{801A8807-F95E-428B-B8C3-3F9244B9E080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{801A8807-F95E-428B-B8C3-3F9244B9E080}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{801A8807-F95E-428B-B8C3-3F9244B9E080}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{801A8807-F95E-428B-B8C3-3F9244B9E080}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -156,6 +168,8 @@ Global
 		{1634234A-379C-44DC-BFEC-7BBDEF50B47B} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
 		{C3F289D5-C50B-46DB-852C-9543EF9B0355} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
 		{CF2A8B80-FF4E-4291-B383-D735BB629F32} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
+		{9F99C272-5BC2-452C-9D97-BC756AF04669} = {D041C554-B78E-4AAF-B597-E309DC8EEF4F}
+		{801A8807-F95E-428B-B8C3-3F9244B9E080} = {08D63205-1445-430F-A4AB-EF1744E3AC11}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {4B8EA790-BD13-4422-8D63-D6DBB77B823F}

+ 6 - 4
src/Hotline.Api/Controllers/OrderController.cs

@@ -43,9 +43,11 @@ using Hotline.Share.Enums.Push;
 using Hotline.Share.Enums.Quality;
 using Hotline.Share.Enums.Settings;
 using Hotline.Share.Requests;
+using Hotline.Share.Tools;
 using Hotline.Tools;
 using Hotline.Users;
 using Hotline.YbEnterprise.Sdk;
+using Mapster;
 using MapsterMapper;
 using MediatR;
 using Microsoft.AspNetCore.Authorization;
@@ -86,7 +88,7 @@ public class OrderController : BaseController
     private readonly IRepository<OrderVisitDetail> _orderVisitedDetailRepository;
     private readonly ICapPublisher _capPublisher;
     private readonly IOrderDelayRepository _orderDelayRepository;
-    //private readonly ITimeLimitDomainService _timeLimitDomainService;
+    private readonly ITimeLimitDomainService _timeLimitDomainService;
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
     private readonly IRepository<OrderRedo> _orderRedoRepository;
     private readonly IRepository<OrderSupervise> _orderSuperviseRepository;
@@ -3142,7 +3144,7 @@ public class OrderController : BaseController
         {
             //期满时间
             //expiredTimeConfig = _timeLimitDomainService.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToOrg, order.AcceptTypeCode);
-            expiredTimeConfig = await _expireTime.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToOrg, order.AcceptTypeCode);
+            expiredTimeConfig = await _expireTime.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToOrg, order.Adapt<OrderTimeClacInfo>());
         }
 
         _mapper.Map(expiredTimeConfig, order);
@@ -3245,7 +3247,7 @@ public class OrderController : BaseController
         else if (dto.FlowDirection is EFlowDirection.CenterToOrg)
         {
             //expiredTimeConfig = _timeLimitDomainService.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToOrg, order.AcceptTypeCode);
-            expiredTimeConfig = await _expireTime.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToOrg, order.AcceptTypeCode);
+            expiredTimeConfig = await _expireTime.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToOrg, order.Adapt<OrderTimeClacInfo>());
             var canUpdateOrderSender = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.CanUpdateOrderSender).SettingValue[0]);
             order.CenterToOrg(
                 expiredTimeConfig.TimeText, expiredTimeConfig.Count,
@@ -3260,7 +3262,7 @@ public class OrderController : BaseController
         else if (dto.FlowDirection is EFlowDirection.CenterToCenter)
         {
             // expiredTimeConfig = _timeLimitDomainService.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToCenter, order.AcceptTypeCode);
-            expiredTimeConfig = await _expireTime.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToCenter, order.AcceptTypeCode);
+            expiredTimeConfig = await _expireTime.CalcExpiredTime(DateTime.Now, EFlowDirection.CenterToCenter, order.Adapt<OrderTimeClacInfo>());
             order.CenterToCenter(expiredTimeConfig.TimeText, expiredTimeConfig.Count,
                 expiredTimeConfig.TimeType, expiredTimeConfig.ExpiredTime, expiredTimeConfig.NearlyExpiredTime, expiredTimeConfig.NearlyExpiredTimeOne);
             //TODO发送短信即将超期

+ 7 - 0
src/Hotline.Api/StartupExtensions.cs

@@ -27,6 +27,8 @@ using Hotline.CallCenter.Calls;
 using Swashbuckle.AspNetCore.SwaggerUI;
 using Hotline.Configurations;
 using Hotline.DI;
+using Hotline.Share.Tools;
+using Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
 
 namespace Hotline.Api;
 
@@ -180,8 +182,13 @@ internal static class StartupExtensions
 
         services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
         services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
+        services.AddScoped<ExpireTimeFactory>();
+        services.AddScoped<IExpireTimeSupplier, DaySupplier>();
+        services.AddScoped<IExpireTimeSupplier, WorkDaySupplier>();
+        services.AddScoped<IExpireTimeSupplier, HourSupplier>();
 
         //services.AddScoped<LogFilterAttribute>();
+        ServiceLocator.Instance = services.BuildServiceProvider();
         return builder.Build();
     }
 

+ 27 - 0
src/Hotline.Application.Tests/Application/ExpireTimeHandlerTest.cs

@@ -0,0 +1,27 @@
+using Hotline.Settings.TimeLimitDomain;
+using Hotline.Share.Dtos.Settings;
+using Shouldly;
+
+namespace Hotline.Application.Tests.Application;
+public class ExpireTimeHandlerTest
+{
+    private readonly IExpireTimeHandler _expireTimeHandler;
+
+    public ExpireTimeHandlerTest(IExpireTimeHandler expireTimeHandler)
+    {
+        _expireTimeHandler = expireTimeHandler;
+    }
+
+    [Theory]
+    [InlineData("2024/09/04", false)]
+    [InlineData("2024/09/05", false)]
+    [InlineData("2024/09/06", false)]
+    [InlineData("2024/09/07", true)]
+    public async Task Test_GetExpireTime(string time, bool work)
+    {
+        var dateTime = DateTime.Parse(time);
+        var result = await _expireTimeHandler.NotWorkDay(dateTime);
+        result.ShouldBe(work);
+    }
+
+}

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

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

+ 54 - 0
src/Hotline.Application.Tests/DefaultHttpContextAccessor.cs

@@ -0,0 +1,54 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Authentications;
+
+namespace Hotline.Application.Tests;
+public class DefaultHttpContextAccessor : IHttpContextAccessor, ISessionContext
+{
+    private HttpContext _content = new DefaultHttpContext();
+    private HttpContext GetContext()
+    { 
+        var context = new DefaultHttpContext();
+        //var openId = new Claim(AppClaimTypes.OpenId, "测试生成的OpenId");
+        var id = new ClaimsIdentity("身份");
+        //id.AddClaim(openId);
+        context.User = new ClaimsPrincipal(id);
+        //OpenId = context.User.FindFirstValue(AppClaimTypes.OpenId);
+        return context;
+
+    }
+    public HttpContext? HttpContext { get => GetContext(); set => _content = value; }
+
+    public string? UserId => throw new NotImplementedException();
+
+    public string RequiredUserId => throw new NotImplementedException();
+
+    public string? UserName => throw new NotImplementedException();
+
+    public string? Phone => throw new NotImplementedException();
+
+    public string[] Roles => throw new NotImplementedException();
+
+    public string? OrgId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+    public string RequiredOrgId => throw new NotImplementedException();
+
+    public string? OrgName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+    public int OrgLevel { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+    public string? OrgAreaCode { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+    public bool OrgIsCenter { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+    public string? OrgAreaName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+    public string? AreaId => throw new NotImplementedException();
+
+    public string? ClientId => throw new NotImplementedException();
+
+    public string? StaffNo => throw new NotImplementedException();
+
+    public string? OpenId { get; set; }
+}

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

@@ -0,0 +1,79 @@
+using Hotline.Configurations;
+using Hotline.Orders;
+using Hotline.Settings;
+using Hotline.Settings.TimeLimitDomain;
+using Hotline.Share.Dtos.Settings;
+using Hotline.Share.Enums.FlowEngine;
+using Hotline.Share.Enums.Settings;
+using Hotline.Share.Tools;
+using Mapster;
+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.Domain;
+public class YiBinExpireTimeTest
+{
+    private readonly YiBinExpireTimeLimit _calcExpireTime;
+    private readonly IRepository<Order> _orderRepository;
+    private readonly IRepository<TimeLimitSetting> _timeLimitSettingRepository;
+
+    public YiBinExpireTimeTest(YiBinExpireTimeLimit calcExpireTime, IRepository<Order> orderRepository, IRepository<TimeLimitSetting> timeLimitSettingRepository)
+    {
+        _calcExpireTime = calcExpireTime;
+        _orderRepository = orderRepository;
+        _timeLimitSettingRepository = timeLimitSettingRepository;
+    }
+
+    [Theory]
+    [InlineData("2024-09-04 14:00:00", 2, "10", "2024-09-06 14:00:00", "2024/9/6 10:24:00", "2024/9/5 14:00:00", "2个工作日")]
+    [InlineData("2024-09-04 14:01:01", 3, "10", "2024-09-09 14:01:01", "2024/9/6 17:37:01", "2024/9/6 9:31:01", "3个工作日")]
+    public async Task CalcEndTime_Test(string begin, int count, string busCode, string expiredTime, string nearlyExpiredTime, string nearlyExpiredTimeOne, string timeText)
+    {
+        var beginTime = DateTime.Parse(begin);
+        var result = await _calcExpireTime.CalcEndTime(beginTime, new TimeConfig(count, ETimeType.WorkDay), busCode);
+        result.ShouldNotBeNull();
+        result.ExpiredTime.ShouldBe(DateTime.Parse(expiredTime));
+        result.NearlyExpiredTime.ShouldBe(DateTime.Parse(nearlyExpiredTime));
+        result.NearlyExpiredTimeOne.ShouldBe(DateTime.Parse(nearlyExpiredTimeOne));
+        result.Count.ShouldBe(count);
+        result.TimeText.ShouldBe(timeText);
+    }
+
+    [Theory]
+    [InlineData("2024-09-04 14:00:00", "CenterToOrg", "08dccc8f-37b0-40d8-8112-1afb2230c5a3", "2024-09-05 14:00:00")]
+    public async Task CalcExpiredTime_Test(string beginTxt, string flowTxt, string orderId, string expected)
+    {
+        var beginTime = DateTime.Parse(beginTxt);
+        if (orderId.Equals("08dccc8f-37b0-40d8-8112-1afb2230c5a3"))
+            await InitOrderData(orderId);
+        var order = await _orderRepository.Queryable().Where(m => m.Id == orderId).FirstAsync();
+        Enum.TryParse(flowTxt, out EFlowDirection flow);
+        var time = await _calcExpireTime.CalcExpiredTime(beginTime, flow, order.Adapt<OrderTimeClacInfo>());
+        time.ShouldNotBeNull();
+        time.ExpiredTime.ShouldBe(DateTime.Parse(expected));
+    }
+
+    public async Task InitOrderData(string orderId)
+    {
+        var order = await _orderRepository.Queryable().Where(m => m.Id == orderId).FirstAsync();
+        if (order.Is24HoursComplete) return;
+        order.Is24HoursComplete = true;
+        await _orderRepository.UpdateAsync(order);
+    }
+
+    [Fact]
+    public async Task InitExpireTime_Test()
+    {
+        var entity = new TimeLimitSetting() { BusCode = "YQ", BusName = "疫情", TimeType = ETimeType.WorkDay, TimeValue = 2, Percentage = 80, PercentageOne = 50 };
+        if (await _timeLimitSettingRepository.Queryable().Where(m => m.BusCode == entity.BusCode).FirstAsync() == null)
+            await _timeLimitSettingRepository.AddAsync(entity);
+        entity = new TimeLimitSetting() { BusCode = "24", BusName = "24小时", TimeType = ETimeType.Hour, TimeValue = 24, Percentage = 80, PercentageOne = 50 };
+        if (await _timeLimitSettingRepository.Queryable().Where(m => m.BusCode == entity.BusCode).FirstAsync() == null)
+            await _timeLimitSettingRepository.AddAsync(entity);
+    }
+}

+ 121 - 0
src/Hotline.Application.Tests/Domain/ZiGongExpireTimeTest.cs

@@ -0,0 +1,121 @@
+using Hotline.Orders;
+using Hotline.Settings;
+using Hotline.Settings.TimeLimitDomain;
+using Hotline.Settings.TimeLimitDomain.Repository;
+using Hotline.Share.Dtos.Settings;
+using Hotline.Share.Enums.FlowEngine;
+using Hotline.Share.Enums.Settings;
+using Mapster;
+using Shouldly;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.Tests.Domain;
+public class ZiGongExpireTimeTest
+{
+    private readonly ZiGongExpireTimeLimit _ziGongExpireTimeLimit;
+    private readonly IRepository<Order> _orderRepository;
+    private readonly ITimeLimitSettingAttributeRepository _timeLimitSettingAttributeRepository;
+    private readonly ITimeLimitSettingInventoryRepository _timeLimitSettingInventoryRepository;
+
+    public ZiGongExpireTimeTest(ZiGongExpireTimeLimit ziGongExpireTimeLimit, IRepository<Order> orderRepository, ITimeLimitSettingAttributeRepository timeLimitSettingAttributeRepository, ITimeLimitSettingInventoryRepository timeLimitSettingInventoryRepository)
+    {
+        _ziGongExpireTimeLimit = ziGongExpireTimeLimit;
+        _orderRepository = orderRepository;
+        _timeLimitSettingAttributeRepository = timeLimitSettingAttributeRepository;
+        _timeLimitSettingInventoryRepository = timeLimitSettingInventoryRepository;
+    }
+
+    //[Theory]
+    //// 24小时件
+    //[InlineData("24小时件", "2024-09-04 14:00:00", "CenterToOrg", "08dccc8f-37b0-40d8-8112-1afb2230c5a3", "2024-09-05 14:00:00")]
+    //// 疫情件
+    //[InlineData("疫情件", "2024-09-05 14:01:01", "CenterToOrg", "08dccd5c-9bda-4e7d-8d63-82039dcfbde7", "2024-09-09 14:01:01")]
+    //public async Task CalcExpiredTime_Test(string tip, string beginTxt, string flowTxt, string orderId, string expected)
+    //{
+    //    var beginTime = DateTime.Parse(beginTxt);
+    //    if (orderId.Equals("08dccc8f-37b0-40d8-8112-1afb2230c5a3"))
+    //        await InitOrderData(orderId);
+    //    var order = await _orderRepository.Queryable().Where(m => m.Id == orderId).FirstAsync();
+    //    Enum.TryParse(flowTxt, out EFlowDirection flow);
+    //    var time = await _ziGongExpireTimeLimit.CalcExpiredTime(beginTime, flow, order.Adapt<OrderTimeClacInfo>());
+    //    time.ShouldNotBeNull();
+    //    time.ExpiredTime.ShouldBe(DateTime.Parse(expected), $"{tip} 期满时间错误");
+    //}
+
+    [Theory]
+    [InlineData("企业咨询件单元测试", "2024-09-04 14:00:00", "CenterToOrg", "2024-09-05 14:00:00")]
+    [InlineData("企业建议件单元测试", "2024-09-05 14:00:00", "CenterToOrg", "2024-09-09 14:00:00")]
+    [InlineData("企业求助件单元测试", "2024-09-05 14:00:00", "CenterToOrg", "2024-09-09 14:00:00")]
+    [InlineData("企业表扬件单元测试", "2024-09-05 14:00:00", "CenterToOrg", "2024-09-09 14:00:00")]
+    [InlineData("企业举报件单元测试", "2024-09-05 14:00:00", "CenterToOrg", "2024-09-09 14:00:00")]
+    [InlineData("企业投诉件单元测试", "2024-09-05 14:00:00", "CenterToOrg", "2024-09-09 14:00:00")]
+    [InlineData("四川省12345咨询件单元测试", "2024-09-05 14:00:00", "CenterToOrg", "2024-09-06 14:00:00")]
+    [InlineData("四川省12345建议件单元测试", "2024-09-05 14:00:00", "CenterToOrg", "2024-09-10 14:00:00")]
+    public async Task CalcExpiredTime_Test(string title, string beginTxt, string flowTxt, string expected)
+    {
+        var beginTime = DateTime.Parse(beginTxt);
+        var order = await _orderRepository.Queryable().Where(m => m.Title == title).FirstAsync();
+        order.ShouldNotBeNull($"{title} 测试数据不存在");
+        Enum.TryParse(flowTxt, out EFlowDirection flow);
+        var time = await _ziGongExpireTimeLimit.CalcExpiredTime(beginTime, flow, order.Adapt<OrderTimeClacInfo>());
+        time.ShouldNotBeNull();
+        time.ExpiredTime.ShouldBe(DateTime.Parse(expected), $"{title} 期满时间错误 AcceptTypeCode:{order.AcceptTypeCode}");
+        time.TimeText.ShouldBe(order.Content, $"{title} 内容结果比对失败 AcceptTypeCode:{order.AcceptTypeCode}");
+    }
+
+    public async Task InitOrderData(string orderId)
+    {
+        var order = await _orderRepository.Queryable().Where(m => m.Id == orderId).FirstAsync();
+        if (order.Is24HoursComplete) return;
+        order.Is24HoursComplete = true;
+        await _orderRepository.UpdateAsync(order);
+    }
+
+    [Theory]
+    [InlineData("", "Is24HoursComplete", "true", false)]
+    [InlineData("", "HotspotSpliceName", "疫情", false)]
+    [InlineData("10", "IdentityType", "Enterprise", false)]
+    [InlineData("15", "IdentityType", "Enterprise", false)]
+    [InlineData("20", "IdentityType", "Enterprise", false)]
+    public async Task InitTimeLimitData_Test(string busCode, string name, string value, bool isCommon)
+    {
+        var attributeEntity = new TimeLimitSettingAttribute { BusCode = busCode, Name = name, Value = value, IsCommon = isCommon };
+        var dataEntity = await _timeLimitSettingAttributeRepository.GetAsync(attributeEntity.BusCode, attributeEntity.Name, attributeEntity.Value);
+        if (dataEntity is null) await _timeLimitSettingAttributeRepository.AddAsync(attributeEntity);
+    }
+
+    [Theory]
+    [InlineData("", "Is24HoursComplete", "true", "Hour", 24, "24小时件")]
+    [InlineData("", "HotspotSpliceName", "疫情", "WorkDay", 2, "疫情类2个工作日")]
+    [InlineData("10", "IdentityType", "Enterprise", "WorkDay", 1, "企业 '咨询' 件1个工作日")] 
+    [InlineData("",   "IdentityType", "Enterprise", "WorkDay", 2, "企业 '非咨询' 件2个工作日")] 
+
+    [InlineData("10", "SourceChannel", "四川省12345", "WorkDay", 1, "四川省12345 '咨询' 件1个工作日")] 
+    [InlineData("", "SourceChannel", "四川省12345", "WorkDay", 3, "四川省12345 '非咨询' 件3个工作日")]
+
+    [InlineData("10", "SourceChannel", "省政民互动", "WorkDay", 1, "省政民互动 '咨询' 件1个工作日")]
+    [InlineData("", "SourceChannel", "省政民互动", "WorkDay", 3, "省政民互动 '非咨询' 件3个工作日")]
+
+    [InlineData("10", "SourceChannel", "国家政务平台", "WorkDay", 1, "国家政务平台 '咨询' 件1个工作日")]
+    [InlineData("", "SourceChannel", "国家政务平台", "WorkDay", 3, "国家政务平台 '非咨询' 件3个工作日")]
+
+    [InlineData("10", "SourceChannel", "天府通办", "WorkDay", 1, "天府通办 '咨询' 件1个工作日")]
+    [InlineData("", "SourceChannel", "天府通办", "WorkDay", 3, "天府通办 '非咨询' 件3个工作日")]
+
+    [InlineData("10", "SourceChannel", "中国政府网", "WorkDay", 1, "中国政府网 '咨询' 件1个工作日")]
+    [InlineData("", "SourceChannel", "中国政府网", "WorkDay", 3, "中国政府网 '非咨询' 件3个工作日")]
+    public async Task InitTimeLimitInventory_Test(string busCode, string name, string value, string timeType, int timeValue, string remark)
+    {
+        var attributeEntity = new TimeLimitSettingAttribute { BusCode = busCode, Name = name, Value = value, IsCommon = false };
+        var dataEntity = await _timeLimitSettingAttributeRepository.GetAsync(attributeEntity.BusCode, attributeEntity.Name, attributeEntity.Value);
+        if (dataEntity is null) await _timeLimitSettingAttributeRepository.AddAsync(attributeEntity);
+
+        var attribute = await _timeLimitSettingAttributeRepository.GetAsync(busCode, name, value);
+        attribute.ShouldNotBeNull($"{name} 属性未初始化");
+        Enum.TryParse(timeType, out ETimeType eTimeType);
+        var entity = new TimeLimitSettingInventory { Code = attribute.Code.ToString(), TimeType = eTimeType, TimeValue = timeValue, Percentage = 80, PercentageOne = 50 , Remark = remark};
+        var m = await _timeLimitSettingInventoryRepository.GetByCode(entity.Code);
+        if (m is null) await _timeLimitSettingInventoryRepository.AddAsync(entity);
+    }
+
+}

+ 47 - 0
src/Hotline.Application.Tests/Hotline.Application.Tests.csproj

@@ -0,0 +1,47 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net7.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+
+    <IsPackable>false</IsPackable>
+    <IsTestProject>true</IsTestProject>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Remove="appsettings.Development.json" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Content Include="appsettings.Development.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+    </Content>
+  </ItemGroup>
+
+  <ItemGroup>
+    <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="Shouldly" Version="4.2.1" />
+    <PackageReference Include="xunit" Version="2.4.2" />
+    <PackageReference Include="Xunit.DependencyInjection" Version="9.3.0" />
+    <PackageReference Include="Xunit.DependencyInjection.AspNetCoreTesting" Version="9.0.0" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <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="..\XF.Domain.Repository\XF.Domain.Repository.csproj" />
+    <ProjectReference Include="..\XF.Domain\XF.Domain.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Using Include="Xunit" />
+  </ItemGroup>
+
+</Project>

+ 2 - 0
src/Hotline.Application.Tests/ReadME.md

@@ -0,0 +1,2 @@
+## 运行测试
+dotnet test --filter CalcEndTime_Test

+ 201 - 0
src/Hotline.Application.Tests/Startup.cs

@@ -0,0 +1,201 @@
+using Microsoft.AspNetCore.Hosting;
+using Hotline.Repository.SqlSugar.Extensions;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Hotline.Api;
+using Microsoft.AspNetCore.Identity;
+using XF.Domain.Dependency;
+using System.Reflection;
+using Hotline.DI;
+using XF.Domain.Repository;
+using Hotline.Repository.SqlSugar;
+using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Configurations;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder;
+using Xunit.DependencyInjection.AspNetCoreTesting;
+using Polly;
+using Hotline.Share.Tools;
+using Hotline.Users;
+using Hotline.Identity;
+using XF.Domain.Cache;
+using XF.EasyCaching;
+using Mapster;
+using Hotline.EventBus;
+using XF.Utility.MQ;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using DotNetCore.CAP;
+using XF.Domain.Options;
+using Hotline.Settings.TimeLimitDomain;
+using Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+using Microsoft.AspNetCore.WebSockets;
+
+namespace Hotline.Application.Tests;
+public class Startup
+{
+    private const string JsonFile = "appsettings.Development.json";
+    public void ConfigureHost(IHostBuilder hostBuilder)
+    {
+        //hostBuilder
+        //.ConfigureHostConfiguration(builder =>
+        //{
+        //    builder.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true)
+        //    .AddEnvironmentVariables();
+        //});
+        hostBuilder
+            .ConfigureWebHost(webHostBuilder => webHostBuilder
+            .UseTestServerAndAddDefaultHttpClient()
+            .ConfigureAppConfiguration((hostingContext, config) =>
+            {
+                config.AddJsonFile(JsonFile);
+            })
+            .UseStartup<AspNetCoreStartup>()) ;
+    }
+
+    private class AspNetCoreStartup
+    {
+        public AspNetCoreStartup(IConfiguration configuration)
+        {
+            Configuration = configuration;
+        }
+
+        public IConfiguration Configuration { get; }
+
+        public void ConfigureServices(IServiceCollection services) 
+        {
+            AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
+            var configuration = Configuration;
+            var appConfigurationSection = configuration.GetRequiredSection(nameof(AppConfiguration));
+            var appConfiguration = appConfigurationSection.Get<AppConfiguration>();
+            if (appConfiguration is null) throw new ArgumentNullException(nameof(appConfiguration));
+            services.Configure<AppConfiguration>(d => appConfigurationSection.Bind(d));
+            services.Configure<IdentityConfiguration>(d => configuration.GetSection(nameof(IdentityConfiguration)).Bind(d));
+
+            services.RegisterMapper();
+
+            //sqlsugar
+            services.AddSqlSugar(configuration);
+
+            // application services
+            // services.AddScoped<ISnapshotApplication, SnapshotApplication>();
+
+            //mq
+            services.AddTestMq(configuration);
+
+            services.AddScoped<IDataPermissionFilterBuilder, DataPermissionFilterBuilder>();
+            services.AddScoped(typeof(IRepository<>), typeof(BaseRepository<>));
+            services.AddScoped<IHttpContextAccessor, DefaultHttpContextAccessor>();
+            services.AddCache(d =>
+            {
+                var cacheConfig = configuration.GetSection("Cache").Get<CacheOptions>();
+                cacheConfig.Adapt(d);
+                d.Prefix = "Hotline";
+                d.TopicName = "hotline-topic";
+            });
+            services.AddScoped(typeof(IPasswordHasher<>), typeof(PasswordHasher<>));
+            services.AddScoped(typeof(ITypedCache<>), typeof(DefaultTypedCache<>));
+            services.RegisterMediatR(appConfiguration);
+
+            // services.AddSenparcWeixinServices(configuration);
+            AppDomain.CurrentDomain.GetAssemblies().ToList().SelectMany(d => d.GetTypes())
+                .Where(d => d.GetInterfaces().Any(x =>
+                    x == typeof(IScopeDependency)
+                    || x == typeof(ISingletonDependency)
+                    || x == typeof(ITransientDependency))
+                            && d.GetInterfaces().All(x => x != typeof(IIgnoreDependency)))
+                .ToList()
+                .ForEach(d => ServiceRegister.Register(services, d));
+
+            //services.AddScoped<IThirdIdentiyService, ThirdTestService>();
+            // services.AddScoped<IThirdIdentiyService, WeChatService>();
+
+            //services.AddScoped<IThirdAccountRepository, ThirdAccountRepository>();
+            services.AddApplication();
+            services.AddScoped<IExpireTimeSupplier, DaySupplier>();
+            services.AddScoped<IExpireTimeSupplier, WorkDaySupplier>();
+            services.AddScoped<IExpireTimeSupplier, HourSupplier>();
+            services.AddScoped<ExpireTimeFactory>();
+            services.AddScoped<YiBinExpireTimeLimit>();
+            services.AddScoped<ZiGongExpireTimeLimit>();
+
+            ServiceLocator.Instance = services.BuildServiceProvider();
+        }
+
+        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+        {
+            // app.UseSenparcWeixin(env, null, null, register => { }, (register, weixinSetting) => { });
+        }
+    }
+}
+
+public static class StartupHelper
+{
+    public static IServiceCollection RegisterMediatR(this IServiceCollection services, AppConfiguration appConfiguration)
+    {
+        services.AddScoped<Publisher>();
+        services.AddMediatR(cfg =>
+        {
+            cfg.TypeEvaluator = d =>
+            {
+                var attr = d.GetCustomAttribute(typeof(InjectionAttribute)) as InjectionAttribute;
+                if (attr is null) return true;
+                return attr.IsEnable(appConfiguration.AppScope,
+                    appConfiguration.GetDefaultAppScopeConfiguration().CallCenterType);
+            };
+            cfg.RegisterServicesFromAssembly(typeof(ApplicationStartupExtensions).Assembly);
+        });
+
+        return services;
+    }
+
+    public static IServiceCollection AddTestMq(this IServiceCollection services, IConfiguration configuration)
+    {
+        MqConfiguration mqConfiguration = configuration.GetSection("MqConfiguration").Get<MqConfiguration>();
+        services.AddCap(delegate (CapOptions x)
+        {
+            if (!(mqConfiguration.DbType == "PostgreSql"))
+            {
+                throw new ArgumentOutOfRangeException();
+            }
+
+            x.UsePostgreSql(mqConfiguration.DbConnectionString);
+            if (!(mqConfiguration.MqType == "RabbitMq"))
+            {
+                throw new ArgumentOutOfRangeException();
+            }
+
+            RabbitMq rabbitmq = mqConfiguration.RabbitMq;
+            if (rabbitmq == null)
+            {
+                throw new ArgumentNullException("未配置RabbitMq参数");
+            }
+
+            x.UseRabbitMQ(delegate (RabbitMQOptions d)
+            {
+                d.UserName = rabbitmq.UserName;
+                d.Password = rabbitmq.Password;
+                d.HostName = rabbitmq.HostName;
+                d.Port = rabbitmq.Port;
+                d.VirtualHost = rabbitmq.VirtualHost;
+            });
+
+            if (!string.IsNullOrEmpty(mqConfiguration.DefaultGroup))
+            {
+                x.DefaultGroupName = mqConfiguration.DefaultGroup;
+            }
+
+            if (!string.IsNullOrEmpty(mqConfiguration.TopicNamePrefix))
+            {
+                x.TopicNamePrefix = mqConfiguration.TopicNamePrefix;
+            }
+        });
+        return services;
+    }
+}

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

@@ -0,0 +1,174 @@
+{
+    "SenparcWeixinSetting": {
+        "IsDebug": true,
+
+        //小程序
+        "WxOpenAppId": "#{WxOpenAppId}#",
+        "WxOpenAppSecret": "#{WxOpenAppSecret}#",
+        "WxOpenToken": "#{WxOpenToken}#",
+        "WxOpenEncodingAESKey": "#{WxOpenEncodingAESKey}#"
+    },
+    "AllowedHosts": "*",
+    "AppConfiguration": {
+        "AppScope": "ZiGong",
+        "YiBin": {
+            "CallCenterType": "TianRun", //XunShi、WeiErXin、TianRun、XingTang
+            //智能回访
+            "AiVisit": {
+                "Url": "http://118.122.73.80:19061",
+                "Appkey": "MTAwMDAx",
+                "ServiceVersion": "V1.0.0" //接口版本号
+            },
+            //智能质检
+            "AiQuality": {
+                "Url": "http://118.122.73.80:19072/" // 正式
+                //"Url": "http://118.122.73.80:19072/", // 测试
+            },
+            //企业服务
+            "Enterprise": {
+                "AddressUrl": "http://10.12.185.227:8834/",
+                "ClientId": "1462598736",
+                "ClientSecret": "6nZtVK4rKfnsncGymUHB",
+                "TenantId": "000000"
+            }
+        },
+        "ZiGong": {
+            "CallCenterType": "XingTang"
+        },
+        "LuZhou": {
+            "CallCenterType": "XingTang"
+        }
+    },
+    "CallCenterConfiguration": {
+        //"CallCenterType": "TianRun", //XunShi、WeiErXin、TianRun、XingTang
+        "NewRock": {
+            "Address": "http://192.168.100.100/xml",
+            "Authorize": true,
+            "ReceiveKey": "E1BBD1BB-A269-44",
+            "SendKey": "2A-BA92-160A3B1D",
+            "Expired": 86300 //认证过期时间(秒)
+        },
+        "Wex": {
+            "Address": "http://222.212.82.225:8083",
+            "Username": "admin",
+            "Password": "Wex@12345"
+        },
+        "TianRun": {
+            //"Address": "http://internal.ttf-cti.com:8080",
+            //"Username": "yscs",
+            //"Password": "123456",
+            "Address": "http://222.213.23.229:29003/",
+            "Username": "root",
+            "Password": "12345678aa",
+            "Ip": "222.213.23.229"
+        },
+        "XingTang": {
+            //"DbConnectionString": "server=123.56.10.71;Database=callcenter_db;Uid=root;Pwd=Lhw1981!(*!"
+            "DbConnectionString": "server=110.188.24.182;Database=callcenter_xingtang;Uid=dev;Pwd=fengwo11!!"
+        }
+    },
+    "ConnectionStrings": {
+        "Hotline": "PORT=5432;DATABASE=hotline_dev;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;"
+    },
+    "Cache": {
+        "Host": "110.188.24.182",
+        "Port": 50179,
+        "Password": "fengwo123!$!$",
+        "Database": 5 //release:3, dev:5
+    },
+    "Swagger": true,
+    "Cors": {
+        "Origins": [ "http://localhost:8888", "http://admin.hotline.fw.com", "http://localhost:80", "http://localhost:8113" ]
+    },
+    "IdentityConfiguration": {
+        "Password": {
+            "RequiredLength": 8,
+            "RequireNonAlphanumeric": true,
+            "RequireLowercase": true,
+            "RequireUppercase": true
+        },
+        "User": {
+            "RequireUniqueEmail": false
+        },
+        "SignIn": {
+            "RequireConfirmedAccount": false
+        },
+        "Lockout": {
+            "MaxFailedAccessAttempts": 5,
+            "DefaultLockoutTimeSpan": "00:10:00"
+        },
+        "Account": {
+            "DefaultPassword": "Fwkj@789"
+        },
+        "Jwt": {
+            "SecretKey": "e660d04ef1d3410798c953f5d7b8a4e1",
+            "Issuer": "hotline_server",
+            "Audience": "hotline",
+            "Scope": "hotline_api",
+            "Expired": 86400 //seceonds
+        }
+    },
+    "DatabaseConfiguration": {
+        "ApplyDbMigrations": false,
+        "ApplySeed": false
+    },
+    "MqConfiguration": {
+        "DbConnectionString": "PORT=5432;DATABASE=fwmq;HOST=110.188.24.182;PASSWORD=fengwo11!!;USER ID=dev;",
+        "UseDashBoard": true,
+        "RabbitMq": {
+            "UserName": "dev",
+            "Password": "123456",
+            "HostName": "110.188.24.182",
+            "VirtualHost": "fwt-master"
+        }
+    },
+    //"SmsAccountInfo": {
+    //  "MessageServerUrl": "http://webservice.fway.com.cn:1432/FWebService.asmx/FWay_Service", //短信发送地址
+    //  "AccountUser": "CS12345", //短信系统账号
+    //  "AccountPwd": "9EE3899305A8FC97D6146CAC6B802E6F", //短信系统密码
+    //  "ReturnAccountUser": "fwkj", //短信回传账号
+    //  "ReturnAccountPwd": "fwkj12" //短信回传密码
+    //},
+    "FwClient": {
+        "ClientId": "hotline",
+        "ClientSecret": "08db29cc-0da0-4adf-850c-1b2689bd535d"
+    }
+    //"ConfigCenter": {
+    //  "ServerAddresses": [ "http://110.188.24.28:8848" ],
+    //  "Namespace": "17503980-9b0d-4d3e-8e35-c842c41fb888", //debug
+    //  "ServiceName": "hotline"
+    //},
+    //"Tr": {
+    //  //"Address": "http://internal.ttf-cti.com:8080",
+    //  //"Username": "yscs",
+    //  //"Password": "123456",
+    //  "Address": "http://222.213.23.229:29003/",
+    //  "Username": "root",
+    //  "Password": "12345678aa",
+    //  "Ip": "222.213.23.229"
+    //},
+    ////智能回访
+    //"AiVisit": {
+    //  "Url": "http://118.122.73.80:19061",
+    //  "Appkey": "MTAwMDAx",
+    //  "ServiceVersion": "V1.0.0" //接口版本号
+    //},
+    ////智能质检
+    //"AiQuality": {
+    //  "Url": "http://118.122.73.80:19072/" // 正式
+    //  //"Url": "http://118.122.73.80:19072/", // 测试
+    //},
+    ////企业服务
+    //"Enterprise": {
+    //  "AddressUrl": "http://10.12.185.227:8834/",
+    //  "ClientId": "1462598736",
+    //  "ClientSecret": "6nZtVK4rKfnsncGymUHB",
+    //  "TenantId": "000000"
+    //},
+
+    //"SendSms": {
+    //  "Url": "http://localhost:50108/api/v1/PushMessage/addwaitmsg"
+    //}
+
+
+}

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

@@ -9,6 +9,7 @@ using Hotline.Share.Dtos.JudicialManagement;
 using Hotline.Share.Dtos.OrderExportWord;
 using Hotline.Share.Dtos.Org;
 using Hotline.Share.Dtos.Push.FWMessage;
+using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Enums.Order;
 using Mapster;
 using XF.Domain.Entities;
@@ -19,6 +20,16 @@ namespace Hotline.Application.Mappers
     {
         public void Register(TypeAdapterConfig config)
         {
+            config.ForType<TimeLimitSettingInventory, TimeConfig>()
+                .Map(d => d.Count, m => m.TimeValue);
+            config.ForType<TimeResult, ExpiredTimeWithConfig>()
+                .Map(d => d.ExpiredTime, x => x.EndTime)
+                .Map(d => d.NearlyExpiredTime, x => x.NearlyExpiredTime)
+                .Map(d => d.NearlyExpiredTimeOne, x => x.NearlyExpiredTimeOne);
+                
+            config.ForType<TimeLimitSetting, TimeConfig>()
+                .Map(d => d.Count, x => x.TimeValue);
+
             config.ForType<AddBlacklistDto, Blacklist>()
                 .Ignore(d => d.Expired)
                 .AfterMapping((s, t) => t.InitExpired());

+ 28 - 0
src/Hotline.Repository.SqlSugar/ExpireTime/TimeLimitSettingAttributeRepository.cs

@@ -0,0 +1,28 @@
+using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Settings;
+using Hotline.Settings.TimeLimitDomain.Repository;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Dependency;
+using XF.Domain.Repository;
+
+namespace Hotline.Repository.SqlSugar.ExpireTime;
+public class TimeLimitSettingAttributeRepository : BaseRepository<TimeLimitSettingAttribute>, ITimeLimitSettingAttributeRepository, IScopeDependency
+{
+    public TimeLimitSettingAttributeRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
+    {
+    }
+
+    public async Task<TimeLimitSettingAttribute?> GetAsync(string busCode, string name, string value)
+        => await base.GetAsync(x => x.Name == name && x.Value == value && x.BusCode == busCode);
+
+    public async Task<TimeLimitSettingAttribute?> GetAsync(string name, string value)
+        => await GetAsync(m => m.Name == name && m.Value == value);
+
+    public async Task<TimeLimitSettingAttribute?> GetFuzzyAsync(string busCode, string name, string value)
+        => await base.GetAsync(m => m.Name == name && value.Contains(m.Value) && m.BusCode == busCode);
+}

+ 24 - 0
src/Hotline.Repository.SqlSugar/ExpireTime/TimeLimitSettingInventoryRepository.cs

@@ -0,0 +1,24 @@
+using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Settings;
+using Hotline.Settings.TimeLimitDomain.Repository;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Dependency;
+
+namespace Hotline.Repository.SqlSugar.ExpireTime;
+public class TimeLimitSettingInventoryRepository : BaseRepository<TimeLimitSettingInventory>, ITimeLimitSettingInventoryRepository, IScopeDependency
+{
+    public TimeLimitSettingInventoryRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
+    {
+    }
+
+    public async Task<TimeLimitSettingInventory?> GetByCode(string code)
+    {
+        return await base.Queryable().Where(m => m.Code == code).FirstAsync();
+    }
+}
+

+ 21 - 0
src/Hotline.Repository.SqlSugar/ExpireTime/TimeLimitSettingRepository.cs

@@ -0,0 +1,21 @@
+using Hotline.Repository.SqlSugar.DataPermissions;
+using Hotline.Settings;
+using Hotline.Settings.TimeLimitDomain.Repository;
+using SqlSugar;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Dependency;
+
+namespace Hotline.Repository.SqlSugar.ExpireTime;
+public class TimeLimitSettingRepository : BaseRepository<TimeLimitSetting>, ITimeLimitSettingRepository, IScopeDependency
+{
+    public TimeLimitSettingRepository(ISugarUnitOfWork<HotlineDbContext> uow, IDataPermissionFilterBuilder dataPermissionFilterBuilder) : base(uow, dataPermissionFilterBuilder)
+    {
+    }
+
+    public async Task<TimeLimitSetting?> GetByBusCode(string busCode)
+        => await base.GetAsync(x => x.BusCode == busCode);
+}

+ 6 - 0
src/Hotline.Share/Dtos/Settings/IsFuzzyQueryAttribute.cs

@@ -0,0 +1,6 @@
+
+namespace Hotline.Share.Dtos.Settings;
+
+public class IsFuzzyQueryAttribute : Attribute
+{
+}

+ 6 - 0
src/Hotline.Share/Dtos/Settings/NoCodeAttribute.cs

@@ -0,0 +1,6 @@
+
+namespace Hotline.Share.Dtos.Settings;
+
+public class NoCodeAttribute : Attribute
+{
+}

+ 73 - 2
src/Hotline.Share/Dtos/Settings/TimeConfig.cs

@@ -3,16 +3,58 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Settings;
+using Hotline.Share.Tools;
 using XF.Utility.EnumExtensions;
 
 namespace Hotline.Share.Dtos.Settings
 {
+    /// <summary>
+    /// 计算期满时间所需要的 工单信息
+    /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+    /// !!!!!该类中的字段顺序很重要!!!!!!
+    /// 字段的顺序等于期满条件的优先级
+    /// 计算期满时间时, 会根据该类中字段顺序计算期满
+    /// </summary>
+    public class OrderTimeClacInfo
+    {
+        public const string BusCodeName = "AcceptTypeCode";
+
+        /// <summary>
+        /// 受理类型代码
+        /// </summary>
+        public string? AcceptTypeCode { get; set; }
+
+        /// <summary>
+        /// 24小时办结
+        /// </summary>
+        [NoCode] // 不需要查询 BusCode
+        public bool Is24HoursComplete { get; set; }
+
+        /// <summary>
+        /// 热点分类类目名称
+        /// </summary>
+        [IsFuzzyQuery] // 模糊查询
+        [NoCode] // 不需要查询 BusCode
+        public string? HotspotSpliceName { get; set; }
+
+        /// <summary>
+        /// 来电/信人身份
+        /// </summary>
+        public EIdentityType? IdentityType { get; set; }
+
+        /// <summary>
+        /// 来源渠道
+        /// </summary>
+        public string? SourceChannel { get; set; }
+    }
+
     public class TimeConfig
     {
         public TimeConfig()
         {
-            
+
         }
 
         public TimeConfig(int count, ETimeType timeType)
@@ -24,12 +66,41 @@ namespace Hotline.Share.Dtos.Settings
 
         public int Count { get; set; }
         public ETimeType TimeType { get; set; }
-        public string TimeText { get; set; }
+
+        private string timeText;
+        public string TimeText
+        {
+            get
+            {
+                if (timeText.IsNullOrEmpty()) return $"{Count}个{TimeType.GetDescription()}";
+                return timeText;
+            }
+            set 
+            {
+                timeText = value;
+            }
+        }
+
+        /// <summary>
+        /// 超期时限百分比
+        /// </summary>
+        public int Percentage { get; set; }
+
+        /// <summary>
+        /// 超期时间百分比(第一级)
+        /// </summary>
+        public int PercentageOne { get; set; }
+
+        /// <summary>
+        /// 工作时间
+        /// </summary>
+        public IList<string>? WorkTime { get; set; }
     }
 
     public class ExpiredTimeWithConfig : TimeConfig
     {
         public DateTime ExpiredTime { get; set; }
+
         /// <summary>
         /// 即将超期时间
         /// </summary>

+ 2 - 2
src/Hotline.Share/Enums/Settings/ETimeType.cs

@@ -9,8 +9,8 @@ namespace Hotline.Share.Enums.Settings
 {
     public enum ETimeType
     {
-        //[Description("小时")]
-        //Hour = 1,
+        [Description("小时")]
+        Hour = 1,
         [Description("工作日")]
         WorkDay = 2,
         [Description("自然日")]

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

@@ -12,7 +12,9 @@
 
   <ItemGroup>
     <PackageReference Include="DocXCore" Version="1.0.10" />
+    <PackageReference Include="Mapster" Version="7.3.0" />
     <PackageReference Include="MediatR.Contracts" Version="1.0.1" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
     <PackageReference Include="XF.Utility.EnumExtensions" Version="1.0.4" />
   </ItemGroup>
 

+ 29 - 0
src/Hotline.Share/Tools/ObjectExtensions.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Hotline.Share.Tools;
+public static class ObjectExtensions
+{
+    /// <summary>
+    /// 遍历类属性
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="model"></param>
+    /// <param name="func">返回 true: contions; false: break</param>
+    public static void ForeachClassProperties<T>(this T model, Func<T, PropertyInfo , string, object, Task<bool>> func)
+    {
+        var t = model.GetType();
+        var propertyList = t.GetProperties();
+        foreach (var item in propertyList)
+        {
+            var name = item.Name;
+            var value = item.GetValue(model, null);
+            var result = func(model, item, name, value).GetAwaiter().GetResult();
+            if (result == false) break;
+        }
+    }
+}

+ 8 - 0
src/Hotline.Share/Tools/ServiceLocator.cs

@@ -0,0 +1,8 @@
+namespace Hotline.Share.Tools;
+public static class ServiceLocator
+{
+    /// <summary>
+    /// 服务集
+    /// </summary>
+    public static IServiceProvider Instance { get; set; }
+}

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

@@ -0,0 +1,17 @@
+using System.Reflection;
+
+namespace Hotline.Share.Tools;
+public static class TypeExtensions
+{
+    /// <summary>
+    /// 检查指定指定类型成员中是否存在指定的Attribute特性
+    /// </summary>
+    /// <typeparam name="T">要检查的Attribute特性类型</typeparam>
+    /// <param name="memberInfo">要检查的类型成员</param>
+    /// <param name="inherit">是否从继承中查找</param>
+    /// <returns>是否存在</returns>
+    public static bool HasAttribute<T>(this MemberInfo memberInfo, bool inherit = false) where T : Attribute
+    {
+        return memberInfo.IsDefined(typeof(T), inherit);
+    }
+}

+ 33 - 26
src/Hotline/Orders/Order.cs

@@ -138,6 +138,9 @@ namespace Hotline.Orders
         /// </summary>
         public string? AcceptType { get; set; }
 
+        /// <summary>
+        /// 受理类型代码
+        /// </summary>
         public string? AcceptTypeCode { get; set; }
 
         public string Title { get; set; }
@@ -160,6 +163,9 @@ namespace Hotline.Orders
 
         public string? HotspotName { get; set; }
 
+        /// <summary>
+        /// 热点分类类目名称
+        /// </summary>
         public string? HotspotSpliceName { get; set; }
 
         /// <summary>
@@ -775,12 +781,12 @@ namespace Hotline.Orders
         /// 派单退回次数
         /// </summary>
         public int? SendBackNum { get; set; }
-		#endregion
+        #endregion
 
-		/// <summary>
-		/// 敏感标签
-		/// </summary>
-		[SugarColumn(ColumnDataType = "json", IsJson = true, IsNullable = true)]
+        /// <summary>
+        /// 敏感标签
+        /// </summary>
+        [SugarColumn(ColumnDataType = "json", IsJson = true, IsNullable = true)]
         public List<string>? Sensitive { get; set; }
 
         /// <summary>
@@ -852,14 +858,14 @@ namespace Hotline.Orders
         [SugarColumn(ColumnDescription = "政民互动公开ID")]
         public string? OrderProvinceZmhdId { get; set; }
 
-		/// <summary>
-		/// 超期部门
-		/// </summary>
-		[SugarColumn(IsIgnore = true)]
-		public string? DaysOverdueOrgName { get; set; }
-	}
+        /// <summary>
+        /// 超期部门
+        /// </summary>
+        [SugarColumn(IsIgnore = true)]
+        public string? DaysOverdueOrgName { get; set; }
+    }
 
-	public partial class Order
+    public partial class Order
     {
         /// <summary>
         /// 受理人
@@ -1000,25 +1006,26 @@ namespace Hotline.Orders
         /// <summary>
         /// 特提之后 归档信息清空
         /// </summary>
-        public void FileEmpty() {
+        public void FileEmpty()
+        {
             FiledTime = null;
-			HandleDurationWorkday = 0;
+            HandleDurationWorkday = 0;
             HandleDuration = 0;
-			FileDurationWorkday = 0;
+            FileDurationWorkday = 0;
             FileDuration = 0;
-			AllDurationWorkday = 0;
+            AllDurationWorkday = 0;
             AllDuration = 0;
-			CreationTimeHandleDurationWorkday = 0;
+            CreationTimeHandleDurationWorkday = 0;
             CreationTimeHandleDuration = 0;
-			CenterToOrgHandleDurationWorkday = 0;
+            CenterToOrgHandleDurationWorkday = 0;
             CenterToOrgHandleDuration = 0;
-		}
+        }
 
-		/// <summary>
-		/// 发布
-		/// </summary>
-		/// <param name="isPublicity"></param>
-		public void Publish(bool isPublicity)
+        /// <summary>
+        /// 发布
+        /// </summary>
+        /// <param name="isPublicity"></param>
+        public void Publish(bool isPublicity)
         {
             //Progress = EProgress.Published;
             Status = EOrderStatus.Published;
@@ -1041,7 +1048,7 @@ namespace Hotline.Orders
         }
 
         public void CenterToOrg(string timelimit, int timelimitCount, ETimeType timilimitUnit,
-            DateTime expiredTime, DateTime nearlyExpiredTime,DateTime nearlyExpiredTimeOne,
+            DateTime expiredTime, DateTime nearlyExpiredTime, DateTime nearlyExpiredTimeOne,
             string opinion, string handlerId, string handlerName,
             bool canUpdateOrderSender)
         {
@@ -1065,7 +1072,7 @@ namespace Hotline.Orders
                 WaitForPublisherId = handlerId;
         }
 
-        public void OrgToCenter(string timelimit, int timelimitCount, ETimeType timilimitUnit, DateTime expiredTime, DateTime nearlyExpiredTime,DateTime nearlyExpiredTimeOne)
+        public void OrgToCenter(string timelimit, int timelimitCount, ETimeType timilimitUnit, DateTime expiredTime, DateTime nearlyExpiredTime, DateTime nearlyExpiredTimeOne)
         {
             ProcessType = EProcessType.Zhiban;
             TimeLimit = timelimit;

+ 1 - 0
src/Hotline/SeedData/TimeLimitSettingSeedData.cs

@@ -18,6 +18,7 @@ public class TimeLimitSettingSeedData : ISeedData<TimeLimitSetting>
             new() { BusCode = "15", BusName = "建议", TimeType = ETimeType.WorkDay, TimeValue = 3, Percentage = 80 , PercentageOne = 50 },
             new() { BusCode = "25", BusName = "表扬", TimeType = ETimeType.WorkDay, TimeValue = 3, Percentage = 80 ,PercentageOne = 50 },
             new() { BusCode = "40", BusName = "其他", TimeType = ETimeType.WorkDay, TimeValue = 3, Percentage = 80 , PercentageOne = 50 },
+            new() { BusCode = "YQ", BusName = "疫情", TimeType = ETimeType.Hour, TimeValue = 24, Percentage = 80 , PercentageOne = 50 },
         };
     }
 }

+ 6 - 0
src/Hotline/Settings/SystemDicData.cs

@@ -47,5 +47,11 @@ namespace Hotline.Settings
 
         [SugarColumn(IsIgnore = true)]
         public List<SystemDicData> Children { get; set; }
+
+		/// <summary>
+		/// 附加数据
+		///</summary>
+		[SugarColumn(ColumnDataType = "json", IsJson = true)]
+		public string? Attach { get; set; }        
     }
 }

+ 22 - 6
src/Hotline/Settings/TimeLimitDomain/ExpireTimeHandler.cs

@@ -1,4 +1,5 @@
-using Hotline.Settings.TimeLimits;
+using Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+using Hotline.Settings.TimeLimits;
 using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Enums.Settings;
 using XF.Domain.Dependency;
@@ -7,10 +8,12 @@ namespace Hotline.Settings.TimeLimitDomain;
 public class ExpireTimeHandler : IExpireTimeHandler, IScopeDependency
 {
     private readonly IDaySettingRepository _daySettingRepository;
+    private readonly ExpireTimeFactory _expireTimeFactory;
 
-    public ExpireTimeHandler(IDaySettingRepository daySettingRepository)
+    public ExpireTimeHandler(IDaySettingRepository daySettingRepository, ExpireTimeFactory expireTimeFactory)
     {
         _daySettingRepository = daySettingRepository;
+        _expireTimeFactory = expireTimeFactory;
     }
 
     /// <summary>
@@ -21,7 +24,7 @@ public class ExpireTimeHandler : IExpireTimeHandler, IScopeDependency
     /// <param name="workTime">工作日时间段</param>
     /// <param name="isCenter"></param>
     /// <returns></returns>
-    public async Task<int> CalcWorkTimeAsync(DateTime beginTime, DateTime endTime, List<string>? workTime, bool isCenter)
+    public async Task<int> CalcWorkTimeAsync(DateTime beginTime, DateTime endTime, IList<string>? workTime, bool isCenter)
     {
         if (isCenter)
         {
@@ -209,7 +212,7 @@ public class ExpireTimeHandler : IExpireTimeHandler, IScopeDependency
     /// <param name="workTime">工作日时间段</param>
     /// <param name="isCenter"></param>
     /// <returns></returns>
-    public async Task<int> CalcWorkTimeExAsync(DateTime beginTime, DateTime endTime, List<string>? workTime, bool isCenter)
+    public async Task<int> CalcWorkTimeExAsync(DateTime beginTime, DateTime endTime, IList<string>? workTime, bool isCenter)
     {
         if (isCenter)
         {
@@ -494,6 +497,11 @@ public class ExpireTimeHandler : IExpireTimeHandler, IScopeDependency
         return date;
     }
 
+    public async Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, TimeConfig timeConfig)
+    { 
+        return await _expireTimeFactory.GetSupplier(timeConfig.TimeType).CalcEndTimeAsync(beginTime, timeConfig);
+    }
+
     /// <summary>
     /// 计算工作日
     /// </summary>
@@ -504,8 +512,16 @@ public class ExpireTimeHandler : IExpireTimeHandler, IScopeDependency
     /// <param name="Percentage">即将超期百分比</param>
     /// <param name="PercentageOne">超期百分比第一级</param>
     /// <returns></returns>
-    public async Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, ETimeType timeType, List<string>? workTime, int timeValue, int Percentage, int PercentageOne)
+    public async Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, ETimeType timeType, IList<string>? workTime, int timeValue, int Percentage, int PercentageOne)
     {
+        return await _expireTimeFactory.GetSupplier(timeType).CalcEndTimeAsync(beginTime, new TimeConfig
+        {
+            WorkTime = workTime,
+            Count = timeValue,
+            Percentage = Percentage,
+            PercentageOne = PercentageOne
+        });
+
         var startTime = beginTime;
         var startTimeOne = beginTime;
         //如果是部门,采用部门计算方式
@@ -728,7 +744,7 @@ public class ExpireTimeHandler : IExpireTimeHandler, IScopeDependency
         return await _daySettingRepository.IsWorkDay(date);
     }
 
-    private async Task<bool> NotWorkDay(DateTime date)
+    public async Task<bool> NotWorkDay(DateTime date)
     {
         return !await IsWorkDay(date);
     }

+ 61 - 46
src/Hotline/Settings/TimeLimitDomain/ExpireTimeLimitBase.cs

@@ -3,6 +3,8 @@ using Hotline.Settings.TimeLimits;
 using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Enums.FlowEngine;
 using Hotline.Share.Enums.Settings;
+using Hotline.Share.Tools;
+using Mapster;
 using MapsterMapper;
 using XF.Domain.Exceptions;
 using XF.Domain.Repository;
@@ -13,35 +15,35 @@ namespace Hotline.Settings.TimeLimitDomain;
 /// <summary>
 /// 过期时间处理基类
 /// </summary>
-public abstract class ExpireTimeLimitBase 
+public abstract class ExpireTimeLimitBase
 {
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
     private readonly IRepository<TimeLimitSetting> _timeLimitSettingRepository;
     private readonly IExpireTimeHandler _expireTimeHandler;
     private readonly IMapper _mapper;
-    private readonly IRepository<SystemSetting> _systemSettingRepository;
     private readonly IDaySettingRepository _daySettingRepository;
 
-    public ExpireTimeLimitBase(ISystemSettingCacheManager systemSettingCacheManager, IRepository<TimeLimitSetting> timeLimitSettingRepository, IExpireTimeHandler expireTimeHandler, IMapper mapper, IRepository<SystemSetting> systemSettingRepository, IDaySettingRepository daySettingRepository)
+    public ExpireTimeLimitBase(ISystemSettingCacheManager systemSettingCacheManager, IRepository<TimeLimitSetting> timeLimitSettingRepository, IExpireTimeHandler expireTimeHandler, IMapper mapper, IDaySettingRepository daySettingRepository)
     {
         _systemSettingCacheManager = systemSettingCacheManager;
         _timeLimitSettingRepository = timeLimitSettingRepository;
         _expireTimeHandler = expireTimeHandler;
         _mapper = mapper;
-        _systemSettingRepository = systemSettingRepository;
         _daySettingRepository = daySettingRepository;
     }
 
     public virtual async Task<int> CalcWorkTime(DateTime beginTime, DateTime endTime, bool isCenter)
     {
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
-        return await _expireTimeHandler.CalcWorkTimeAsync(beginTime, endTime, workTime.SettingValue, isCenter);
+        // var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
+        return await _expireTimeHandler.CalcWorkTimeAsync(beginTime, endTime, workTime, isCenter);
     }
 
     public virtual async Task<int> CalcWorkTimeEx(DateTime beginTime, DateTime endTime, bool isCenter)
     {
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTimeOrg);
-        return await _expireTimeHandler.CalcWorkTimeExAsync(beginTime, endTime, workTime.SettingValue, isCenter);
+        // var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTimeOrg);
+        var workTime = GetWorkTimes(SettingConstants.WorkTimeOrg);
+        return await _expireTimeHandler.CalcWorkTimeExAsync(beginTime, endTime, workTime, isCenter);
     }
 
     /// <summary>
@@ -51,13 +53,13 @@ public abstract class ExpireTimeLimitBase
     /// <returns></returns>
     public virtual async Task<DateTime> WorkDay(DateTime date)
     {
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
-        var (WorkBeginTime, WorkEndTime) = GetWorkTime(DateTime.Now, workTime.SettingValue);
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
+        var (WorkBeginTime, WorkEndTime) = GetWorkTime(DateTime.Now, workTime);
         if (await IsWorkDay(date))
         {
             if (date < WorkBeginTime || date > WorkEndTime)
             {
-                date = DateTime.Parse(date.ToShortDateString() + " " + workTime.SettingValue[0] + ":00");
+                date = DateTime.Parse(date.ToShortDateString() + " " + workTime[0] + ":00");
             }
         }
         else
@@ -68,7 +70,7 @@ public abstract class ExpireTimeLimitBase
             }
             if (date < WorkBeginTime || date > WorkEndTime)
             {
-                date = DateTime.Parse(date.ToShortDateString() + " " + workTime.SettingValue[0] + ":00");
+                date = DateTime.Parse(date.ToShortDateString() + " " + workTime[0] + ":00");
             }
         }
         return date;
@@ -89,46 +91,47 @@ public abstract class ExpireTimeLimitBase
         if (string.IsNullOrEmpty(code))
         {
             return new TimeConfig(
-                int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.BackCenterTimeSetting).SettingValue[0]),
+                int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.BackCenterTimeSetting)!.SettingValue[0]),
                 ETimeType.WorkDay);
         }
         else
         {
-            var timeSetting = _timeLimitSettingRepository.Queryable().First(x => x.BusCode == code);
-            return new TimeConfig(timeSetting.TimeValue, timeSetting.TimeType);
+            return _timeLimitSettingRepository.Queryable().First(x => x.BusCode == code).Adapt<TimeConfig>();
         }
     }
 
     /// <summary>
     /// 计算期满时间
     /// </summary>
-    public virtual async Task<ExpiredTimeWithConfig?> CalcExpiredTime(DateTime beginTime, EFlowDirection flowDirection, string? acceptTypeCode)
+    public virtual async Task<ExpiredTimeWithConfig> CalcExpiredTime(DateTime beginTime, EFlowDirection flowDirection, OrderTimeClacInfo order)
     {
-        if (flowDirection is EFlowDirection.CenterToOrg && string.IsNullOrEmpty(acceptTypeCode))
+        if (EFlowDirection.CenterToOrg == flowDirection && order.Is24HoursComplete)
+        {
+            order.AcceptTypeCode = "24";
+        }
+
+        if (flowDirection is EFlowDirection.CenterToOrg && string.IsNullOrEmpty(order.AcceptTypeCode))
             throw new UserFriendlyException("中心派至部门的工单期满时间需受理类型参数");
         var timeConfig = flowDirection switch
         {
-            EFlowDirection.CenterToOrg => GetOrderTimeLimitConfig(acceptTypeCode),
+            EFlowDirection.CenterToOrg => GetOrderTimeLimitConfig(order.AcceptTypeCode),
             EFlowDirection.OrgToCenter => GetOrderTimeLimitConfig(),
             EFlowDirection.CenterToCenter => GetOrderTimeLimitConfig(acceptTypeCode),
             _ => throw new ArgumentOutOfRangeException(nameof(flowDirection), flowDirection, null)
         };
 
-        return await CalcEndTime(beginTime, timeConfig, acceptTypeCode);
+        return await CalcEndTime(beginTime, timeConfig, order.AcceptTypeCode);
     }
 
-    public virtual async Task<ExpiredTimeWithConfig?> CalcEndTime(DateTime beginTime, TimeConfig timeConfig, string? busCode)
+    public virtual async Task<ExpiredTimeWithConfig> CalcEndTime(DateTime beginTime, TimeConfig timeConfig, string busCode)
     {
-        var setting = _timeLimitSettingRepository.Queryable().First(x => x.BusCode == busCode);
-        if (setting == null) return null;
+        var setting = _timeLimitSettingRepository.Queryable().First(x => x.BusCode == busCode)
+            ?? throw new UserFriendlyException($"缺少busCode:{busCode}, 的time limit setting 设置;");
 
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
-        var result = await _expireTimeHandler.CalcEndTimeAsync(beginTime, timeConfig.TimeType, workTime.SettingValue, timeConfig.Count, setting.Percentage, setting.PercentageOne);
-        var expiredTimeWithConfig = _mapper.Map<ExpiredTimeWithConfig>(timeConfig);
-        expiredTimeWithConfig.ExpiredTime = result.EndTime;
-        expiredTimeWithConfig.NearlyExpiredTime = result.NearlyExpiredTime;
-        expiredTimeWithConfig.NearlyExpiredTimeOne = result.NearlyExpiredTimeOne;
-        return expiredTimeWithConfig;
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
+        var result = await _expireTimeHandler.CalcEndTimeAsync(beginTime, timeConfig.TimeType, workTime, timeConfig.Count, setting.Percentage, setting.PercentageOne);
+        var expiredTimeWithConfig = timeConfig.Adapt<ExpiredTimeWithConfig>();
+        return result.Adapt(expiredTimeWithConfig);
     }
 
     /// <summary>
@@ -143,8 +146,8 @@ public abstract class ExpireTimeLimitBase
         var setting = _timeLimitSettingRepository.Queryable().First(x => x.BusCode == busCode);
         if (setting == null) return null;
 
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
-        var result = await _expireTimeHandler.CalcEndTimeAsync(beginTime, setting.TimeType, workTime.SettingValue, setting.TimeValue, setting.Percentage, setting.PercentageOne);
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
+        var result = await _expireTimeHandler.CalcEndTimeAsync(beginTime, setting.TimeType, workTime, setting.TimeValue, setting.Percentage, setting.PercentageOne);
         var expiredTimeWithConfig = new ExpiredTimeWithConfig
         {
             TimeText = setting.TimeValue + "个" + setting.TimeType.GetDescription(),
@@ -159,16 +162,25 @@ public abstract class ExpireTimeLimitBase
 
     public virtual async Task<TimeResult?> CalcEndTime(DateTime beginTime, ETimeType timeType, int timeValue, string busCode)
     {
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
         var setting = _timeLimitSettingRepository.Queryable().First(x => x.BusCode == busCode);
         if (setting == null) return null;
-        return await _expireTimeHandler.CalcEndTimeAsync(beginTime, timeType, workTime.SettingValue, timeValue, setting.Percentage, setting.PercentageOne);
+        return await _expireTimeHandler.CalcEndTimeAsync(beginTime, timeType, workTime, timeValue, setting.Percentage, setting.PercentageOne);
     }
 
-    public async Task<TimeResult> CalcEndTime(DateTime beginTime, ETimeType timeType, int timeValue, int Percentage, int PercentageOne)
-    { 
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
-        return await _expireTimeHandler.CalcEndTimeAsync(beginTime, timeType, workTime.SettingValue, timeValue, Percentage, PercentageOne);
+    /// <summary>
+    /// 计算工作日
+    /// </summary>
+    /// <param name="beginTime"></param>
+    /// <param name="timeType"></param>
+    /// <param name="timeValue"></param>
+    /// <param name="Percentage">即将超期百分比</param>
+    /// <param name="PercentageOne">超期百分比第一级</param>
+    /// <returns></returns>    
+    public virtual async Task<TimeResult> CalcEndTime(DateTime beginTime, ETimeType timeType, int timeValue, int Percentage, int PercentageOne)
+    {
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
+        return await _expireTimeHandler.CalcEndTimeAsync(beginTime, timeType, workTime, timeValue, Percentage, PercentageOne);
     }
 
     /// <summary>
@@ -180,8 +192,8 @@ public abstract class ExpireTimeLimitBase
     /// <returns></returns>
     public virtual async Task<decimal> CalcWorkTimeToHour(DateTime beginTime, DateTime endTime, bool isCenter)
     {
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
-        var min = await _expireTimeHandler.CalcWorkTimeAsync(beginTime, endTime, workTime.SettingValue, isCenter);
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
+        var min = await _expireTimeHandler.CalcWorkTimeAsync(beginTime, endTime, workTime, isCenter);
         if (min == 0) return 0;
         if (isCenter)
         {
@@ -203,14 +215,14 @@ public abstract class ExpireTimeLimitBase
     /// <returns></returns>
     public virtual async Task<decimal> CalcWorkTimeToDecimal(DateTime beginTime, DateTime endTime, bool isCenter)
     {
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
-        var min = await _expireTimeHandler.CalcWorkTimeAsync(beginTime, endTime, workTime.SettingValue, isCenter);
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
+        var min = await _expireTimeHandler.CalcWorkTimeAsync(beginTime, endTime, workTime, isCenter);
         if (min == 0) return 0;
         if (isCenter) return Math.Round((decimal)min / 60 / 60 / 24, 2);
 
         if (workTime == null) return 0;
 
-        var (workBeginTime, workEndTime) = GetWorkTime(beginTime, workTime.SettingValue);
+        var (workBeginTime, workEndTime) = GetWorkTime(beginTime, workTime);
         var minuteSpan = new TimeSpan(workEndTime.Ticks - workBeginTime.Ticks);
 
         //总时差分钟数
@@ -220,16 +232,16 @@ public abstract class ExpireTimeLimitBase
 
     public virtual async Task<TimeResult?> CalcEndTime(EFlowDirection flowDirection, int Percentage, int PercentageOne, ETimeType? timeType = null, int? timeValue = null)
     {
-        var workTime = _systemSettingRepository.Get(x => x.Code == SettingConstants.WorkTime);
+        var workTime = GetWorkTimes(SettingConstants.WorkTime);
         switch (flowDirection)
         {
             case EFlowDirection.CenterToOrg:
                 if (!timeType.HasValue || !timeValue.HasValue)
                     throw new UserFriendlyException("无效参数");
-                return await _expireTimeHandler.CalcEndTimeAsync(DateTime.Now, timeType.Value, workTime.SettingValue, timeValue.Value, Percentage, PercentageOne);
+                return await _expireTimeHandler.CalcEndTimeAsync(DateTime.Now, timeType.Value, workTime, timeValue.Value, Percentage, PercentageOne);
             case EFlowDirection.OrgToCenter:
                 //todo 根据配置
-                return await _expireTimeHandler.CalcEndTimeAsync(DateTime.Now, ETimeType.Day, workTime.SettingValue, 1, Percentage, PercentageOne);
+                return await _expireTimeHandler.CalcEndTimeAsync(DateTime.Now, ETimeType.Day, workTime, 1, Percentage, PercentageOne);
             default:
                 throw new ArgumentOutOfRangeException(nameof(flowDirection), flowDirection, null);
         }
@@ -259,11 +271,14 @@ public abstract class ExpireTimeLimitBase
     /// <param name="date"></param>
     /// <param name="workTime"></param>
     /// <returns></returns>
-    private static (DateTime startTime, DateTime endTime) GetWorkTime(DateTime date, List<string> workTime)
+    private static (DateTime startTime, DateTime endTime) GetWorkTime(DateTime date, IList<string> workTime)
     {
         var startTime = DateTime.Parse(date.ToShortDateString() + " " + workTime[0] + ":00");
         var endTime = DateTime.Parse(date.ToShortDateString() + " " + workTime[1] + ":00");
         return (startTime, endTime);
     }
 
+    public IList<string> GetWorkTimes(string code)
+        => _systemSettingCacheManager.GetSetting(code)!.SettingValue;
+
 }

+ 16 - 0
src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/DaySupplier.cs

@@ -0,0 +1,16 @@
+using Hotline.Share.Dtos.Settings;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Dependency;
+
+namespace Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+public class DaySupplier : IExpireTimeSupplier, IScopeDependency
+{
+    public async Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, TimeConfig timeConfig)
+    {
+        return new TimeResult { EndTime = beginTime.AddDays(timeConfig.Count), RuleStr = timeConfig.Count + "个自然日", NearlyExpiredTime = beginTime };
+    }
+}

+ 26 - 0
src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/ExpireTimeFactory.cs

@@ -0,0 +1,26 @@
+using Hotline.Share.Enums.Settings;
+using XF.Domain.Dependency;
+
+namespace Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+public class ExpireTimeFactory : IScopeDependency
+{
+    private readonly IEnumerable<IExpireTimeSupplier> _expireTimeSuppliers;
+
+    public ExpireTimeFactory(IEnumerable<IExpireTimeSupplier> expireTimeSuppliers)
+    {
+        _expireTimeSuppliers = expireTimeSuppliers;
+    }
+
+    public IExpireTimeSupplier GetSupplier(ETimeType timeType)
+    {
+        foreach (var supplier in _expireTimeSuppliers)
+        {
+            if (supplier.GetType().Name == timeType.ToString() + "Supplier")
+            {
+                return supplier;
+            }
+        }
+
+        return _expireTimeSuppliers.First();
+    }
+}

+ 16 - 0
src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/HourSupplier.cs

@@ -0,0 +1,16 @@
+using Hotline.Share.Dtos.Settings;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Dependency;
+
+namespace Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+public class HourSupplier : IExpireTimeSupplier, IScopeDependency
+{
+    public async Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, TimeConfig timeConfig)
+    {
+        return new TimeResult { EndTime = beginTime.AddHours(timeConfig.Count), RuleStr = timeConfig.Count + "个小时", NearlyExpiredTime = beginTime };
+    }
+}

+ 13 - 0
src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/IExpireTimeSupplier.cs

@@ -0,0 +1,13 @@
+using Hotline.Share.Dtos.Settings;
+
+namespace Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+public interface IExpireTimeSupplier
+{
+    /// <summary>
+    /// 计算期满时间
+    /// </summary>
+    /// <param name="beginTime"></param>
+    /// <param name="timeConfig"></param>
+    /// <returns></returns>
+    Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, TimeConfig timeConfig);
+}

+ 120 - 0
src/Hotline/Settings/TimeLimitDomain/ExpireTimeSupplier/WorkDaySupplier.cs

@@ -0,0 +1,120 @@
+using Hotline.Settings.TimeLimits;
+using Hotline.Share.Dtos.Settings;
+using XF.Domain.Dependency;
+
+namespace Hotline.Settings.TimeLimitDomain.ExpireTimeSupplier;
+public class WorkDaySupplier : IExpireTimeSupplier, IScopeDependency
+{
+    private readonly IDaySettingRepository _daySettingRepository;
+
+    public WorkDaySupplier(IDaySettingRepository daySettingRepository)
+    {
+        _daySettingRepository = daySettingRepository;
+    }
+
+    public async Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, TimeConfig timeConfig)
+    {
+        var startTime = beginTime;
+        var startTimeOne = beginTime;
+        if (timeConfig.WorkTime == null)
+            return new TimeResult { EndTime = beginTime, RuleStr = timeConfig.Count + "个工作日", NearlyExpiredTime = beginTime, NearlyExpiredTimeOne = beginTime };
+
+        //检查时间段内是否存在休息日或者工作日
+        //查询统一部门工作时间
+        //计算一天工作时间(分钟)
+        DateTime WorkBeginTime = DateTime.Parse(beginTime.ToShortDateString() + " " + timeConfig.WorkTime[0] + ":00");
+        DateTime WorkEndTime = DateTime.Parse(beginTime.ToShortDateString() + " " + timeConfig.WorkTime[1] + ":00");
+        //计算一天多少个工作小时
+        var duration = WorkEndTime - WorkBeginTime;
+        double workMinutes = duration.TotalMinutes;
+        double totalWorkMinutes = (workMinutes * timeConfig.Count) * (timeConfig.Percentage / 100.00);
+        double totalWorkMinutesOne = (workMinutes * timeConfig.Count) * (timeConfig.PercentageOne / 100.00);
+
+        //判断开始时间不在工时间段修正时间复位至当天或者第二天的开始时间(如果大于结束时间则复位至第二天的开始时间)
+        if (beginTime < WorkBeginTime)
+        {
+            beginTime = WorkBeginTime;
+        }
+        else
+        {
+            if (beginTime > WorkEndTime)
+            {
+                beginTime = WorkBeginTime.AddDays(1);
+            }
+        }
+
+        int day = 1;
+        for (int i = 1;i < timeConfig.Count + 1;i++)
+        {
+            if (await IsWorkDay(beginTime.AddDays(day)))
+            {
+                beginTime = beginTime.AddDays(day);
+
+                if (totalWorkMinutes >= workMinutes)
+                {
+                    startTime = startTime.AddDays(day);
+                    totalWorkMinutes = totalWorkMinutes - workMinutes;
+                }
+                else if (totalWorkMinutes < workMinutes && totalWorkMinutes != 0)
+                {
+                    if (startTime.AddMinutes(totalWorkMinutes) > DateTime.Parse(startTime.ToShortDateString() + " " + timeConfig.WorkTime[1] + ":00"))
+                    {
+                        totalWorkMinutes = totalWorkMinutes - ((DateTime.Parse(startTime.ToShortDateString() + " " + timeConfig.WorkTime[1] + ":00") - startTime).TotalMinutes);
+                        startTime = startTime.AddDays(day);
+                        startTime = DateTime.Parse(startTime.ToShortDateString() + " " + timeConfig.WorkTime[0] + ":00").AddMinutes(totalWorkMinutes);
+                        totalWorkMinutes = 0;
+                    }
+                    else
+                    {
+                        startTime = startTime.AddMinutes(totalWorkMinutes);
+                    }
+                }
+
+                if (totalWorkMinutesOne >= workMinutes)
+                {
+                    startTimeOne = startTimeOne.AddDays(day);
+                    totalWorkMinutesOne = totalWorkMinutesOne - workMinutes;
+                }
+                else if (totalWorkMinutesOne < workMinutes && totalWorkMinutesOne != 0)
+                {
+                    if (startTimeOne.AddMinutes(totalWorkMinutesOne) > DateTime.Parse(startTimeOne.ToShortDateString() + " " + timeConfig.WorkTime[1] + ":00"))
+                    {
+                        totalWorkMinutesOne = totalWorkMinutesOne - ((DateTime.Parse(startTimeOne.ToShortDateString() + " " + timeConfig.WorkTime[1] + ":00") - startTimeOne).TotalMinutes);
+                        startTimeOne = startTimeOne.AddDays(day);
+                        startTimeOne = DateTime.Parse(startTimeOne.ToShortDateString() + " " + timeConfig.WorkTime[0] + ":00").AddMinutes(totalWorkMinutesOne);
+                        totalWorkMinutesOne = 0;
+                    }
+                    else
+                    {
+                        startTimeOne = startTimeOne.AddMinutes(totalWorkMinutesOne);
+                    }
+                }
+
+
+                day = 1;
+            }
+            else
+            {
+                i--;
+                day++;
+            }
+        }
+        return new TimeResult { EndTime = beginTime, RuleStr = timeConfig.Count + "个工作日", NearlyExpiredTime = startTime, NearlyExpiredTimeOne = startTimeOne };
+    }
+
+    /// <summary>
+    /// 是否是工作日
+    /// 根据数据库的工作日和周末时间判断
+    /// </summary>
+    /// <param name="date"></param>
+    /// <returns></returns>
+    private async Task<bool> IsWorkDay(DateTime date)
+    {
+        return await _daySettingRepository.IsWorkDay(date);
+    }
+
+    private async Task<bool> NotWorkDay(DateTime date)
+    {
+        return !await IsWorkDay(date);
+    }
+}

+ 10 - 3
src/Hotline/Settings/TimeLimitDomain/ICalcExpireTime.cs

@@ -10,12 +10,10 @@ public interface ICalcExpireTime
 
     public Task<ExpiredTimeWithConfig?> CalcEndTime(DateTime beginTime, string busCode);
 
-    public Task<ExpiredTimeWithConfig?> CalcEndTime(DateTime beginTime, TimeConfig timeConfig, string? busCode);
+    public Task<ExpiredTimeWithConfig> CalcEndTime(DateTime beginTime, TimeConfig timeConfig, string? busCode);
 
     public Task<TimeResult?> CalcEndTime(EFlowDirection flowDirection, int Percentage, int PercentageOne, ETimeType? timeType = null, int? timeValue = null);
 
-    public Task<ExpiredTimeWithConfig?> CalcExpiredTime(DateTime beginTime, EFlowDirection flowDirection, string? acceptTypeCode);
-
     public Task<decimal> CalcWorkTimeToDecimal(DateTime beginTime, DateTime endTime, bool isCenter);
 
     public Task<decimal> CalcWorkTimeToHour(DateTime beginTime, DateTime endTime, bool isCenter);
@@ -40,4 +38,13 @@ public interface ICalcExpireTime
     Task<DateTime?> CalcWorkTimeReduce(DateTime now, int timeValue);
 
     TimeConfig GetOrderTimeLimitConfig(string? code = null);
+
+    /// <summary>
+    /// 计算工单期满时间
+    /// </summary>
+    /// <param name="now"></param>
+    /// <param name="centerToOrg"></param>
+    /// <param name="orderTimeClacInfo"></param>
+    /// <returns></returns>
+    Task<ExpiredTimeWithConfig> CalcExpiredTime(DateTime now, EFlowDirection centerToOrg, OrderTimeClacInfo orderTimeClacInfo);
 }

+ 7 - 3
src/Hotline/Settings/TimeLimitDomain/IExpireTimeHandler.cs

@@ -5,6 +5,8 @@ using XF.Domain.Dependency;
 namespace Hotline.Settings.TimeLimitDomain;
 public interface IExpireTimeHandler 
 {
+    Task<bool> NotWorkDay(DateTime date);
+
     /// <summary>
     /// 计算工作时间分钟数
     /// </summary>
@@ -13,7 +15,7 @@ public interface IExpireTimeHandler
     /// <param name="workTime">工作日时间段</param>
     /// <param name="isCenter"></param>
     /// <returns>秒数</returns>
-    public Task<int> CalcWorkTimeAsync(DateTime beginTime, DateTime endTime, List<string>? workTime, bool isCenter);
+    public Task<int> CalcWorkTimeAsync(DateTime beginTime, DateTime endTime, IList<string>? workTime, bool isCenter);
 
     /// <summary>
     /// 计算工作时间分钟数(已改返回秒)
@@ -23,7 +25,7 @@ public interface IExpireTimeHandler
     /// <param name="workTime">工作日时间段</param>
     /// <param name="isCenter"></param>
     /// <returns></returns>
-    public Task<int> CalcWorkTimeExAsync(DateTime beginTime, DateTime endTime, List<string>? workTime, bool isCenter);
+    public Task<int> CalcWorkTimeExAsync(DateTime beginTime, DateTime endTime, IList<string>? workTime, bool isCenter);
 
     /// <summary>
     /// 计算工作日
@@ -35,7 +37,9 @@ public interface IExpireTimeHandler
     /// <param name="Percentage">即将超期百分比</param>
     /// <param name="PercentageOne">超期百分比第一级</param>
     /// <returns></returns>
-    public Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, ETimeType timeType, List<string>? workTime, int timeValue, int Percentage, int PercentageOne);
+    public Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, ETimeType timeType, IList<string>? workTime, int timeValue, int Percentage, int PercentageOne);
+
+    Task<TimeResult> CalcEndTimeAsync(DateTime beginTime, TimeConfig timeConfig);
 
     /// <summary>
     /// 计算时间间隔

+ 15 - 0
src/Hotline/Settings/TimeLimitDomain/Repository/ITimeLimitSettingAttributeRepository.cs

@@ -0,0 +1,15 @@
+using Hotline.Settings.TimeLimits;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Settings.TimeLimitDomain.Repository;
+public interface ITimeLimitSettingAttributeRepository : IRepository<TimeLimitSettingAttribute>
+{
+    Task<TimeLimitSettingAttribute?> GetAsync(string busCode, string name, string value);
+    Task<TimeLimitSettingAttribute?> GetAsync(string name, string value);
+    Task<TimeLimitSettingAttribute?> GetFuzzyAsync(string busCode, string name, string value);
+}

+ 12 - 0
src/Hotline/Settings/TimeLimitDomain/Repository/ITimeLimitSettingInventoryRepository.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Settings.TimeLimitDomain.Repository;
+public interface ITimeLimitSettingInventoryRepository : IRepository<TimeLimitSettingInventory>
+{
+    Task<TimeLimitSettingInventory?> GetByCode(string code);
+}

+ 12 - 0
src/Hotline/Settings/TimeLimitDomain/Repository/ITimeLimitSettingRepository.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using XF.Domain.Repository;
+
+namespace Hotline.Settings.TimeLimitDomain.Repository;
+public interface ITimeLimitSettingRepository : IRepository<TimeLimitSetting>
+{
+    Task<TimeLimitSetting?> GetByBusCode(string busCode);
+}

+ 5 - 6
src/Hotline/Settings/TimeLimitDomain/YBExpireTimeLimit.cs → src/Hotline/Settings/TimeLimitDomain/YiBinExpireTimeLimit.cs

@@ -1,21 +1,19 @@
 using Hotline.Caching.Interfaces;
+using Hotline.DI;
 using Hotline.Settings.TimeLimits;
 using Hotline.Share.Dtos.Settings;
 using Hotline.Share.Enums.FlowEngine;
-using Hotline.Share.Enums.Settings;
 using MapsterMapper;
-using System.Runtime.CompilerServices;
 using XF.Domain.Dependency;
-using XF.Domain.Exceptions;
 using XF.Domain.Repository;
-using XF.Utility.EnumExtensions;
 
 namespace Hotline.Settings.TimeLimitDomain;
 
 /// <summary>
 /// 宜宾过期时间计算
 /// </summary>
-public class YBExpireTimeLimit : ExpireTimeLimitBase, ICalcExpireTime, IScopeDependency
+[Injection(AppScopes = EAppScope.YiBin)]
+public class YiBinExpireTimeLimit : ExpireTimeLimitBase, ICalcExpireTime, IScopeDependency
 {
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
     private readonly IRepository<TimeLimitSetting> _timeLimitSettingRepository;
@@ -24,7 +22,7 @@ public class YBExpireTimeLimit : ExpireTimeLimitBase, ICalcExpireTime, IScopeDep
     private readonly IRepository<SystemSetting> _systemSettingRepository;
     private readonly IDaySettingRepository _daySettingRepository;
 
-    public YBExpireTimeLimit(ISystemSettingCacheManager systemSettingCacheManager, IRepository<TimeLimitSetting> timeLimitSettingRepository, IExpireTimeHandler expireTimeHandler, IMapper mapper, IRepository<SystemSetting> systemSettingRepository, IDaySettingRepository daySettingRepository) : base(systemSettingCacheManager, timeLimitSettingRepository, expireTimeHandler, mapper, systemSettingRepository, daySettingRepository)
+    public YiBinExpireTimeLimit(ISystemSettingCacheManager systemSettingCacheManager, IRepository<TimeLimitSetting> timeLimitSettingRepository, IExpireTimeHandler expireTimeHandler, IMapper mapper, IRepository<SystemSetting> systemSettingRepository, IDaySettingRepository daySettingRepository) : base(systemSettingCacheManager, timeLimitSettingRepository, expireTimeHandler, mapper,daySettingRepository)
     {
         _systemSettingCacheManager = systemSettingCacheManager;
         _timeLimitSettingRepository = timeLimitSettingRepository;
@@ -33,4 +31,5 @@ public class YBExpireTimeLimit : ExpireTimeLimitBase, ICalcExpireTime, IScopeDep
         _systemSettingRepository = systemSettingRepository;
         _daySettingRepository = daySettingRepository;
     }
+
 }

+ 114 - 0
src/Hotline/Settings/TimeLimitDomain/ZiGongExpireTimeLimit.cs

@@ -0,0 +1,114 @@
+using Hotline.Caching.Interfaces;
+using Hotline.DI;
+using Hotline.Settings.TimeLimitDomain.Repository;
+using Hotline.Settings.TimeLimits;
+using Hotline.Share.Dtos.Settings;
+using Hotline.Share.Enums.FlowEngine;
+using Hotline.Share.Tools;
+using Mapster;
+using MapsterMapper;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+
+namespace Hotline.Settings.TimeLimitDomain;
+
+/// <summary>
+/// 自贡过期时间计算
+/// </summary>
+[Injection(AppScopes = EAppScope.ZiGong)]
+public class ZiGongExpireTimeLimit : ExpireTimeLimitBase, ICalcExpireTime, IScopeDependency
+{
+    private readonly ISystemSettingCacheManager _systemSettingCacheManager;
+    private readonly ITimeLimitSettingRepository _timeLimitSettingRepository;
+    private readonly ITimeLimitSettingInventoryRepository _timeLimitSettingInventoryRepository;
+    private readonly ITimeLimitSettingAttributeRepository _timeLimitSettingAttributeRepository;
+    private readonly IExpireTimeHandler _expireTimeHandler;
+    private readonly IMapper _mapper;
+    private readonly IRepository<SystemSetting> _systemSettingRepository;
+    private readonly IDaySettingRepository _daySettingRepository;
+
+    public ZiGongExpireTimeLimit(ISystemSettingCacheManager systemSettingCacheManager, ITimeLimitSettingRepository timeLimitSettingRepository, IExpireTimeHandler expireTimeHandler, IMapper mapper, IRepository<SystemSetting> systemSettingRepository, IDaySettingRepository daySettingRepository, ITimeLimitSettingAttributeRepository timeLimitSettingAttributeRepository, ITimeLimitSettingInventoryRepository timeLimitSettingInventoryRepository) : base(systemSettingCacheManager, timeLimitSettingRepository, expireTimeHandler, mapper, daySettingRepository)
+    {
+        _systemSettingCacheManager = systemSettingCacheManager;
+        _timeLimitSettingRepository = timeLimitSettingRepository;
+        _expireTimeHandler = expireTimeHandler;
+        _mapper = mapper;
+        _systemSettingRepository = systemSettingRepository;
+        _daySettingRepository = daySettingRepository;
+        _timeLimitSettingAttributeRepository = timeLimitSettingAttributeRepository;
+        _timeLimitSettingInventoryRepository = timeLimitSettingInventoryRepository;
+    }
+
+    public override async Task<ExpiredTimeWithConfig> CalcExpiredTime(DateTime beginTime, EFlowDirection flowDirection, OrderTimeClacInfo order)
+    {
+        if (EFlowDirection.CenterToOrg == flowDirection)
+        {
+            var timeConfig = await GetTimeConfigByOrderAsync(order);
+            timeConfig.WorkTime = GetWorkTimes(SettingConstants.WorkTime);
+            var result = await _expireTimeHandler.CalcEndTimeAsync(beginTime, timeConfig);
+
+            var expiredTimeWithConfig = timeConfig.Adapt<ExpiredTimeWithConfig>();
+            return result.Adapt(expiredTimeWithConfig);
+        }
+
+        return await base.CalcExpiredTime(beginTime, flowDirection, order);
+    }
+
+    /// <summary>
+    /// 根据订单信息获取时间配置
+    /// </summary>
+    /// <param name="orderInfo"></param>
+    /// <returns></returns>
+    /// <exception cref="UserFriendlyException"></exception>
+    private async Task<TimeConfig> GetTimeConfigByOrderAsync(OrderTimeClacInfo orderInfo)
+    {
+        var busCode = string.Empty;
+        var code = string.Empty;
+        orderInfo.ForeachClassProperties(async (order, property, name, value) =>
+        {
+            if (value is null) return true;
+            if (name == OrderTimeClacInfo.BusCodeName)
+            {
+                busCode = value.ToString();
+                return true;
+            }
+            TimeLimitSettingAttribute? timeLimitAttribute = null;
+            var query = _timeLimitSettingAttributeRepository.Queryable();
+            var realValue = string.Empty;
+            if (value is Boolean)
+            {
+                if ((bool)value == false) return true;
+                realValue = "true";
+            }
+            else if (value is String || value is Enum)
+            {
+                if (value is null) return true;
+                realValue = value.ToString()!;
+            }
+            query = query.Where(m => m.Name == name);
+            if (property.HasAttribute<IsFuzzyQueryAttribute>())
+                query = query.Where(m => realValue.Contains(m.Value));
+            else
+                query = query.Where(m => m.Value == realValue);
+
+            var noBusCodeAttribute = await query.FirstAsync(m => m.BusCode == "");
+            if (false == property.HasAttribute<NoCodeAttribute>())
+                query = query.Where(m => m.BusCode == busCode);
+            timeLimitAttribute = await query.FirstAsync();
+            timeLimitAttribute ??= noBusCodeAttribute;
+            if (timeLimitAttribute is null) return true;
+            code += timeLimitAttribute.Code;
+            if (timeLimitAttribute is not null && false == timeLimitAttribute.IsCommon)
+                return false;
+            return true;
+        });
+        if (busCode is null && code.IsNullOrEmpty()) base.GetOrderTimeLimitConfig();
+        var inventory = await _timeLimitSettingInventoryRepository.GetByCode(code);
+        if (inventory is not null) return inventory.Adapt<TimeConfig>();
+        var timeConfig = await _timeLimitSettingRepository.GetByBusCode(busCode)
+            ?? throw new UserFriendlyException($"缺少busCode:{busCode}, 的time_limit_setting 设置;");
+        return timeConfig.Adapt<TimeConfig>();
+    }
+}
+

+ 102 - 29
src/Hotline/Settings/TimeLimitSetting.cs

@@ -1,5 +1,7 @@
 using System.ComponentModel;
+using Hotline.Share.Enums.Order;
 using Hotline.Share.Enums.Settings;
+using SqlSugar;
 using XF.Domain.Repository;
 
 namespace Hotline.Settings;
@@ -7,33 +9,104 @@ namespace Hotline.Settings;
 [Description("工时常量配置")]
 public class TimeLimitSetting : CreationEntity
 {
-        /// <summary>
-        /// 业务Code
-        /// </summary>
-        public string BusCode { get; set; }
-
-        /// <summary>
-        /// 时间类型
-        /// </summary>
-        public ETimeType TimeType { get; set; }
-
-        /// <summary>
-        /// 时间值
-        /// </summary>
-        public int TimeValue { get; set; }
-
-        /// <summary>
-        /// 业务名称
-        /// </summary>
-        public string BusName { get; set; }
-
-        /// <summary>
-        /// 超期时限百分比
-        /// </summary>
-        public int Percentage { get; set; }
-
-        /// <summary>
-        /// 超期时间百分比(第一级)
-        /// </summary>
-        public int PercentageOne { get; set; }
+    /// <summary>
+    /// 业务Code
+    /// </summary>
+    public string BusCode { get; set; }
+
+    /// <summary>
+    /// 时间类型
+    /// </summary>
+    public ETimeType TimeType { get; set; }
+
+    /// <summary>
+    /// 时间值
+    /// </summary>
+    public int TimeValue { get; set; }
+
+    /// <summary>
+    /// 业务名称
+    /// </summary>
+    public string BusName { get; set; }
+
+    /// <summary>
+    /// 超期时限百分比
+    /// </summary>
+    public int Percentage { get; set; }
+
+    /// <summary>
+    /// 超期时间百分比(第一级)
+    /// </summary>
+    public int PercentageOne { get; set; }
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    public string? Remark { get; set; }
+}
+
+
+[Description("工时常量 属性表")]
+public class TimeLimitSettingAttribute : CreationEntity
+{
+    /// <summary>
+    /// 业务Code
+    /// </summary>
+    public string BusCode { get; set; }
+
+    /// <summary>
+    /// 自增长编码
+    /// </summary>
+    [SugarColumn(IsIdentity = true)]
+    public int Code { get; set; }
+
+    /// <summary>
+    /// 属性名称
+    /// </summary>
+    public string Name { get; set; }
+
+    /// <summary>
+    /// 属性值
+    /// </summary>
+    public string Value { get; set; }
+
+    /// <summary>
+    /// 是否可以组合
+    /// </summary>
+    public bool IsCommon { get; set; }
+
+}
+
+[Description("工作常量 库存")]
+public class TimeLimitSettingInventory : CreationEntity
+{ 
+    /// <summary>
+    /// 备注
+    /// </summary>
+    public string? Remark { get; set; }
+
+    /// <summary>
+    /// 属性表 Code 的 和
+    /// </summary>
+    public string Code { get; set; } 
+
+    /// <summary>
+    /// 时间值
+    /// </summary>
+    public int TimeValue { get; set; }
+
+    /// <summary>
+    /// 时间类型
+    /// </summary>
+    public ETimeType TimeType { get; set; }
+
+    /// <summary>
+    /// 超期时限百分比
+    /// </summary>
+    public int Percentage { get; set; }
+
+    /// <summary>
+    /// 超期时间百分比(第一级)
+    /// </summary>
+    public int PercentageOne { get; set; }    
 }

+ 1 - 1
src/XF.Domain/Dependency/DependencyInjectionExtensions.cs

@@ -84,7 +84,7 @@ public static class DependencyInjectionExtensions
     }
 }
 
-internal class ServiceRegister
+public class ServiceRegister
 {
     public static void Register(IServiceCollection services, Type type)
     {