Преглед изворни кода

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

田爽 пре 2 недеља
родитељ
комит
9872d2de00
48 измењених фајлова са 904 додато и 197 уклоњено
  1. 27 2
      src/Hotline.Api/Controllers/Bi/BiCallController.cs
  2. 116 16
      src/Hotline.Api/Controllers/Bi/BiOrderController.cs
  3. 11 0
      src/Hotline.Api/Controllers/Exam/ExamManageController.cs
  4. 3 0
      src/Hotline.Api/Controllers/OrderController.cs
  5. 2 1
      src/Hotline.Api/Controllers/WebPortalController.cs
  6. 2 0
      src/Hotline.Application/Exam/Constants/ApiRoutes/ExamManageApiRoute.cs
  7. 10 2
      src/Hotline.Application/Exam/Interface/ExamManages/IExamManageService.cs
  8. 5 1
      src/Hotline.Application/Exam/Interface/Strategy/IExamStrategy.cs
  9. 3 1
      src/Hotline.Application/Exam/QueryExtensions/ExamManages/UserExamQueryExtensions.cs
  10. 1 0
      src/Hotline.Application/Exam/QueryExtensions/Practices/PracticeQueryExtensions.cs
  11. 1 0
      src/Hotline.Application/Exam/QueryExtensions/Questions/QuestionQueryExtesions.cs
  12. 71 28
      src/Hotline.Application/Exam/Service/ExamManages/ExamManageService.cs
  13. 1 1
      src/Hotline.Application/Exam/Service/ExamManages/ExtractRuleService.cs
  14. 175 30
      src/Hotline.Application/Exam/Service/ExamManages/UserExamService.cs
  15. 20 16
      src/Hotline.Application/Exam/Service/Practices/PracticeService.cs
  16. 2 2
      src/Hotline.Application/Exam/Service/Questions/QuestionService.cs
  17. 5 5
      src/Hotline.Application/Exam/Service/TestPapers/TestPaperService.cs
  18. 25 4
      src/Hotline.Application/Exam/Service/Trains/TrainRecordService.cs
  19. 17 3
      src/Hotline.Application/Exam/Strategy/CheckEndTimeStrategy.cs
  20. 17 3
      src/Hotline.Application/Exam/Strategy/CheckStartTimeStrategy.cs
  21. 15 3
      src/Hotline.Application/Exam/Strategy/CheckValidateCountStrategy.cs
  22. 19 3
      src/Hotline.Application/Exam/Strategy/CheckValidateTimeStrategy.cs
  23. 12 8
      src/Hotline.Application/Exam/Strategy/ExamStrategyProxy.cs
  24. 33 18
      src/Hotline.Application/OrderApp/IOrderApplication.cs
  25. 119 1
      src/Hotline.Application/OrderApp/OrderApplication.cs
  26. 1 1
      src/Hotline.Application/Snapshot/Notifications/SnapshotHandler.cs
  27. 1 1
      src/Hotline.Application/Snapshot/SnapshotApplicationBase.cs
  28. 4 2
      src/Hotline.Application/StatisticalReport/CallReport/CallReportApplicationBase.cs
  29. 4 2
      src/Hotline.Application/StatisticalReport/CallReport/YiBinCallReportApplication.cs
  30. 1 0
      src/Hotline.Repository.SqlSugar/Exam/Validators/ExamManages/ExamManageValidator.cs
  31. 0 6
      src/Hotline.Share/Dtos/ExamManages/GradingExamItemDto.cs
  32. 16 3
      src/Hotline.Share/Dtos/ExamManages/GradingExamQuestionDto.cs
  33. 80 2
      src/Hotline.Share/Dtos/Order/OrderVisitDto.cs
  34. 5 0
      src/Hotline.Share/Dtos/TrCallCenter/TrTelDao.cs
  35. 6 3
      src/Hotline.Share/Dtos/Trains/TrainPracticeDto.cs
  36. 5 0
      src/Hotline.Share/Dtos/Trains/TrainPracticeOptionsDto.cs
  37. 8 0
      src/Hotline.Share/Dtos/Trains/TrainRecordAnswerDto.cs
  38. 3 1
      src/Hotline.Share/Exams/Extensions/ReportPagedRequestExtensions.cs
  39. 2 2
      src/Hotline.Share/Requests/Exam/UserExamResultReportPagedRequest.cs
  40. 6 0
      src/Hotline.Share/Requests/Question/QuestionPagedRequest.cs
  41. 3 2
      src/TianQue.Sdk/TQHttpClient.cs
  42. 5 5
      test/Hotline.Tests/Application/OrderSnapshotApplicationTest.cs
  43. 10 3
      test/Hotline.Tests/Application/RedPackApplicationTest.cs
  44. 9 8
      test/Hotline.Tests/Application/SnapshotApplicationTest.cs
  45. 1 1
      test/Hotline.Tests/Application/SystemSettingCacheManagerTest.cs
  46. 3 3
      test/Hotline.Tests/Controller/OrderControllerTest.cs
  47. 1 1
      test/Hotline.Tests/Infrastructure/TianQueTest.cs
  48. 18 3
      test/Hotline.Tests/Mock/OrderServiceMock.cs

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

@@ -26,6 +26,9 @@ using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Api.Filter;
 using Hotline.Share.Dtos.Order.Publish;
 using Hotline.Application.StatisticalReport.CallReport;
+using DocumentFormat.OpenXml.Spreadsheet;
+using DocumentFormat.OpenXml.Wordprocessing;
+using NPOI.SS.Formula.Functions;
 
 namespace Hotline.Api.Controllers.Bi;
 
@@ -401,8 +404,30 @@ public class BiCallController : BaseController
     /// <returns></returns>
     [HttpGet("seats")]
     [AllowAnonymous]
-    public async Task<IReadOnlyList<BiSeatCallsDto>> QuerySeatCallsAsync([FromQuery] ReportRequiredPagedRequest dto)
-        => await _callReportApplication.QuerySeatCallAsync(dto, HttpContext.RequestAborted);
+    public async Task<TotalList<BiSeatCallsDto>> QuerySeatCallsAsync([FromQuery] ReportRequiredPagedRequest dto)
+    {
+        var items = await _callReportApplication.QuerySeatCallAsync(dto, HttpContext.RequestAborted);
+        var total = new BiSeatCallsDto
+        {
+            Name="合计",                                                  // 坐席姓名
+            UserId="",                                                    // 用户ID
+            StaffNo="",                                                   // 工号
+            TelNo="",                                                     // 分机号
+            InTotal = items.Sum(m => m.InTotal),                          // 呼入总量 
+            OutTotal = items.Sum(m => m.OutTotal),                        // 呼出总量
+            InAnswered = items.Sum(m => m.InAnswered),                    // 呼入接通量
+            OutAnswered = items.Sum(m => m.OutAnswered),                  // 呼出接通量
+            InHangupImmediate = items.Sum(m => m.InHangupImmediate),      // 呼入秒挂
+            InHanguped = items.Sum(m => m.InHanguped),                    // 呼入未接
+            InDurationAvg = items.Sum(m => m.InDurationAvg),              // 呼入平均时长
+            OutDurationAvg = items.Sum(m => m.OutDurationAvg),            // 呼出平均时长
+            InAvailableAnswer = items.Sum(m => m.InAvailableAnswer),      // 有效接通量
+            InHangupImmediateWhenAnswered = items.Sum(m => m.InHangupImmediateWhenAnswered),      // 呼入接通秒挂
+            LoginDuration = items.Sum(m => m.LoginDuration),              // 登录时长(秒)
+            RestDuration = items.Sum(m => m.RestDuration),                // 小休+摘机时长 (秒)
+        };
+        return new TotalList<BiSeatCallsDto>(items, total);
+    }
 
     /// <summary>
     /// 坐席话务统计分析明细

+ 116 - 16
src/Hotline.Api/Controllers/Bi/BiOrderController.cs

@@ -327,8 +327,20 @@ namespace Hotline.Api.Controllers.Bi
         /// </summary>
         /// <returns></returns>
         [HttpGet("visit/quantity")]
-        public async Task<IList<OrderVisitQuantityOutDto>> QueryOrderVisitQuantityAsync([FromQuery] QueryOrderVisitQuantity dto)
-            => await _orderVisitApplication.QueryOrderVisitQuantityAsync(dto);
+        public async Task<TotalList<OrderVisitQuantityOutDto>> QueryOrderVisitQuantityAsync([FromQuery] QueryOrderVisitQuantity dto)
+        {
+            var items = await _orderVisitApplication.QueryOrderVisitQuantityAsync(dto);
+            var total = new OrderVisitQuantityOutDto
+            {
+                EmployeeName = "合计",
+                EmployeeId = "",
+                CallVisitCount = items.Sum(m => m.CallVisitCount),
+                DefaultVisitCount = items.Sum(m => m.DefaultVisitCount),
+                SmsVisitCount = items.Sum(m => m.SmsVisitCount)
+            };
+            return new TotalList<OrderVisitQuantityOutDto>(items, total);
+
+        }
 
         /// <summary>
         /// 回访量统计--导出
@@ -342,6 +354,94 @@ namespace Hotline.Api.Controllers.Bi
             return _exportApplication.GetExcelFile(dto, items, "回访量统计");
         }
 
+        /// <summary>
+        /// 回访量明细
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpGet("visit/quantity/list")]
+        public async Task<PagedDto<OrderVisitDto>> QueryOrderVisitQuantityListAsync([FromQuery] QueryOrderVisitListDto dto)
+        {
+            if (_appOptions.Value.IsZiGong == true)
+            {
+                var (total, items) = await _orderApplication.QueryOrderVisitQuantityListByZgAsync(dto)
+                .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+                return new PagedDto<OrderVisitDto>(total, _mapper.Map<IReadOnlyList<OrderVisitDto>>(items));
+            }
+            else
+            {
+                var (total, items) = await _orderApplication.QueryOrderVisitQuantityListAsync(dto)
+                .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+                return new PagedDto<OrderVisitDto>(total, _mapper.Map<IReadOnlyList<OrderVisitDto>>(items));
+            }
+        }
+
+        /// <summary>
+        /// 回访量明细--导出
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("visit/quantity/list/export")]
+        [LogFilterAlpha("导出日志")]
+        public async Task<FileStreamResult> QueryOrderVisitQuantityExport([FromBody] ExportExcelDto<QueryOrderVisitListDto> dto)
+        {
+            if (_appOptions.Value.IsZiGong == true)
+            {
+                var query = _orderApplication.QueryOrderVisitQuantityListByZgAsync(dto.QueryDto);
+                List<OrderVisitRecord> orders;
+                if (dto.IsExportAll)
+                {
+                    orders = await query.ToListAsync(HttpContext.RequestAborted);
+                }
+                else
+                {
+                    var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+                    orders = items;
+                }
+
+
+                var orderDtos = _mapper.Map<ICollection<OrderVisitDto>>(orders);
+                dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass<OrderVisitDto>(dto.ColumnInfos);
+
+                var dtos = orderDtos
+                    .Select(stu => _mapper.Map(stu, typeof(OrderVisitDto), dynamicClass))
+                    .Cast<object>()
+                    .ToList();
+
+                var stream = ExcelHelper.CreateStream(dtos);
+                var excelTitle = "回访量明细";
+                return ExcelStreamResult(stream, excelTitle);
+            }
+            else
+            {
+                var query = _orderApplication.QueryOrderVisitQuantityListAsync(dto.QueryDto);
+                List<OrderVisit> orders;
+                if (dto.IsExportAll)
+                {
+                    orders = await query.ToListAsync(HttpContext.RequestAborted);
+                }
+                else
+                {
+                    var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+                    orders = items;
+                }
+
+
+                var orderDtos = _mapper.Map<ICollection<OrderVisitDto>>(orders);
+                dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass<OrderVisitDto>(dto.ColumnInfos);
+
+                var dtos = orderDtos
+                    .Select(stu => _mapper.Map(stu, typeof(OrderVisitDto), dynamicClass))
+                    .Cast<object>()
+                    .ToList();
+
+                var stream = ExcelHelper.CreateStream(dtos);
+                var excelTitle = "回访量明细";
+                return ExcelStreamResult(stream, excelTitle);
+            }
+        }
+
+
         /// <summary>
         /// 部门超期统计明细
         /// </summary>
@@ -3034,14 +3134,14 @@ namespace Hotline.Api.Controllers.Bi
                   //WaitVisitd = SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState != EVisitState.None && x.VisitState != EVisitState.Visited, 1, 0)),//待回访
               }).FirstAsync();
 
-			var waitVisitd = await _orderVisitRepository.Queryable()
-			 .Where(x => x.CreationTime >= StartTime && x.CreationTime <= EndTime).Select(x =>
-				   SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState != EVisitState.None && x.VisitState != EVisitState.Visited, 1, 0)) //待回访
-			  ).FirstAsync();
-			centerReportVisitd.WaitVisitd = waitVisitd;
+            var waitVisitd = await _orderVisitRepository.Queryable()
+             .Where(x => x.CreationTime >= StartTime && x.CreationTime <= EndTime).Select(x =>
+                   SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState != EVisitState.None && x.VisitState != EVisitState.Visited, 1, 0)) //待回访
+              ).FirstAsync();
+            centerReportVisitd.WaitVisitd = waitVisitd;
 
-			//部门
-			var listOrg = await _orderVisitDetailRepository.Queryable()
+            //部门
+            var listOrg = await _orderVisitDetailRepository.Queryable()
                 .LeftJoin<OrderVisit>((it, o) => it.VisitId == o.Id)
                 .Where((it, o) => it.VisitTarget == EVisitTarget.Org && o.VisitTime >= StartTime && o.VisitTime <= EndTime && o.VisitState == EVisitState.Visited)
                  .Select((it, o) => new Satisfaction
@@ -3277,15 +3377,15 @@ namespace Hotline.Api.Controllers.Bi
               }).FirstAsync();
 
             var waitVisitd = await _orderVisitRepository.Queryable()
-			  .LeftJoin<Order>((x, o) => x.OrderId == o.Id)
-			  .WhereIF(IdentityType.HasValue, (x, o) => o.IdentityType == IdentityType)
-			  .Where((x, o) => x.CreationTime >= StartTime && x.CreationTime <= EndTime).Select((x, o) => 
+              .LeftJoin<Order>((x, o) => x.OrderId == o.Id)
+              .WhereIF(IdentityType.HasValue, (x, o) => o.IdentityType == IdentityType)
+              .Where((x, o) => x.CreationTime >= StartTime && x.CreationTime <= EndTime).Select((x, o) =>
                     SqlFunc.AggregateSum(SqlFunc.IIF(x.VisitState != EVisitState.None && x.VisitState != EVisitState.Visited, 1, 0)) //待回访
                ).FirstAsync();
             centerReportVisitd.WaitVisitd = waitVisitd;
 
-			//部门
-			var listOrg = await _orderVisitDetailRepository.Queryable()
+            //部门
+            var listOrg = await _orderVisitDetailRepository.Queryable()
                 .LeftJoin<OrderVisit>((x, ov) => x.VisitId == ov.Id)
                 .LeftJoin<Order>((x, ov, o) => ov.OrderId == o.Id)
                 .WhereIF(IdentityType.HasValue, (x, ov, o) => o.IdentityType == IdentityType)
@@ -4941,8 +5041,8 @@ namespace Hotline.Api.Controllers.Bi
 
             var stream = ExcelHelper.CreateStream(dtos);
 
-			return ExcelStreamResult(stream, "派单量明细数据");
-		}
+            return ExcelStreamResult(stream, "派单量明细数据");
+        }
 
         /// <summary>
         /// 二次办理统计

+ 11 - 0
src/Hotline.Api/Controllers/Exam/ExamManageController.cs

@@ -101,5 +101,16 @@ namespace Hotline.Api.Controllers.Exam
         {
             await _examManageService.UpdateStatus(statusActionRequests, HttpContext.RequestAborted);
         }
+
+        /// <summary>
+        /// 更新考试状态
+        /// </summary>
+        /// <param name="entityQueryRequest"></param>
+        /// <returns></returns>
+        [HttpPost(ExamManageApiRoute.UpdateExamStatus)]
+        public async Task UpdateExamStatus([FromBody] EntityQueryRequest entityQueryRequest)
+        {
+            await _examManageService.UpdateExamStatus(entityQueryRequest, HttpContext.RequestAborted);
+        }
     }
 }

+ 3 - 0
src/Hotline.Api/Controllers/OrderController.cs

@@ -1769,6 +1769,7 @@ public class OrderController : BaseController
     /// <param name="dto"></param>
     /// <returns></returns>
     [HttpPost("visit/judge")]
+    [LogFilterAlpha("扭转满意度")]
     public async Task<JudgeVisitRsp> JudgeVisit([FromBody] JudgeVisitReq dto)
     {
         int error = 0;
@@ -1792,6 +1793,7 @@ public class OrderController : BaseController
     /// <param name="dto"></param>
     /// <returns></returns>
     [HttpPost("visit/migration")]
+    [LogFilterAlpha("回访平移")]
     public async Task VisitMigrationBatch([FromBody] DistributionVisitDto dto)
     {
         var visits = await _orderVisitRepository.Queryable()
@@ -1838,6 +1840,7 @@ public class OrderController : BaseController
     /// <param name="dto"></param>
     /// <returns></returns>
     [HttpPut("visit/put_through")]
+    [LogFilterAlpha("设置未接通")]
     public async Task VisitPutThrough([FromBody] VisitPutThroughDto dto)
     {
         await _orderVisitRepository.Updateable().SetColumns(x => new OrderVisit { IsPutThrough = false }).Where(x => x.Id == dto.id)

+ 2 - 1
src/Hotline.Api/Controllers/WebPortalController.cs

@@ -1991,9 +1991,10 @@ namespace Hotline.Api.Controllers
                 }
 
                 // 工单流转到部门才允许显示办理内容
-                if (data.ActualHandleOrgCode == OrgSeedData.CenterId)
+                if (data.ActualHandleOrgCode == OrgSeedData.CenterId && data.Status < EOrderStatus.Published)
                 {
                     orderDetail.FlowContent = "信件正在办理中...";
+                    orderDetail.FlowResult = "信件正在办理中...";
                 }
 
                 //能否进行评价

+ 2 - 0
src/Hotline.Application/Exam/Constants/ApiRoutes/ExamManageApiRoute.cs

@@ -5,5 +5,7 @@
         public const string ImportExcel = "ImportExcel";
 
         public const string Download = "Download";
+
+        public const string UpdateExamStatus = "UpdateExamStatus";
     }
 }

+ 10 - 2
src/Hotline.Application/Exam/Interface/ExamManages/IExamManageService.cs

@@ -1,4 +1,5 @@
-using Exam.Share;
+using Exam.Infrastructure.Data.Entity;
+using Exam.Share;
 using Exam.Share.ViewResponses.Exam;
 using Hotline.Exams.ExamManages;
 using Hotline.Repository.SqlSugar.Exam.Interface;
@@ -16,6 +17,13 @@ namespace Hotline.Application.Exam.Interface.ExamManages
         /// <param name="cancellationToken"></param>
         /// <returns></returns>
         public Task GenerateTestPaper(GenerateExamTestPaperRequest generateExamTestPaperRequest, CancellationToken cancellationToken);
-
+        
+        /// <summary>
+        /// 更新考试状态
+        /// </summary>
+        /// <param name="entityQueryRequest"></param>
+        /// <param name="requestAborted"></param>
+        /// <returns></returns>
+        public Task UpdateExamStatus(EntityQueryRequest entityQueryRequest, CancellationToken requestAborted);
     }
 }

+ 5 - 1
src/Hotline.Application/Exam/Interface/Strategy/IExamStrategy.cs

@@ -16,6 +16,10 @@ namespace Hotline.Application.Exam.Interface.Strategy
 
         public string ErroMessage { get; }
 
-        public Action CallBack { get; set; }
+        public Func<object> CallBack { get; set; }
+
+        public object GetResult();
+
+
     }
 }

+ 3 - 1
src/Hotline.Application/Exam/QueryExtensions/ExamManages/UserExamQueryExtensions.cs

@@ -103,7 +103,9 @@ namespace Hotline.Application.Exam.QueryExtensions.ExamManages
 
             expression = ExpressionableUtility.CreateExpression<ExamUserExam>()
             .AndIF(userExamResultReportPagedRequest.MinScore.IsNotNull(), x => x.Score >= userExamResultReportPagedRequest.MinScore)
-            .AndIF(userExamResultReportPagedRequest.MaxScore.IsNotNull(), x => x.Score <= userExamResultReportPagedRequest.MaxScore)
+            .AndIF(userExamResultReportPagedRequest.MaxScore.IsNotNull() && userExamResultReportPagedRequest.MinScore.IsNull(), x => x.Score <= userExamResultReportPagedRequest.MaxScore || x.Score== null)
+            .AndIF(userExamResultReportPagedRequest.MaxScore.IsNotNull() && userExamResultReportPagedRequest.MinScore.IsNotNull(), x => x.Score <= userExamResultReportPagedRequest.MaxScore)
+            .And(x=>x.ExamStatus == Share.Enums.Exams.EExamStatus.Complete)
             .ToExpression();
 
             return expression;

+ 1 - 0
src/Hotline.Application/Exam/QueryExtensions/Practices/PracticeQueryExtensions.cs

@@ -30,6 +30,7 @@ namespace Hotline.Application.Exam.QueryExtensions.Practices
 
             expression = ExpressionableUtility.CreateExpression<ExamPracticeQuestion>()
                 .AndIF(practiceQuestionGroupRequest.PracticeId.IsNotNull(),x=>x.PracticeId == practiceQuestionGroupRequest.PracticeId)
+                .And(x=>x.QuestionType == EQuestionType.Single || x.QuestionType == EQuestionType.Multi || x.QuestionType == EQuestionType.Judge)
                 .ToExpression();
 
             return expression;

+ 1 - 0
src/Hotline.Application/Exam/QueryExtensions/Questions/QuestionQueryExtesions.cs

@@ -20,6 +20,7 @@ namespace Hotline.Application.Exam.QueryExtensions.Questions
 
             expression = ExpressionableUtility.CreateExpression<ExamQuestion>().AndIF(questionPagedRequest.DifficultyLevel.IsNotNull(), x => questionPagedRequest.DifficultyLevel == x.DifficultyLevel)
             .AndIF(questionPagedRequest.Title.IsNotNullOrEmpty(), x => x.Title.Contains(questionPagedRequest.Title))
+           .AndIF(questionPagedRequest.QuestionType.IsNotNull(),x=>x.QuestionType == questionPagedRequest.QuestionType)
             .ToExpression();
             return expression;
         }

+ 71 - 28
src/Hotline.Application/Exam/Service/ExamManages/ExamManageService.cs

@@ -328,36 +328,11 @@ namespace Hotline.Application.Exam.Service.ExamManages
             }
         }
 
-        private async Task<List<ExamQuestion>> GetQuestions(ExamManage examManage)
-        {
-            var questionRepository = new ExamRepository<ExamQuestion>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
-
-            var questionTable = questionRepository.Queryable();
-            var testPaperItemTable = _testPaperItemRepository.Queryable().Where(x => x.TestPaperId == examManage.TestPaperId);
-
-            var questions = questionTable.InnerJoin(testPaperItemTable, (q, t) => q.Id == t.QuestionId)
-                .Select((q, t) => q);
-
-            return await questions.ToListAsync();
-        }
-
-        private async Task<List<ExamTagQuestion>> GetTagQuestions(ExamManage examManage)
+        public async Task UpdateExamStatus(EntityQueryRequest entityQueryRequest, CancellationToken requestAborted)
         {
-            var extractRuleRepository = new ExamRepository<ExamExtractRule>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
-            var ruleTagRepository = new ExamRepository<ExamRuleTag>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
-            var tagQuestionRepository = new ExamRepository<ExamTagQuestion>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
-
-          
-            var extractRuleTable = extractRuleRepository.Queryable().Where(x=>x.Id == examManage.ExtractRuleId && x.RuleType == examManage.ExamType);
-            var ruleTagTable = ruleTagRepository.Queryable();
-            var tagQuestionTable = tagQuestionRepository.Queryable();
-
-            var tagQuestions = await tagQuestionTable
-                .InnerJoin(ruleTagTable, (q, rt) => q.TagId == rt.TagId)
-                .InnerJoin(extractRuleTable, (q, rt, x) => rt.RuleId == x.Id)
-                .Select((q, rt, x) => q).ToListAsync();
+            List<ExamManage> examManages = await UpdateExamManageStatus(entityQueryRequest, requestAborted);
 
-            return tagQuestions;
+            //await UdpateUserExam(examManages, requestAborted);
 
         }
         #endregion
@@ -679,6 +654,74 @@ namespace Hotline.Application.Exam.Service.ExamManages
             }
 
         }
+
+        private async Task<List<ExamQuestion>> GetQuestions(ExamManage examManage)
+        {
+            var questionRepository = new ExamRepository<ExamQuestion>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+
+            var questionTable = questionRepository.Queryable();
+            var testPaperItemTable = _testPaperItemRepository.Queryable().Where(x => x.TestPaperId == examManage.TestPaperId);
+
+            var questions = questionTable.InnerJoin(testPaperItemTable, (q, t) => q.Id == t.QuestionId)
+                .Select((q, t) => q);
+
+            return await questions.ToListAsync();
+        }
+
+        private async Task<List<ExamTagQuestion>> GetTagQuestions(ExamManage examManage)
+        {
+            var extractRuleRepository = new ExamRepository<ExamExtractRule>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var ruleTagRepository = new ExamRepository<ExamRuleTag>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var tagQuestionRepository = new ExamRepository<ExamTagQuestion>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+
+
+            var extractRuleTable = extractRuleRepository.Queryable().Where(x => x.Id == examManage.ExtractRuleId && x.RuleType == examManage.ExamType);
+            var ruleTagTable = ruleTagRepository.Queryable();
+            var tagQuestionTable = tagQuestionRepository.Queryable();
+
+            var tagQuestions = await tagQuestionTable
+                .InnerJoin(ruleTagTable, (q, rt) => q.TagId == rt.TagId)
+                .InnerJoin(extractRuleTable, (q, rt, x) => rt.RuleId == x.Id)
+                .Select((q, rt, x) => q).ToListAsync();
+
+            return tagQuestions;
+
+        }
+
+
+        private async Task UdpateUserExam(List<ExamManage> examManages, CancellationToken requestAborted)
+        {
+            var examIds = examManages.Select(x => x.Id).ToList();
+            var userExams = await _userExamRepository.Queryable().Where(x => examIds.Contains(x.ExamId)).ToListAsync();
+
+            userExams.ForEach(item =>
+            {
+                item.ExamStatus = Share.Enums.Exams.EExamStatus.Complete;
+            });
+
+            userExams.ToUpdate(_sessionContext);
+
+            await _userExamRepository.UpdateWithValidateAsync(userExams, requestAborted);
+        }
+
+        private async Task<List<ExamManage>> UpdateExamManageStatus(EntityQueryRequest entityQueryRequest, CancellationToken requestAborted)
+        {
+            var queryable = _repository.Queryable()
+                .WhereIF(entityQueryRequest != null && entityQueryRequest.Ids != null, x => entityQueryRequest.Ids.Contains(x.Id))
+                .Where(x => x.EndTime < DateTime.Now);
+            var examManages = await queryable.ToListAsync();
+
+            examManages.ForEach(item =>
+            {
+                item.ExamStatus = Share.Enums.Exams.EExamStatus.Complete;
+            });
+
+            examManages.ToUpdate(_sessionContext);
+
+            await _repository.UpdateWithValidateAsync(examManages, requestAborted);
+
+            return examManages;
+        }
         #endregion
     }
 }

+ 1 - 1
src/Hotline.Application/Exam/Service/ExamManages/ExtractRuleService.cs

@@ -83,7 +83,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
                 extractRuleDto.TagQuestionDtos.ForEach(item =>
                 {
-                    var tagQuestionCount = tagQuestionCounts.FirstOrDefault(m => m.TagId == item.TagId);
+                    var tagQuestionCount = tagQuestionCounts.FirstOrDefault(m => m.TagId == item.TagId && m.QuestionType == item.QuestionType);
 
                     item.TotalCount = tagQuestionCount?.TotalCount ?? 0;
                 });

+ 175 - 30
src/Hotline.Application/Exam/Service/ExamManages/UserExamService.cs

@@ -41,6 +41,7 @@ using System.Threading;
 using DocumentFormat.OpenXml.Office2013.Excel;
 using Hotline.Share.Enums.Exams;
 using DocumentFormat.OpenXml.Wordprocessing;
+using Hotline.Repository.SqlSugar.Exam.Repositories.ExamManages;
 
 namespace Hotline.Application.Exam.Service.ExamManages
 {
@@ -141,8 +142,8 @@ namespace Hotline.Application.Exam.Service.ExamManages
             var examAnswerRepository = new ExamRepository<ExamAnswer>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
             var userExamItemRepository = new ExamRepository<ExamUserExamItem>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
 
-            var userExamItemTable = userExamItemRepository.Queryable().Where(i=> i.QuestionId == examQuestionRequest.QuestionId);
-            var userExamTable = _repository.Queryable().Where(u=> u.UserId == _sessionContext.UserId);
+            var userExamItemTable = userExamItemRepository.Queryable().Where(i => i.QuestionId == examQuestionRequest.QuestionId);
+            var userExamTable = _repository.Queryable().Where(u => u.UserId == _sessionContext.UserId);
             var examAnswerTable = examAnswerRepository.Queryable();
 
             var examAnswers = await examAnswerTable.InnerJoin(userExamItemTable, (e, i) => e.UserExamItemId == i.Id)
@@ -182,9 +183,9 @@ namespace Hotline.Application.Exam.Service.ExamManages
             var result = queryable.GroupBy(x => x.QuestionType).Select(m => new ExamQuestionViewResponse
             {
                 QuestionType = m.Key,
-                Questions = m.Select(n => new SimpleViewResponse
+                Questions = m.GroupBy(g => g.QuestionId).Select(n => new SimpleViewResponse
                 {
-                    Id = n.QuestionId
+                    Id = n.Key
                 }).ToList()
             }).ToList();
 
@@ -287,7 +288,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             if (userExams.All(x => x.IsSubmit))
             {
-                var examManage = await _examManageRepository.GetAsync(x=>x.Id == examId);
+                var examManage = await _examManageRepository.GetAsync(x => x.Id == examId);
 
                 examManage.ExamStatus = EExamStatus.Complete;
 
@@ -303,6 +304,8 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             if (!startExamViewResponse.CheckValidate())
             {
+                await SetExamStatus(userExam, startExamViewResponse, cancellationToken);
+
                 return startExamViewResponse;
             }
 
@@ -321,6 +324,16 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             return startExamViewResponse;
         }
+
+        private async Task SetExamStatus(ExamUserExam userExam, StartExamViewResponse startExamViewResponse, CancellationToken cancellationToken)
+        {
+            userExam.ExamStatus = startExamViewResponse.IsCompleted ? EExamStatus.Complete : EExamStatus.NoStart;
+
+            userExam.ToUpdate(_sessionContext);
+
+            await _repository.UpdateWithValidateAsync(userExam, cancellationToken);
+        }
+
         public async Task<StartExamViewResponse> StartUserExamAsync(StartUserExamDto startUserExamDto, CancellationToken cancellationToken)
         {
             var userExam = await _repository.GetAsync(x => x.Id == startUserExamDto.Id);
@@ -337,6 +350,8 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             if (!startExamViewResponse.CheckValidate())
             {
+                await SetExamStatus(userExam, startExamViewResponse, cancellationToken);
+
                 return startExamViewResponse;
             }
 
@@ -362,7 +377,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
         private async Task<StartExamViewResponse> CheckExamValid(ExamUserExam examUserExam, CancellationToken cancellationToken)
         {
 
-            var examManage = await _examManageRepository.Queryable().Where(x => x.Id == examUserExam.ExamId).FirstAsync();            
+            var examManage = await _examManageRepository.Queryable().Where(x => x.Id == examUserExam.ExamId).FirstAsync();
 
             var startExamViewResponse = new StartExamViewResponse
             {
@@ -381,15 +396,18 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 startExamViewResponse = AddCheckValidateTimeStrategy(examUserExam, examManage, startExamViewResponse, examStrategys);
                 startExamViewResponse = AddCheckValidateCountStrategy(examUserExam, examManage, startExamViewResponse, examStrategys);
 
+                examStrategyProxy.Initial(examStrategys);
+
                 if (!examStrategyProxy.Validate())
                 {
+                    startExamViewResponse = (StartExamViewResponse)examStrategyProxy.GetResult();
                     return startExamViewResponse;
                 }
             }
 
             startExamViewResponse.IsStart = true;
 
-            if (examManage.ExamStatus== EExamStatus.NoStart)
+            if (examManage.ExamStatus == EExamStatus.NoStart)
             {
                 await UpdateExamStatus(examManage, cancellationToken);
             }
@@ -416,12 +434,16 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 {
                     CallBack = () =>
                     {
-                        startExamViewResponse = new StartExamViewResponse
+                        var response = new StartExamViewResponse
                         {
                             IsJoin = false,
                             IsStart = false,
-                            IsCompleted = true
+                            IsCompleted = true,
+                            StartTime = examUserExam.StartTime,
+                            TimeSpan = examManage.TimeSpan
                         };
+
+                        return response;
                     }
                 };
                 examStrategys.Add(checkStartTime);
@@ -435,12 +457,16 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 {
                     CallBack = () =>
                     {
-                        startExamViewResponse = new StartExamViewResponse
+                        var response = new StartExamViewResponse
                         {
                             IsJoin = false,
                             IsStart = false,
-                            IsCompleted = true
+                            IsCompleted = true,
+                            StartTime = examUserExam.StartTime,
+                            TimeSpan = examManage.TimeSpan
                         };
+
+                        return response;
                     }
                 };
                 examStrategys.Add(checkStartTime);
@@ -457,12 +483,16 @@ namespace Hotline.Application.Exam.Service.ExamManages
             {
                 CallBack = () =>
                 {
-                    startExamViewResponse = new StartExamViewResponse
+                    var response = new StartExamViewResponse
                     {
                         IsJoin = false,
                         IsStart = false,
-                        IsCompleted = true
+                        IsCompleted = true,
+                        StartTime = examUserExam.StartTime,
+                        TimeSpan = examManage.TimeSpan
                     };
+
+                    return response;
                 }
             };
             examStrategys.Add(checkStartTime);
@@ -477,12 +507,16 @@ namespace Hotline.Application.Exam.Service.ExamManages
             {
                 CallBack = () =>
                 {
-                    startExamViewResponse = new StartExamViewResponse
+                    var response = new StartExamViewResponse
                     {
                         IsJoin = false,
                         IsStart = false,
-                        IsCompleted = true
+                        IsCompleted = true,
+                        StartTime = examUserExam.StartTime,
+                        TimeSpan = examManage.TimeSpan
                     };
+
+                    return response;
                 }
             };
             examStrategys.Add(checkStartTime);
@@ -497,12 +531,16 @@ namespace Hotline.Application.Exam.Service.ExamManages
             {
                 CallBack = () =>
                 {
-                    startExamViewResponse = new StartExamViewResponse
+                    var response = new StartExamViewResponse
                     {
                         IsJoin = false,
                         IsStart = false,
-                        IsCompleted = false
+                        IsCompleted = false,
+                        StartTime = examUserExam.StartTime,
+                        TimeSpan = examManage.TimeSpan
                     };
+
+                    return response;
                 }
             };
             examStrategys.Add(checkStartTime);
@@ -570,7 +608,8 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 Label = qo.Label,
                 IsAnswer = qo.IsAnswer,
                 IsSelected = o.Id != null,
-                Score = s.Score
+                QuestionScore = s.Score,
+                Score = i.Score
             }
                 );
 
@@ -585,6 +624,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 Answer = g.FirstOrDefault().Answer,
                 QuestionType = g.Key.QuestionType,
                 Id = g.Key.Id,
+                QuestionScore = g.FirstOrDefault().QuestionScore,
                 Score = g.FirstOrDefault().Score,
                 Title = g.FirstOrDefault().Title,
                 CorrectAnswer = g.Key.QuestionType.CheckSelectType() ? string.Join(",", g.Where(i => i.IsAnswer).Select(n => n.Label).Distinct()) : g.FirstOrDefault()?.CorrectAnswer
@@ -604,7 +644,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
         public async Task<UnExamUserPageViewResponse> GetUnExamUsers(UnExamUserReportPagedRequest unExamUserReportPagedRequest)
         {
-            unExamUserReportPagedRequest.ResoleEndTime();
+            unExamUserReportPagedRequest.ResolveEndTime();
 
             var examManageRepository = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
             var userRepository = new ExamRepository<User>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
@@ -637,7 +677,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
         public async Task<UserExamResultPageViewResponse> GetUserExamResults(UserExamResultReportPagedRequest userExamResultReportPagedRequest)
         {
-            userExamResultReportPagedRequest.ResoleEndTime();
+            userExamResultReportPagedRequest.ResolveEndTime();
 
             var examManageRepository = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
             var userRepository = new ExamRepository<User>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
@@ -659,7 +699,9 @@ namespace Hotline.Application.Exam.Service.ExamManages
                     CutoffScore = e.CutoffScore,
                     Score = ue.Score ?? 0,
 
-                });
+                })
+                .MergeTable()
+                .OrderByDescending(x => x.Score);
 
             var total = await queryResult.CountAsync();
             var items = await queryResult.ToPageListAsync(userExamResultReportPagedRequest.PageIndex, userExamResultReportPagedRequest.PageSize);
@@ -676,7 +718,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
         public async Task<GradingResultPageViewResponse> GetGradingResultPagedList(GradingPagedRequest gradingPagedRequest)
         {
             // 只要有阅卷记录就在已阅卷列表中,已阅卷和未阅卷会有重复数据,只有所有记录都已阅卷才会从未阅卷列表中排除
-            var userExamTable =  _repository.Queryable().WhereIF(gradingPagedRequest.IsCheck!=null, x => x.IsCheck == gradingPagedRequest.IsCheck && x.IsSubmit);
+            var userExamTable = _repository.Queryable().WhereIF(gradingPagedRequest.IsCheck != null, x => x.IsCheck == gradingPagedRequest.IsCheck && x.IsSubmit);
 
             var examManageTable = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
 
@@ -695,7 +737,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 TotalScore = e.TotalScore,
                 ExamName = e.Name,
                 ExamCode = e.Code,
-                IsCheck = SqlFunc.Subqueryable<ExamUserExam>().Where(x=>x.ExamId == e.Id && x.IsCheck).Any(),
+                IsCheck = SqlFunc.Subqueryable<ExamUserExam>().Where(x => x.ExamId == e.Id && x.IsCheck).Any(),
                 Id = e.Id,
                 Remark = e.Remark
             });
@@ -720,16 +762,45 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             if (userExamItems != null && userExamItemIds.Any())
             {
-                userExamItems = _mapper.Map<List<GradingExamItemDto>, List<ExamUserExamItem>>(batchGradingExamItemDto.Items, userExamItems);
+                var updateUserExamItems = new List<ExamUserExamItem>();
+                userExamItems.ForEach(x =>
+                {
+                    var gradingExamItemDto = batchGradingExamItemDto.Items.Find(m => m.UserExamItemId == x.Id);
+
+                    var updateUserExamItem = _mapper.Map<GradingExamItemDto, ExamUserExamItem>(gradingExamItemDto, x);
+
+                    updateUserExamItem.IsCheck = true;
+
+                    updateUserExamItems.Add(updateUserExamItem);
+                });
+
+                updateUserExamItems.ToUpdate(_sessionContext);
+
+                await _userExamItemRepository.UpdateWithValidateAsync(updateUserExamItems, cancellationToken);
+
+                var userExamId = userExamItems.FirstOrDefault()?.UserExamId;
+                // 计算本次考试得分
+                var userExamItemsInCheck = await _userExamItemRepository.Queryable()
+                    .InnerJoin<ExamQuestionBak>((u, q) => u.QuestionId == q.QuestionId)
+                    .Where((u, q) => u.UserExamId == userExamId && (q.QuestionType == EQuestionType.Single || q.QuestionType == EQuestionType.Multi || q.QuestionType == EQuestionType.Judge)).Select((u, q) => new UpdateUserExamItemDto
+                    {
+                        QuestionId = u.QuestionId,
+                        QuestionType = q.QuestionType,
+                        UserExamId = u.UserExamId,
+                    }).Distinct().ToListAsync();
+                await CalcuteExamItemScore(_userExamItemRepository, userExamItemsInCheck, cancellationToken);
+
+                await CalcuteTotalScore(_userExamItemRepository, userExamId, cancellationToken);
 
-                await _userExamItemRepository.UpdateWithValidateAsync(userExamItems, cancellationToken);
             }
+
+
         }
 
         public async Task<List<ExamUserViewResponse>> GetUserListAsync(ExamUserQueryRequest examUserQueryRequest)
         {
-            var userExamTable =  _repository.Queryable().Where(x => x.ExamId == examUserQueryRequest.ExamId);
-            var queryable = await userExamTable.InnerJoin<User>((ux, u) => ux.UserId == u.Id).Select((ux,u) => new ExamUserViewResponse
+            var userExamTable = _repository.Queryable().Where(x => x.ExamId == examUserQueryRequest.ExamId);
+            var queryable = await userExamTable.InnerJoin<User>((ux, u) => ux.UserId == u.Id).Select((ux, u) => new ExamUserViewResponse
             {
                 ExamId = ux.ExamId,
                 UserId = ux.UserId,
@@ -774,6 +845,80 @@ namespace Hotline.Application.Exam.Service.ExamManages
             await userExamRepository.UpdateWithValidateAsync(userExamItem, cancellationToken);
         }
 
+        private async Task CalcuteExamItemScore(IUserExamItemRepository userExamItemRepository, List<UpdateUserExamItemDto> addUserExamItemDtos, CancellationToken cancellationToken)
+        {
+            var questionIds = addUserExamItemDtos.Select(x => x.QuestionId).ToList();
+            var userExamIds = addUserExamItemDtos.Select(x => x.UserExamId).ToList();
+            var questionTypes = addUserExamItemDtos.Select(x => x.QuestionType).ToList();
+
+            var testPaperItemOptionsRepository = new ExamRepository<ExamQuestionOptionsBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var userExamItemOptionRepository = new ExamRepository<ExamUserExamItemOptions>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var examManageRepository = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var testPaperItemRepository = new ExamRepository<Exams.ExamManages.ExamQuestionBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var examQuestionScoreRepository = new ExamRepository<ExamQuestionScoreBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var testPaperOptionsTable = testPaperItemOptionsRepository.Queryable().Where(x => questionIds.Contains(x.QuestionId) && x.IsAnswer);
+            var testPaperItemTable = testPaperItemRepository.Queryable().Where(x => x.QuestionType == EQuestionType.Single || x.QuestionType == EQuestionType.Multi || x.QuestionType == EQuestionType.Judge);
+            var userExamTable = _repository.Queryable().Where(x => userExamIds.Contains(x.Id));
+            var examManageTable = examManageRepository.Queryable();
+            var testPaperOptionIds = await testPaperOptionsTable.InnerJoin(testPaperItemTable, (t, i) => t.ExamQuestionId == i.Id)
+                .InnerJoin(examManageTable, (t, i, e) => i.ExamId == e.Id)
+                .InnerJoin(userExamTable, (t, i, e, u) => e.Id == u.ExamId)
+                .Select((t, i, e, u) => t.Id).ToListAsync();
+
+            var userExamItems = await userExamItemRepository.Queryable().Where(x => userExamIds.Contains(x.UserExamId)).ToListAsync();
+            var userExamItemIds = userExamItems.Select(x => x.Id).ToList();
+            var userExamItemOptions = await userExamItemOptionRepository.Queryable().Where(x => userExamItemIds.Contains(x.UserExamItemId)).ToListAsync();
+            var examQuesiontScores = await examQuestionScoreRepository.Queryable().Where(x => questionTypes.Contains(x.QuestionType))
+                .InnerJoin(userExamTable, (e, u) => e.Id == u.ExamId)
+                .Select((e, u) => e).ToListAsync();
+
+
+            foreach (var addUserExamItemDto in addUserExamItemDtos)
+            {
+                var isCorrect = userExamItemOptions.Select(x => x.QuestionOptionId).OrderBy(x => x).SequenceEqual(testPaperOptionIds.OrderBy(x => x));
+                var userExamItem = userExamItems.FirstOrDefault(x => x.QuestionId == addUserExamItemDto.QuestionId);
+                if (userExamItem != null)
+                {
+                    userExamItem.IsCheck = true;
+                    userExamItem.Score = isCorrect ? examQuesiontScores.FirstOrDefault(x => x.QuestionType == addUserExamItemDto.QuestionType)?.Score : 0;
+                    userExamItem.ToUpdate(_sessionContext);
+                }
+            }
+
+
+            await userExamItemRepository.UpdateWithValidateAsync(userExamItems, cancellationToken);
+        }
+
+        private async Task CalcuteTotalScore(IUserExamItemRepository userExamItemRepository, string userExamId, CancellationToken cancellationToken)
+        {
+            var userExam = await _repository.GetAsync(x => x.Id == userExamId);
+
+            if (userExam != null)
+            {
+                var userExamItems = await userExamItemRepository.Queryable().Where(x => x.UserExamId == userExamId).ToListAsync();
+                var examManageRepository = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+                var examManage = await examManageRepository.GetAsync(x => x.Id == userExam.ExamId);
+
+                var totalScore = userExamItems.Sum(x => x.Score);
+
+                userExam.Score = totalScore;
+
+                userExam.IsCheck = true;
+
+                userExam.ExamStatus = EExamStatus.Complete;
+
+
+                if (examManage != null)
+                {
+                    userExam.IsSuccess = userExam.Score > examManage.CutoffScore;
+                }
+
+                userExam.ToUpdate(_sessionContext);
+
+                await _repository.UpdateWithValidateAsync(userExam, cancellationToken);
+            }
+        }
+
         private async Task AddExamAsync(IRepository<ExamUserExamItem> userExamItemRepository, AddUserExamItemDto addUserExamItemDto, CancellationToken cancellationToken)
         {
             var userExamItem = await AddUserExamItem(addUserExamItemDto, cancellationToken);
@@ -781,7 +926,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             userExamItem.UserExamItemOptionses = await AddUserExamItemOptions(addUserExamItemDto, cancellationToken);
 
             userExamItem.ExamAnswers = await AddExamAnswer(addUserExamItemDto, userExamItem.Id, cancellationToken);
-            
+
 
             await userExamItemRepository.AddNav(userExamItem)
                 .Include(x => x.UserExamItemOptionses)
@@ -836,7 +981,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             return null;
         }
 
-        private async Task<List<ExamAnswer>> AddExamAnswer(AddUserExamItemDto addUserExamItemDto,string id, CancellationToken cancellationToken)
+        private async Task<List<ExamAnswer>> AddExamAnswer(AddUserExamItemDto addUserExamItemDto, string id, CancellationToken cancellationToken)
         {
             if (addUserExamItemDto.QuestionType.CheckSelectType()) return null;
 
@@ -952,7 +1097,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
                     await _examAnswerRepository.ValidateAddAsync(examAnswer, cancellationToken);
 
                     examAnswers.Add(examAnswer);
-                }                
+                }
 
                 return examAnswers;
             }

+ 20 - 16
src/Hotline.Application/Exam/Service/Practices/PracticeService.cs

@@ -38,6 +38,8 @@ using XF.Domain.Authentications;
 using XF.Domain.Dependency;
 using Hotline.Repository.SqlSugar.Exam.Service;
 using Hotline.Repository.SqlSugar.Exam.Extensions;
+using Exam.Infrastructure.Extensions;
+using Consul;
 
 namespace Hotline.Application.Exam.Service.Practices
 {
@@ -321,6 +323,8 @@ namespace Hotline.Application.Exam.Service.Practices
         }
         private async Task<PracticeQuestionDto> QueryPracticeQuestion(PracticeQuestionRequest practiceQuestionRequest)
         {
+            if (practiceQuestionRequest.PracticeQuestionId.IsNullOrEmpty()) return null;
+
             var practiceQuestion = await _practiceQuestionRepository.GetAsync(x => x.Id == practiceQuestionRequest.PracticeQuestionId);
 
             var practiceQuestionDto = _mapper.Map<PracticeQuestionDto>(practiceQuestion);
@@ -590,44 +594,44 @@ namespace Hotline.Application.Exam.Service.Practices
             var tagIds = actionRequest.PracticeTagDtos.Select(x => x.TagId).ToList();
 
             var questionTagRepostiory = new ExamRepository<ExamQuestionTag>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+
             var questionRepository = new ExamRepository<ExamQuestion>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
 
-            var questionTagTable = questionTagRepostiory.Queryable();
-            var questionTable = questionRepository.Queryable();
+            ISugarQueryable<ExamQuestionTag, ExamQuestion> questionTable = GetCheckQuestions(questionTagRepostiory);
 
             // 按照标签获取试题,至少取一道
             if (actionRequest.Count < actionRequest.PracticeTagDtos.Count)
             {
-                questionTagTable = questionTagTable.Where(x => tagIds.Contains(x.TagId)).Take(actionRequest.Count);
+                var queryResult = questionTable.Where((qt, q) => tagIds.Contains(qt.TagId)).Take(actionRequest.Count).Select((qt, q) => q);
+
+                return await queryResult.ToListAsync();
             }
             else
             {
-                var unionQuestions = new List<ISugarQueryable<ExamQuestionTag>>();
+                var unionQuestions = new List<ISugarQueryable<ExamQuestion>>();
                 var ids = new List<string>();
                 // 保证每个标签至少获取一道题
                 tagIds.ForEach(v =>
                 {
-                    var unionQuestion = questionTagTable;
-                    unionQuestion= unionQuestion.Where(x => x.TagId == v).Take(1);
-                    ids.Add(questionTagRepostiory.Queryable().Where(x => x.TagId == v).Select(x => x.Id).First());
+                    var unionQuestion = GetCheckQuestions(questionTagRepostiory).Where((qt, q) => qt.TagId == v).Take(1).Select((qt, q) => q);
+                    ids.Add(GetCheckQuestions(questionTagRepostiory).Where((qt, q) => qt.TagId == v).Select(qt => qt.Id).First());
                     unionQuestions.Add(unionQuestion);
-                    questionTagTable = questionTagRepostiory.Queryable();
                 });
 
-                var mainQuesiton = questionTagTable;
-                mainQuesiton = mainQuesiton.Where(x => tagIds.Contains(x.TagId) && !ids.Contains(x.Id)).Take(actionRequest.Count - tagIds.Count);
+                var mainQuesiton = GetCheckQuestions(questionTagRepostiory).Where((qt, q) => tagIds.Contains(qt.TagId) && !ids.Contains(qt.Id)).Take(actionRequest.Count - tagIds.Count).Select((qt, q) => q);
 
                 unionQuestions.Add(mainQuesiton);
 
-                questionTagTable = questionTagRepostiory.Queryable();
-                questionTagTable = questionTagRepostiory.UnionAll(unionQuestions.ToArray());
-            }
+                var queryResult = questionRepository.UnionAll(unionQuestions.ToArray());
 
-            var queryResult = questionTagTable.InnerJoin(questionTable, (t, q) => t.QuestionId == q.Id)
-                              .Select((t, q) => q);
+                return await queryResult.ToListAsync();
+            }
 
-            return await queryResult.ToListAsync();
+        }
 
+        private static ISugarQueryable<ExamQuestionTag, ExamQuestion> GetCheckQuestions(ExamRepository<ExamQuestionTag> questionTagRepostiory)
+        {
+            return questionTagRepostiory.Queryable().InnerJoin<ExamQuestion>((qt, q) => qt.QuestionId == q.Id).Where((qt, q) => q.QuestionType == Share.Enums.Exams.EQuestionType.Single || q.QuestionType == Share.Enums.Exams.EQuestionType.Multi || q.QuestionType == Share.Enums.Exams.EQuestionType.Judge);
         }
 
         private async Task<string> GenerateCode(string codePrefix, int length)

+ 2 - 2
src/Hotline.Application/Exam/Service/Questions/QuestionService.cs

@@ -627,7 +627,7 @@ namespace Hotline.Application.Exam.Service.Questions
 
             actionRequest.QuestionKnowladgeDtos.ResolveOperationStatus();
 
-            var questionKnowladgeDtos = actionRequest.QuestionKnowladgeDtos.Where(x => x.OperationStatus == EEOperationStatus.Add).ToList();
+            var questionKnowladgeDtos = actionRequest.QuestionKnowladgeDtos.Where(x => x.OperationStatus == EEOperationStatus.Update).ToList();
 
             var ids = questionKnowladgeDtos.Select(x => x.Id);
 
@@ -980,7 +980,7 @@ namespace Hotline.Application.Exam.Service.Questions
 
             var questionAnswerDto = _mapper.Map<QuestionAnswerDto>(questionAnswer);
 
-            return questionAnswerDto;
+            return questionAnswerDto ?? new QuestionAnswerDto();
         }
 
         private async Task<List<QuestionTagDto>> GetQuestionTags(EntityQueryRequest entityQueryRequest)

+ 5 - 5
src/Hotline.Application/Exam/Service/TestPapers/TestPaperService.cs

@@ -825,14 +825,14 @@ namespace Hotline.Application.Exam.Service.TestPapers
             var tagQuestionTable = tagQuestionRepository.Queryable();
 
 
-            var testPaperRules = await exatractTable.InnerJoin(ruleTagTable,(e,rt)=>e.Id == rt.RuleId)
-                .InnerJoin(tagQuestionTable,(e,rt,tq)=>rt.RuleId == tq.RuleId && rt.TagId == tq.TagId)
-                .GroupBy((e, rt, tq)=> tq.QuestionType)
-                .Select((e,rt,tq)=> new ExamTagQuestion
+            var testPaperRules = await exatractTable.InnerJoin(ruleTagTable, (e, rt) => e.Id == rt.RuleId)
+                .InnerJoin(tagQuestionTable, (e, rt, tq) => rt.RuleId == tq.RuleId && rt.TagId == tq.TagId)
+                .GroupBy((e, rt, tq) => tq.QuestionType)
+                .Select((e, rt, tq) => new ExamTagQuestion
                 {
                     QuestionType = tq.QuestionType,
                     Count = SqlFunc.AggregateSum(tq.Count)
-                })
+                }).MergeTable().Where(m => m.Count > 0)
                 .ToListAsync();
 
             if (testPaperRules != null)

+ 25 - 4
src/Hotline.Application/Exam/Service/Trains/TrainRecordService.cs

@@ -71,7 +71,7 @@ namespace Hotline.Application.Exam.Service.Trains
             trainRecord= _mapper.Map<CompleteTrainRecordDto,ExamTrainRecord>(completeTrainRecordDto, trainRecord);
             trainRecord.ToUpdate(_sessionContext);
 
-            await _repository.ValidateUpdateAsync(trainRecord, cancellationToken);
+            await _repository.UpdateWithValidateAsync(trainRecord, cancellationToken);
         }
 
         public async Task CompleteTrainKnowladgeAsync(CompleteTrainKnowladgeDto completeTrainPracticeDto, CancellationToken cancellationToken)
@@ -80,7 +80,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
             trainKnowladge.ToUpdate(_sessionContext);
 
-            await _trainKnowladgeRepository.ValidateUpdateAsync(trainKnowladge,cancellationToken);
+            await _trainKnowladgeRepository.UpdateWithValidateAsync(trainKnowladge,cancellationToken);
 
         }
 
@@ -166,9 +166,29 @@ namespace Hotline.Application.Exam.Service.Trains
 
             trainPracticeDto.TrainPracticeKnowladgeDtos = await GetTrainPracticeKnowladges(trainPracticeRequest);
 
+            trainPracticeDto.AddTrainRecordAnswerDto = await GetTrainRecordAnswer(trainPracticeRequest);
+
             return trainPracticeDto;
         }
 
+        private async Task<AddTrainRecordAnswerDto?> GetTrainRecordAnswer(TrainPracticeRequest trainPracticeRequest)
+        {
+            var trainRecordAnswerRepository = new ExamRepository<ExamTrainRecordAnswer>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
+            var queryable = trainRecordAnswerRepository.Queryable()
+               .InnerJoin<ExamTrainRecord>((t, x) => t.TrainRecordId == x.Id)
+               .InnerJoin<ExamTrainPlanTemplate>((t, x, p) => x.TrainPlanId == p.TrainPlanId)
+               .InnerJoin<ExamTrainPractice>((t, x, p, tp) => p.TrainTemplateId == tp.TrainTemplateId)
+               .Where((t, x, p, tp) => tp.Id == trainPracticeRequest.TrainPracticeId)
+               .Select((t, x, p, tp) => t);
+
+            var examTrainRecordAnswer = await queryable
+                .FirstAsync();
+
+            var addTrainRecordAnswerDto = _mapper.Map<ExamTrainRecordAnswer, AddTrainRecordAnswerDto>(examTrainRecordAnswer);
+
+            return addTrainRecordAnswerDto;
+        }
+
         public async Task<SimpleTrainPracticeDto> GetSimpleTrainPracticeAsync(TrainPracticeRequest trainPracticeRequest)
         {
             SqlSugar.ISugarQueryable<ExamTrainPractice> trainPractices = GetTrainPractices(trainPracticeRequest);
@@ -188,7 +208,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
         public async Task<TrainResultPagedViewResponse> AnalysisTrainResult(TrainResultReportPagedRequest trainResultReportPagedRequest)
         {
-            trainResultReportPagedRequest.ResoleEndTime();
+            trainResultReportPagedRequest.ResolveEndTime();
 
             var expression = trainResultReportPagedRequest.GetExpression();
             var templateExpression = trainResultReportPagedRequest.GetTemplateExpression();
@@ -230,7 +250,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
         public async Task<TrainResultRateViewResponse> CalcuteAnalysisRate(TrainResultReportPagedRequest trainResultReportPagedRequest)
         {
-            trainResultReportPagedRequest.ResoleEndTime();
+            trainResultReportPagedRequest.ResolveEndTime();
 
             var expression = trainResultReportPagedRequest.GetExpression();
             var templateExpression = trainResultReportPagedRequest.GetTemplateExpression();
@@ -381,6 +401,7 @@ namespace Hotline.Application.Exam.Service.Trains
             if (addTrainDto.AddTrainRecordAnswerDto != null)
             {
                 addTrainDto.AddTrainRecordAnswerDto.TrainRecordId = addTrainDto.TrainRecordId;
+                addTrainDto.AddTrainRecordAnswerDto.QuestionId = addTrainDto.QuestionId;
                 trainRecordAnswers.Add(_mapper.Map<ExamTrainRecordAnswer>(addTrainDto.AddTrainRecordAnswerDto));
             }
 

+ 17 - 3
src/Hotline.Application/Exam/Strategy/CheckEndTimeStrategy.cs

@@ -11,7 +11,7 @@ namespace Hotline.Application.Exam.Strategy
     {
         public string ErroMessage { get; private set; }
 
-        public Action CallBack { get; set; }
+        public Func<object> CallBack { get; set; }
 
         private IExamStrategy _next;
 
@@ -19,6 +19,9 @@ namespace Hotline.Application.Exam.Strategy
 
         private DateTime? _startTime;
 
+        private IExamStrategy _current;
+
+
         public CheckEndTimeStrategy(DateTime examEndTime, DateTime? startTime)
         {
             _examEndTime = examEndTime;
@@ -35,12 +38,23 @@ namespace Hotline.Application.Exam.Strategy
         {
             if (_examEndTime < _startTime)
             {
-                if (CallBack != null)
-                    CallBack();
                 ErroMessage = "考试结束";
+                _current = this;
                 return false;
             }
+            _current = _next;
+
+            if (_next == null) return true;
+
             return _next.Validate();
         }
+
+        public object GetResult()
+        {
+            if (_current != null && _current.CallBack != null)
+                return _current.CallBack();
+
+            return null;
+        }
     }
 }

+ 17 - 3
src/Hotline.Application/Exam/Strategy/CheckStartTimeStrategy.cs

@@ -6,7 +6,7 @@ namespace Hotline.Application.Exam.Strategy
     {
         public string ErroMessage { get; private set; }
 
-        public Action CallBack { get; set; }
+        public Func<object> CallBack { get; set; }
 
         private IExamStrategy _next;
 
@@ -14,6 +14,9 @@ namespace Hotline.Application.Exam.Strategy
 
         private DateTime? _startTime;
 
+        private IExamStrategy _current;
+
+
         public CheckStartTimeStrategy(DateTime examStartTime,DateTime? startTime)
         {
             _examStartTime = examStartTime;
@@ -30,12 +33,23 @@ namespace Hotline.Application.Exam.Strategy
         {
             if (_examStartTime > _startTime)
             {
-                if (CallBack != null)
-                    CallBack();
                 ErroMessage = "考试未开始";
+                _current = this;
                 return false;
             }
+            _current = _next;
+
+            if (_next == null) return true;
+
             return _next.Validate();
         }
+
+        public object GetResult()
+        {
+            if (_current != null && _current.CallBack != null)
+                return _current.CallBack();
+
+            return null;
+        }
     }
 }

+ 15 - 3
src/Hotline.Application/Exam/Strategy/CheckValidateCountStrategy.cs

@@ -11,7 +11,7 @@ namespace Hotline.Application.Exam.Strategy
     {
         public string ErroMessage { get; private set; }
 
-        public Action CallBack { get; set; }
+        public Func<object> CallBack { get; set; }
 
         private IExamStrategy _next;
 
@@ -19,6 +19,8 @@ namespace Hotline.Application.Exam.Strategy
 
         private int _count;
 
+        private IExamStrategy _current;
+
         public CheckValidateCountStrategy(int validateCount, int count)
         {
             _validateCount = validateCount;
@@ -35,12 +37,22 @@ namespace Hotline.Application.Exam.Strategy
         {
             if (_count>=_validateCount)
             {
-                if (CallBack != null)
-                    CallBack();
                 ErroMessage = "已超过可考次数,考试结束";
+                _current = this;
                 return false;
             }
+            _current = _next;
+
+            if (_next == null) return true;
+
             return _next.Validate();
         }
+        public object GetResult()
+        {
+            if (_current!=null && _current.CallBack != null)
+                return _current.CallBack();
+
+            return null;
+        }
     }
 }

+ 19 - 3
src/Hotline.Application/Exam/Strategy/CheckValidateTimeStrategy.cs

@@ -11,7 +11,7 @@ namespace Hotline.Application.Exam.Strategy
     {
         public string ErroMessage { get; private set; }
 
-        public Action CallBack { get; set; }
+        public Func<object> CallBack { get; set; }
 
         private IExamStrategy _next;
 
@@ -19,6 +19,9 @@ namespace Hotline.Application.Exam.Strategy
 
         private int _timeSpan;
 
+        private IExamStrategy _current;
+
+
         public CheckValidateTimeStrategy(int timeSpan, DateTime? startTime)
         {
             _startTime = startTime;
@@ -35,12 +38,25 @@ namespace Hotline.Application.Exam.Strategy
         {
             if (_startTime.Value.AddMinutes(_timeSpan) < DateTime.Now)
             {
-                if (CallBack != null)
-                    CallBack();
                 ErroMessage = "考试已超时,考试结束";
+                _current = this;
                 return false;
             }
+            _current = _next;
+
+            if (_next == null) return true;
+
             return _next.Validate();
         }
+
+
+
+        public object GetResult()
+        {
+            if (_current != null && _current.CallBack != null)
+                return _current.CallBack();
+
+            return null;
+        }
     }
 }

+ 12 - 8
src/Hotline.Application/Exam/Strategy/ExamStrategyProxy.cs

@@ -14,6 +14,10 @@ namespace Hotline.Application.Exam.Strategy
         /// </summary>
         public string ErrorMessage { get; private set; }
 
+        public ExamStrategyProxy()
+        {
+        }
+
 
         public bool Validate()
         {
@@ -26,19 +30,19 @@ namespace Hotline.Application.Exam.Strategy
                 }
                 else
                 {
-                    ErrorMessage = _current.ErroMessage;
-                    if (_current.CallBack != null)
-                    {
-                        _current.CallBack();
-                    }
-                    return false;
-                }
+                    ErrorMessage = _current.ErroMessage;                  
 
-                
+                    return false;
+                }                
             }
             return true;
         }
 
+        public object GetResult()
+        {
+            return _current.GetResult();
+        }
+
         public void Initial(List<IExamStrategy> examStrategys)
         {
             if (examStrategys == null || examStrategys.Count <= 0) return;

+ 33 - 18
src/Hotline.Application/OrderApp/IOrderApplication.cs

@@ -27,12 +27,12 @@ namespace Hotline.Application.OrderApp
 
         ISugarQueryable<Order> QuerySendOrderDetail(QuerySendOrderDetailRequest dto);
 
-		/// <summary>
-		/// 更新工单办理期满时间
-		/// 1.更新工单 2.更新流程
-		/// </summary>
-		/// <returns></returns>
-		Task DelayOrderExpiredTimeAsync(string orderId, int timeCount, ETimeType timeType, bool IsProDelay, CancellationToken cancellationToken);
+        /// <summary>
+        /// 更新工单办理期满时间
+        /// 1.更新工单 2.更新流程
+        /// </summary>
+        /// <returns></returns>
+        Task DelayOrderExpiredTimeAsync(string orderId, int timeCount, ETimeType timeType, bool IsProDelay, CancellationToken cancellationToken);
 
         // /// <summary>
         // /// 新增工单办理流程记录
@@ -378,6 +378,21 @@ namespace Hotline.Application.OrderApp
         /// <returns></returns>
         ISugarQueryable<OrderVisit> QueryOrderVisitList(QueryOrderVisitDto dto);
 
+        /// <summary>
+        /// 回访量明细
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<OrderVisit> QueryOrderVisitQuantityListAsync(QueryOrderVisitListDto dto);
+
+        /// <summary>
+        /// 回访量明细自贡
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<OrderVisitRecord> QueryOrderVisitQuantityListByZgAsync(QueryOrderVisitListDto dto);
+
+
         /// <summary>
         /// 热点类型小类统计明细
         /// </summary>
@@ -515,18 +530,18 @@ namespace Hotline.Application.OrderApp
         /// <returns></returns>
         ISugarQueryable<OrderDto> SeatSendBackStatisticsDetail(SeatSendBackStatisticsDetail dto);
 
-		/// <summary>
-		/// 自动延期记录写入
-		/// </summary>
-		/// <returns></returns>
-		Task OrderDelayAutomatic();
+        /// <summary>
+        /// 自动延期记录写入
+        /// </summary>
+        /// <returns></returns>
+        Task OrderDelayAutomatic();
 
-		/// <summary>
-		/// 自动延期处理
-		/// </summary>
-		/// <param name="type"></param>
-		/// <returns></returns>
-		Task OrderDelayAutomaticHandle(EOrderDelayAutomaticType type);
+        /// <summary>
+        /// 自动延期处理
+        /// </summary>
+        /// <param name="type"></param>
+        /// <returns></returns>
+        Task OrderDelayAutomaticHandle(EOrderDelayAutomaticType type);
 
-	}
+    }
 }

+ 119 - 1
src/Hotline.Application/OrderApp/OrderApplication.cs

@@ -89,6 +89,7 @@ public class OrderApplication : IOrderApplication, IScopeDependency
     private readonly IFileRepository _fileRepository;
     private readonly IRepository<OrderVisit> _orderVisitRepository;
     private readonly IRepository<OrderVisitDetail> _orderVisitDetailRepository;
+    private readonly IRepository<OrderVisitRecord> _orderVisitRecordRepository;
     private readonly IQualityApplication _qualityApplication;
     private readonly ICapPublisher _capPublisher;
     private readonly IRepository<SystemOrganize> _systemOrganizeRepository;
@@ -137,6 +138,7 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         ISessionContext sessionContext,
         IRepository<OrderVisit> orderVisitRepository,
         IRepository<OrderVisitDetail> orderVisitDetailRepository,
+        IRepository<OrderVisitRecord> orderVisitRecordRepository,
         IQualityApplication qualityApplication,
         ICapPublisher capPublisher,
         IRepository<SystemOrganize> systemOrganizeRepository,
@@ -190,6 +192,7 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         _repositoryts = repositoryts;
         _fileRepository = fileRepository;
         _orderVisitRepository = orderVisitRepository;
+        _orderVisitRecordRepository = orderVisitRecordRepository;
         _orderVisitDetailRepository = orderVisitDetailRepository;
         _qualityApplication = qualityApplication;
         _capPublisher = capPublisher;
@@ -2576,7 +2579,8 @@ public class OrderApplication : IOrderApplication, IScopeDependency
             .Where(x => x.OrderVisit.VisitTime >= dto.StartTime.Value && x.OrderVisit.VisitTime <= dto.EndTime.Value &&
                         x.VisitTarget == EVisitTarget.Org && x.OrderVisit.VisitState == EVisitState.Visited && !string.IsNullOrEmpty(x.VisitOrgCode))
             .WhereIF(string.IsNullOrEmpty(dto.OrgName) == false, x => x.VisitOrgName.Contains(dto.OrgName))
-            .WhereIF(string.IsNullOrEmpty(dto.LineNum) == false, x => x.OrderVisit.Order.CallRecord.Gateway.Contains(dto.LineNum))
+            .WhereIF(string.IsNullOrEmpty(dto.LineNum) == false && _appOptions.Value.IsYiBin == true, x => x.OrderVisit.Order.CallRecord.Gateway.Contains(dto.LineNum))
+            .WhereIF(string.IsNullOrEmpty(dto.LineNum) == false && _appOptions.Value.IsYiBin == false, x => x.OrderVisit.Order.FwCallRecord.ToNo.Contains(dto.LineNum))
             .WhereIF(dto.TypeCode != null && dto.TypeCode == 1, x => x.OrderVisit.Order.IdentityType == EIdentityType.Citizen)
             .WhereIF(dto.TypeCode != null && dto.TypeCode == 2, x => x.OrderVisit.Order.IdentityType == EIdentityType.Enterprise)
             .WhereIF(IsCenter == false, x => x.VisitOrgCode.StartsWith(_sessionContext.OrgId))
@@ -4571,6 +4575,120 @@ public class OrderApplication : IOrderApplication, IScopeDependency
         return query;
     }
 
+    /// <summary>
+    /// 回访量明细
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    public ISugarQueryable<OrderVisit> QueryOrderVisitQuantityListAsync(QueryOrderVisitListDto dto)
+    {
+        var query = _orderVisitRepository.Queryable()
+             .Includes(d => d.Order, d => d.OrderTags)
+             .Includes(d => d.Employee)
+             .Includes(d => d.OrderVisitDetails)
+             .Includes(d => d.Order, d => d.OrderVisits.Where(x => Convert.ToInt32(x.VisitState) == 30).ToList())
+             .Where(d => d.VisitTime >= dto.StartTime && d.VisitTime <= dto.EndTime && d.VisitType != null && d.EmployeeId != "" && d.EmployeeId != null)
+             .WhereIF(!string.IsNullOrEmpty(dto.UserID), d => !string.IsNullOrEmpty(d.EmployeeId) && d.EmployeeId == dto.UserID)
+             .WhereIF(!string.IsNullOrEmpty(dto.UserName), d => d.Employee.Name == dto.UserName)
+             .WhereIF(dto.FieldName == "callVisitCount", d => d.VisitType == EVisitType.CallVisit)
+             .WhereIF(dto.FieldName == "defaultVisitCount", d => d.VisitType == EVisitType.ArtificialVisit)
+             .WhereIF(dto.FieldName == "smsVisitCount", d => d.VisitType == EVisitType.SmsVisit)
+             .WhereIF(dto.FieldName == "totalVisitCount", d => d.VisitType == EVisitType.CallVisit || d.VisitType == EVisitType.ArtificialVisit || d.VisitType == EVisitType.SmsVisit);
+
+        query = query
+            .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.No == dto.No)                                                                 // 工单编号
+            .WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Order.Title.Contains(dto.Title!))                                          // 标题
+            .WhereIF(!string.IsNullOrEmpty(dto.ActualHandleOrgName), d => d.Order.ActualHandleOrgName.Contains(dto.ActualHandleOrgName)) // 接办部门
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptType), d => d.Order.AcceptTypeCode == dto.AcceptType)                               // 受理类型
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptorName), d => d.Order.AcceptorName == dto.AcceptorName!)                            // 受理人
+            .WhereIF(!string.IsNullOrEmpty(dto.ActualHandlerName), d => d.Order.ActualHandlerName == dto.ActualHandlerName)              // 接办人
+            .WhereIF(!string.IsNullOrEmpty(dto.FromPhone), d => d.Order.FromPhone.Contains(dto.FromPhone!))                              // 来电号码
+            .WhereIF(!string.IsNullOrEmpty(dto.Contact), d => d.Order.Contact == dto.Contact)                                            // 联系电话
+            .WhereIF(!string.IsNullOrEmpty(dto.FromName), d => d.Order.FromName == dto.FromName)                                         // 来电人姓名
+            .WhereIF(dto.CreationTimeStart.HasValue, d => d.Order.CreationTime >= dto.CreationTimeStart)                                 // 受理时间开始
+            .WhereIF(dto.CreationTimeEnd.HasValue, d => d.Order.CreationTime <= dto.CreationTimeEnd)                                     // 受理时间结束
+            .WhereIF(dto.ExpiredTimeStart.HasValue, d => d.Order.ExpiredTime >= dto.ExpiredTimeStart)                                    // 期满时间开始
+            .WhereIF(dto.ExpiredTimeEnd.HasValue, d => d.Order.ExpiredTime <= dto.ExpiredTimeEnd)                                        // 期满时间结束
+            .WhereIF(dto.Status.HasValue, d => d.Order.Status == dto.Status)                                                             // 工单状态
+            .WhereIF(!string.IsNullOrEmpty(dto.AreaCode), d => d.Order.AreaCode == dto.AreaCode)                                         // 区域
+            .WhereIF(!string.IsNullOrEmpty(dto.Channel), d => d.Order.SourceChannelCode == dto.Channel)                                  // 来源渠道
+            .WhereIF(dto.IsScreen.HasValue && dto.IsScreen.Value, d => SqlFunc.Subqueryable<OrderScreen>().Where(q => q.OrderId == d.OrderId && q.VisitDetailId == d.Id).Any())             // 是否甄别
+            .WhereIF(dto.IsScreen.HasValue && dto.IsScreen.Value == false, d => SqlFunc.Subqueryable<OrderScreen>().Where(q => q.OrderId == d.OrderId && q.VisitDetailId == d.Id).NotAny())
+            .WhereIF(!string.IsNullOrEmpty(dto.CurrentStepName), d => d.Order.CurrentStepName == dto.CurrentStepName)                    // 当前办理节点
+            .WhereIF(dto.FiledTimeStart.HasValue, d => d.Order.FiledTime >= dto.FiledTimeStart)                                          // 办结时间开始
+            .WhereIF(dto.FiledTimeEnd.HasValue, d => d.Order.FiledTime <= dto.FiledTimeEnd)                                              // 办结时间结束
+            .WhereIF(!string.IsNullOrEmpty(dto.Hotspot), d => d.Order.HotspotSpliceName != null && d.Order.HotspotSpliceName.Contains(dto.Hotspot)) // 热点类型
+            .WhereIF(dto.IsSecret.HasValue, d => d.Order.IsSecret == dto.IsSecret)                                                       // 是否紧急
+
+            .OrderByIF(_appOptions.Value.IsZiGong == false, d => d.PublishTime, OrderByType.Desc)
+            .OrderByIF(dto is { SortField: "publishTime", SortRule: 0 }, x => x.PublishTime, OrderByType.Asc) // 发布时间升序
+            .OrderByIF(dto is { SortField: "publishTime", SortRule: 1 }, x => x.PublishTime, OrderByType.Desc)// 发布时间升序
+            .OrderByIF(dto is { SortField: "order.creationTime", SortRule: 0 }, x => x.Order.CreationTime, OrderByType.Asc) // 受理时间升序
+            .OrderByIF(dto is { SortField: "order.creationTime", SortRule: 1 }, x => x.Order.CreationTime, OrderByType.Desc) // 受理时间升序
+            .OrderByIF(dto is { SortField: "order.filedTime", SortRule: 0 }, x => x.Order.FiledTime, OrderByType.Asc) // 办结时间升序
+            .OrderByIF(dto is { SortField: "order.filedTime", SortRule: 1 }, x => x.Order.FiledTime, OrderByType.Desc) // 办结时间升序
+            .OrderByIF(dto is { SortField: "visitTime", SortRule: 0 }, x => x.VisitTime, OrderByType.Asc) // 回访时间升序
+            .OrderByIF(dto is { SortField: "visitTime", SortRule: 1 }, x => x.VisitTime, OrderByType.Desc) // 回访时间升序
+            .OrderByIF(_appOptions.Value.IsZiGong && string.IsNullOrEmpty(dto.SortField), d => d.PublishTime, OrderByType.Desc)
+            ;
+        return query;
+    }
+
+    /// <summary>
+    /// 回访量明细自贡
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    public ISugarQueryable<OrderVisitRecord> QueryOrderVisitQuantityListByZgAsync(QueryOrderVisitListDto dto)
+    {
+        var query = _orderVisitRecordRepository.Queryable()
+            .Includes(d => d.Order, d => d.OrderTags)
+            .Includes(d => d.Employee)
+            .Includes(d => d.Order, d => d.OrderVisits.Where(x => Convert.ToInt32(x.VisitState) == 30).ToList())
+            .WhereIF(!string.IsNullOrEmpty(dto.UserID), d => !string.IsNullOrEmpty(d.EmployeeId) && d.EmployeeId == dto.UserID)
+            .WhereIF(!string.IsNullOrEmpty(dto.UserName), d => d.Employee.Name == dto.UserName)
+            .WhereIF(dto.StartTime != null, d => d.VisitTime >= dto.StartTime && d.VisitType != null && d.EmployeeId != "" && d.EmployeeId != null)
+            .WhereIF(dto.EndTime != null, d => d.VisitTime <= dto.EndTime && d.VisitType != null && d.EmployeeId != "" && d.EmployeeId != null)
+            .WhereIF(dto.FieldName == "callVisitCount", d => d.VisitType == EVisitType.CallVisit)
+            .WhereIF(dto.FieldName == "defaultVisitCount", d => d.VisitType == EVisitType.ArtificialVisit)
+            .WhereIF(dto.FieldName == "smsVisitCount", d => d.VisitType == EVisitType.SmsVisit)
+            .WhereIF(dto.FieldName == "totalVisitCount", d => d.VisitType == EVisitType.CallVisit || d.VisitType == EVisitType.ArtificialVisit || d.VisitType == EVisitType.SmsVisit);
+
+        query = query
+            .WhereIF(!string.IsNullOrEmpty(dto.No), d => d.No == dto.No)                                                                 // 工单编号
+            .WhereIF(!string.IsNullOrEmpty(dto.Title), d => d.Order.Title.Contains(dto.Title!))                                          // 标题
+            .WhereIF(!string.IsNullOrEmpty(dto.ActualHandleOrgName), d => d.Order.ActualHandleOrgName.Contains(dto.ActualHandleOrgName)) // 接办部门
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptType), d => d.Order.AcceptTypeCode == dto.AcceptType)                               // 受理类型
+            .WhereIF(!string.IsNullOrEmpty(dto.AcceptorName), d => d.Order.AcceptorName == dto.AcceptorName!)                            // 受理人
+            .WhereIF(!string.IsNullOrEmpty(dto.ActualHandlerName), d => d.Order.ActualHandlerName == dto.ActualHandlerName)              // 接办人
+            .WhereIF(!string.IsNullOrEmpty(dto.FromPhone), d => d.Order.FromPhone.Contains(dto.FromPhone!))                              // 来电号码
+            .WhereIF(!string.IsNullOrEmpty(dto.Contact), d => d.Order.Contact == dto.Contact)                                            // 联系电话
+            .WhereIF(!string.IsNullOrEmpty(dto.FromName), d => d.Order.FromName == dto.FromName)                                         // 来电人姓名
+            .WhereIF(dto.CreationTimeStart.HasValue, d => d.Order.CreationTime >= dto.CreationTimeStart)                                 // 受理时间开始
+            .WhereIF(dto.CreationTimeEnd.HasValue, d => d.Order.CreationTime <= dto.CreationTimeEnd)                                     // 受理时间结束
+            .WhereIF(dto.ExpiredTimeStart.HasValue, d => d.Order.ExpiredTime >= dto.ExpiredTimeStart)                                    // 期满时间开始
+            .WhereIF(dto.ExpiredTimeEnd.HasValue, d => d.Order.ExpiredTime <= dto.ExpiredTimeEnd)                                        // 期满时间结束
+            .WhereIF(dto.Status.HasValue, d => d.Order.Status == dto.Status)                                                             // 工单状态
+            .WhereIF(!string.IsNullOrEmpty(dto.AreaCode), d => d.Order.AreaCode == dto.AreaCode)                                         // 区域
+            .WhereIF(!string.IsNullOrEmpty(dto.Channel), d => d.Order.SourceChannelCode == dto.Channel)                                  // 来源渠道
+            .WhereIF(dto.IsScreen.HasValue && dto.IsScreen.Value, d => SqlFunc.Subqueryable<OrderScreen>().Where(q => q.OrderId == d.OrderId && q.VisitDetailId == d.Id).Any())             // 是否甄别
+            .WhereIF(dto.IsScreen.HasValue && dto.IsScreen.Value == false, d => SqlFunc.Subqueryable<OrderScreen>().Where(q => q.OrderId == d.OrderId && q.VisitDetailId == d.Id).NotAny())
+            .WhereIF(!string.IsNullOrEmpty(dto.CurrentStepName), d => d.Order.CurrentStepName == dto.CurrentStepName)                    // 当前办理节点
+            .WhereIF(dto.FiledTimeStart.HasValue, d => d.Order.FiledTime >= dto.FiledTimeStart)                                          // 办结时间开始
+            .WhereIF(dto.FiledTimeEnd.HasValue, d => d.Order.FiledTime <= dto.FiledTimeEnd)                                              // 办结时间结束
+            .WhereIF(!string.IsNullOrEmpty(dto.Hotspot), d => d.Order.HotspotSpliceName != null && d.Order.HotspotSpliceName.Contains(dto.Hotspot)) // 热点类型
+            .WhereIF(dto.IsSecret.HasValue, d => d.Order.IsSecret == dto.IsSecret)                                                       // 是否紧急
+
+            .OrderByIF(dto is { SortField: "order.creationTime", SortRule: 0 }, x => x.Order.CreationTime, OrderByType.Asc) // 受理时间升序
+            .OrderByIF(dto is { SortField: "order.creationTime", SortRule: 1 }, x => x.Order.CreationTime, OrderByType.Desc) // 受理时间升序
+            .OrderByIF(dto is { SortField: "order.filedTime", SortRule: 0 }, x => x.Order.FiledTime, OrderByType.Asc) // 办结时间升序
+            .OrderByIF(dto is { SortField: "order.filedTime", SortRule: 1 }, x => x.Order.FiledTime, OrderByType.Desc) // 办结时间升序
+            .OrderByIF(dto is { SortField: "visitTime", SortRule: 0 }, x => x.VisitTime, OrderByType.Asc) // 回访时间升序
+            .OrderByIF(dto is { SortField: "visitTime", SortRule: 1 }, x => x.VisitTime, OrderByType.Desc) // 回访时间升序
+            ;
+        return query;
+    }
+
     /// <summary>
     /// 热点类型小类统计明细
     /// </summary>

+ 1 - 1
src/Hotline.Application/Snapshot/Notifications/SnapshotHandler.cs

@@ -64,7 +64,7 @@ public class SnapshotHandler : ICapSubscribe, IScopeDependency
     /// <param name="bulletinId"></param>
     /// <param name="token"></param>
     /// <returns></returns>
-    [CapSubscribe(EventNames.BulletinIsPass, Group = "snapshot")]
+    [CapSubscribe(EventNames.BulletinIsPass)]
     public async Task BulletinIsPassHandler(string bulletinId, CancellationToken token)
     {
         await _snapshotBulletinApplication.NotifyUserAsync(bulletinId, token);

+ 1 - 1
src/Hotline.Application/Snapshot/SnapshotApplicationBase.cs

@@ -1122,7 +1122,7 @@ public abstract class SnapshotApplicationBase
             Items = await _notificationReceiverRepository.Queryable()
                 .LeftJoin<Notification>((receiver, notify) => notify.Id == receiver.NotificationId)
                 .LeftJoin<SnapshotBulletin>((receiver, notify, bulletin) => bulletin.Id == notify.ExternalId)
-                .Where((receiver, notify, bulletin) => bulletin.Shape == EBulletinShape.Message)
+                .Where((receiver, notify, bulletin) => bulletin.Shape == EBulletinShape.Message && receiver.ReceiverId == _sessionContext.UserId)
                 .GroupBy((receiver, notify, bulletin) => new { notify.Id, notify.ExternalId, notify.Title})
                 .Select((receiver, notify, bulletin) => new PointsBulletinItemsOutDto
                 {

+ 4 - 2
src/Hotline.Application/StatisticalReport/CallReport/CallReportApplicationBase.cs

@@ -148,7 +148,8 @@ public abstract class CallReportApplicationBase : ICallReportApplication
                      RecordingFileUrl = recordPrefix + p.AudioFile,
                      RecordingFileName = p.AudioFile,
                      RecordingBaseAddress = recordPrefix,
-                     RecordingAbsolutePath = p.AudioFile
+                     RecordingAbsolutePath = p.AudioFile,
+                     CreatedTime = p.CreationTime
                  }, true);
 
         if (isAll)
@@ -206,7 +207,8 @@ public abstract class CallReportApplicationBase : ICallReportApplication
                     RecordingFileUrl = recordPrefix + p.AudioFile,
                     RecordingFileName = p.AudioFile,
                     RecordingBaseAddress = recordPrefix,
-                    RecordingAbsolutePath = p.AudioFile
+                    RecordingAbsolutePath = p.AudioFile,
+                    CreatedTime = p.CreationTime
                 }, true);
 
         if (isAll)

+ 4 - 2
src/Hotline.Application/StatisticalReport/CallReport/YiBinCallReportApplication.cs

@@ -127,7 +127,8 @@ public class YiBinCallReportApplication : CallReportApplicationBase, ICallReport
                      EndTime = m.OverTime,
                      OrderId = m.Order.Id,
                      OrderTitle = m.Order.Title,
-                     OrderNo = m.Order.No
+                     OrderNo = m.Order.No,
+                     CreatedTime = m.CreationTime
                  }, true);
         if (isAll)
         {
@@ -175,7 +176,8 @@ public class YiBinCallReportApplication : CallReportApplicationBase, ICallReport
                      EndTime = p.OverTime,
                      OrderId = p.Order.Id,
                      OrderTitle = p.Order.Title,
-                     OrderNo = p.Order.No
+                     OrderNo = p.Order.No,
+                     CreatedTime = p.CreationTime
                  }, true);
 
         if (isAll)

+ 1 - 0
src/Hotline.Repository.SqlSugar/Exam/Validators/ExamManages/ExamManageValidator.cs

@@ -55,6 +55,7 @@ namespace Exam.Application
             RuleFor(m => m.EndTime).NotNull().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.EndTime))));
             RuleFor(m => m.EndTime).Must((e, v) => e.ExamType == EExamType.Simulate || e.StartTime < v).WithMessage(x => string.Format(ExamErrorMessage.Greater, x.GetType().GetDescription(nameof(ExamManage.EndTime)), x.GetType().GetDescription(nameof(ExamManage.StartTime))));
             RuleFor(m => m.TotalScore).NotNull().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.TotalScore))));
+            RuleFor(m => m.CutoffScore).Must((e, v)=> e.TotalScore >= v).WithMessage(x => string.Format(ExamErrorMessage.Greater, x.GetType().GetDescription(nameof(ExamManage.CutoffScore)), x.GetType().GetDescription(nameof(ExamManage.TotalScore))));
         }
 
         protected override void ValidateRuleWithAdd()

+ 0 - 6
src/Hotline.Share/Dtos/ExamManages/GradingExamItemDto.cs

@@ -16,12 +16,6 @@ namespace Exam.Share.Dtos.ExamManage
         /// </summary>
         [Description("得分")]
         public int Score { get; set; }
-
-        /// <summary>
-        /// 是否已阅卷
-        /// </summary>
-        [Description("是否已阅卷")]
-        public bool IsCheck { get; set; }
     }
 
     public class BatchGradingExamItemDto : IAddRequest

+ 16 - 3
src/Hotline.Share/Dtos/ExamManages/GradingExamQuestionDto.cs

@@ -3,6 +3,7 @@ using Exam.Share;
 using Hotline.Share.Dtos.ExamManages;
 using Hotline.Share.Enums.Exams;
 using System.ComponentModel;
+using System.Runtime.CompilerServices;
 
 namespace Exam.Application.Interface.Exam
 {
@@ -21,6 +22,12 @@ namespace Exam.Application.Interface.Exam
 
         public string CorrectAnswer { get; set; }
 
+        /// <summary>
+        /// 试题分数
+        /// </summary>
+        [Description("试题分数")]
+        public int QuestionScore { set; get; }
+
     }
 
     public class GradingExamQuestionTempDto
@@ -92,13 +99,19 @@ namespace Exam.Application.Interface.Exam
         public bool IsSelected { get; set; }
 
         /// <summary>
-        /// 分数
+        /// 试题分数
+        /// </summary>
+        [Description("试题分数")]
+        public int QuestionScore { get; set; }
+
+        /// <summary>
+        /// 实际分数
         /// </summary>
-        [Description("分数")]
+        [Description("实际分数")]
         public int? Score { get; set; }
     }
 
-    public class UserExamQuestionDto:IActionRequest
+    public class UserExamQuestionDto : IActionRequest
     {
         /// <summary>
         /// 主键

+ 80 - 2
src/Hotline.Share/Dtos/Order/OrderVisitDto.cs

@@ -156,10 +156,88 @@ namespace Hotline.Share.Dtos.Order
         /// <summary>
         /// 回访人是否为空
         /// </summary>
-        public bool? IsEmployeeNameNull { get; set;}
+        public bool? IsEmployeeNameNull { get; set; }
 
     }
 
+    public record QueryOrderVisitListDto : QueryOrderVisitDto
+    {
+        public string? UserID { get; set; }
+        public string? UserName { get; set; }
+        public string? FieldName { get; set; }
+        public string? Title { get; set; }
+
+        /// <summary>
+        /// 受理类型
+        /// </summary>
+        public string? AcceptType { get; set; }
+
+        /// <summary>
+        /// 受理人
+        /// </summary>
+        public string? AcceptorName { get; set; }
+
+        /// <summary>
+        /// 接办人
+        /// </summary>
+        public string? ActualHandlerName { get; set; }
+
+        /// <summary>
+        /// 来电人
+        /// </summary>
+        public string? FromName { get; set; }
+
+        /// <summary>
+        /// 期满时间开始
+        /// </summary>
+        public DateTime? ExpiredTimeStart { get; set; }
+
+        /// <summary>
+        /// 期满时间结束
+        /// </summary>
+        public DateTime? ExpiredTimeEnd { get; set; }
+
+        /// <summary>
+        /// 工单状态
+        /// </summary>
+        public EOrderStatus? Status { get; set; }
+
+        /// <summary>
+        /// 区域
+        /// </summary>
+        public string? AreaCode { get; set; }
+
+        /// <summary>
+        /// 当前节点
+        /// </summary>
+        public string? CurrentStepName { get; set; }
+
+        /// <summary>
+        /// 办结时间开始
+        /// </summary>
+        public DateTime? FiledTimeStart { get; set; }
+
+        /// <summary>
+        /// 办结时间结束
+        /// </summary>
+        public DateTime? FiledTimeEnd { get; set; }
+
+        /// <summary>
+        /// 热点
+        /// </summary>
+        public string? Hotspot { get; set; }
+
+        /// <summary>
+        /// 是否紧急
+        /// </summary>
+        public bool? IsSecret { get; set; }
+
+        /// <summary>
+        /// 是否甄别
+        /// </summary>
+        public bool? IsScreen { get; set; }
+    }
+
     public record QueryOrderPublishStatisticsAllDto : PagedRequest
     {
         /// <summary>
@@ -495,7 +573,7 @@ namespace Hotline.Share.Dtos.Order
         /// <summary>
         /// 是否观察件
         /// </summary>
-        public bool? IsObservationPiece {  get; set; }
+        public bool? IsObservationPiece { get; set; }
 
 
     }

+ 5 - 0
src/Hotline.Share/Dtos/TrCallCenter/TrTelDao.cs

@@ -861,6 +861,11 @@ namespace Hotline.Share.Dtos.TrCallCenter
         /// 用户名
         /// </summary>
         public string UserName { get; set; }
+        
+        /// <summary>
+        /// 开始时间
+        /// </summary>
+        public DateTime? CreatedTime { get; set; }
 
         /// <summary>
         /// 开始等待时间

+ 6 - 3
src/Hotline.Share/Dtos/Trains/TrainPracticeDto.cs

@@ -27,9 +27,6 @@ namespace Hotline.Share.Dtos.Trains
         /// </summary>
         [Description("关联知识")]
         public new List<TrainPracticeKnowladgeDto> TrainPracticeKnowladgeDtos { get; set; }
-
-
-        
     }
 
     /// <summary>
@@ -68,6 +65,12 @@ namespace Hotline.Share.Dtos.Trains
         /// </summary>
         [Description("培训习题选项")]
         public List<SimpleTrainPracticeOptionsDto> TrainPracticeOptionsDtos { get; set; }
+
+
+        /// <summary>
+        /// 答案
+        /// </summary>
+        public AddTrainRecordAnswerDto? AddTrainRecordAnswerDto { get; set; }
     }
 
     public class AddTrainPracticeDto : IAddRequest, IOperationStatus

+ 5 - 0
src/Hotline.Share/Dtos/Trains/TrainPracticeOptionsDto.cs

@@ -92,5 +92,10 @@ namespace Hotline.Share.Dtos.Trains
         [Description("是否选择")]
         public bool IsSelected { get; set; }
 
+        /// <summary>
+        /// 是否正确答案
+        /// </summary>
+        [Description("是否正确答案")]
+        public bool IsAnswer { get; set; }
     }
 }

+ 8 - 0
src/Hotline.Share/Dtos/Trains/TrainRecordAnswerDto.cs

@@ -3,6 +3,7 @@ using Exam.Infrastructure.Data.Interface;
 using Exam.Infrastructure.Enums;
 using Hotline.Share.Exams.Interface;
 using System.ComponentModel;
+using System.Text.Json.Serialization;
 
 namespace Hotline.Share.Dtos.Trains
 {
@@ -63,6 +64,13 @@ namespace Hotline.Share.Dtos.Trains
         [Description("培训记录Id")]
         public string TrainRecordId { get; set; }
 
+        /// <summary>
+        /// 试题Id
+        /// </summary>
+        [Description("试题Id")]
+        [JsonIgnore]
+        public string QuestionId { get; set; }
+
         /// <summary>
         /// 答案
         /// </summary>

+ 3 - 1
src/Hotline.Share/Exams/Extensions/ReportPagedRequestExtensions.cs

@@ -4,8 +4,10 @@ namespace Hotline.Share.Exams.Extensions
 {
     public static class ReportPagedRequestExtensions
     {
-        public static void ResoleEndTime<T>(this T analysisReportRequest)where T :AnalysisReportRequest
+        public static void ResolveEndTime<T>(this T analysisReportRequest)where T :AnalysisReportRequest
         {
+            if (analysisReportRequest.StartTime == null) return;
+
             switch (analysisReportRequest.AnlysisType)
             {
                 case Share.Enums.Exams.EAnlysisType.Day:

+ 2 - 2
src/Hotline.Share/Requests/Exam/UserExamResultReportPagedRequest.cs

@@ -9,13 +9,13 @@ namespace Hotline.Share.Requests.Exam
         /// 最小分数
         /// </summary>
         [Description("最小分数")]
-        public int MinScore { get; set; }
+        public int? MinScore { get; set; }
 
         /// <summary>
         /// 最大分数
         /// </summary>
 
         [Description("最大分数")]
-        public int MaxScore { get; set; }
+        public int? MaxScore { get; set; }
     }
 }

+ 6 - 0
src/Hotline.Share/Requests/Question/QuestionPagedRequest.cs

@@ -30,5 +30,11 @@ namespace Hotline.Share.Requests.Question
         /// </summary>
         [Description("知识库Id")]
         public List<string> KnowladgeIds { get; set; }
+
+        /// <summary>
+        /// 题型
+        /// </summary>
+        [Description("题型")]
+        public EQuestionType? QuestionType { get; set; }
     }
 }

+ 3 - 2
src/TianQue.Sdk/TQHttpClient.cs

@@ -78,11 +78,12 @@ public class TQHttpClient
 
             if (SSHHost.NotNullOrEmpty() && SSHPort != 0 && SSHUserName.NotNullOrEmpty() && SSHPassword.NotNullOrEmpty())
             {
+                var port = new Random().Next(8089, 8099);
                 _sshClient.Connect();
-                var forwardedPort = new ForwardedPortLocal("127.0.0.1", 8090, uri.Host, (uint)uri.Port);
+                var forwardedPort = new ForwardedPortLocal("127.0.0.1", (uint)port, uri.Host, (uint)uri.Port);
                 _sshClient.AddForwardedPort(forwardedPort);
                 forwardedPort.Start();
-                uri = new Uri("http://127.0.0.1:8090" + uri.AbsolutePath);
+                uri = new Uri("http://127.0.0.1:" + port + uri.AbsolutePath);
             }
 
             var content = new StringContent(body, Encoding.GetEncoding("UTF-8"));

+ 5 - 5
test/Hotline.Tests/Application/OrderSnapshotApplicationTest.cs

@@ -169,7 +169,7 @@ public class OrderSnapshotApplicationTest : TestBase
     public async Task SnapshotWorkflow_Guider_Timeout_Test()
     {
         SetSettingCache(SettingConstants.OvertimeBack, "0.00027778");
-        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin)
+        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin, "安全隐患")
             .办理到网格员(SetZuoXi)
             .StepHandle(async order =>
             {
@@ -381,11 +381,11 @@ public class OrderSnapshotApplicationTest : TestBase
     {
         var snapshotLabels = _systemDicDataCacheManager.SnapshotOrderLabel;
         var inputLable = snapshotLabels.Where(m => m.DicDataValue == "bss").ToList();
-        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin)
+        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin, "安全隐患")
             .办理到工单标注(SetZuoXi)
-            .办理到派单员(SetZuoXi, false)
-            .办理到归档(SetZuoXi)
-            .发布工单(SetZuoXi, inputLable.Select(m => new Kv { Key = m.DicDataValue, Value = m.DicDataName }).ToList())
+            .办理到派单员(Set班长, false)
+            .办理到归档(SetPaiDanYuan)
+            .发布工单(SetPaiDanYuan, inputLable.Select(m => new Kv { Key = m.DicDataValue, Value = m.DicDataName }).ToList())
             .GetCreateResult();
         order.Id.ShouldNotBeNull();
 

+ 10 - 3
test/Hotline.Tests/Application/RedPackApplicationTest.cs

@@ -58,12 +58,19 @@ public class RedPackApplicationTest : TestBase
         var dhzy = _systemDicDataCacheManager.OrderTag.Where(m => m.DicDataName == "动火作业").FirstOrDefault();
         dhzy.ShouldNotBeNull("SysDicTypeConsts.OrderTag 缺少动作火作业基础信息");
         SetZuoXi();
-        SetSettingCache(SettingConstants.OvertimeBack, "0.00027778");
         var industryItems = await _industryRepository.Queryable().Where(m => m.Name == "安全隐患").Select(d => new { d.Id, d.Name, }).ToListAsync();
         var industry = industryItems.First();
         var order = _orderServiceMock.CreateOrder(industryId: industry.Id, industryName: industry.Name, tags: [dhzy.Id])
+            .StepHandle(async order => 
+            {
+                SetSettingCache(SettingConstants.OvertimeBack, "0.00027778");
+            })
             .办理到网格员(SetZuoXi)
-            .StepHandle(async order => Thread.Sleep(5 * 1000))
+            .StepHandle(async order =>
+            {
+                Thread.Sleep(5 * 1000);
+                SetSettingCache(SettingConstants.OvertimeBack, "4");
+            })
             .办理到派单员(Set班长)
             .办理到一级部门(SetPaiDanYuan)
             .办理到归档(Set一级部门)
@@ -109,7 +116,7 @@ public class RedPackApplicationTest : TestBase
     /// 获取红包记录
     /// </summary>
     /// <returns></returns>
-    [Fact]
+    //[Fact]
     public async Task AuditRedPackAudit_Test()
     {
         var items = await _redPackApplication.GetRedPackAuditItems(new SnapshotOrderAuditItemsInDto(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 0)).ToListAsync();

+ 9 - 8
test/Hotline.Tests/Application/SnapshotApplicationTest.cs

@@ -160,7 +160,7 @@ public class SnapshotApplicationTest : TestBase
     public async Task PublishOrder_Test()
     {
         var industry = await _industryRepository.Queryable()
-            .Where(m => m.IndustryType == EIndustryType.Clue)
+            .Where(m => m.Name == "电气焊作业申报")
             .OrderBy(m => m.DisplayOrder)
             .FirstAsync();
 
@@ -251,11 +251,11 @@ public class SnapshotApplicationTest : TestBase
     [Fact]
     public async Task RefreshTokenAsync()
     {
-        var token = await _identityAppService.GetThirdTokenAsync(new ThirdTokenInDto(), CancellationToken.None);
-        var newToken = await _identityAppService.RefreshTokenAsync(token["OpenId"].ToString(), CancellationToken.None);
+        var token = await _identityAppService.GetThirdTokenAsync(new ThirdTokenInDto { ThirdType = EThirdType.Test, LoginCode = "测试生成的OpenId" }, CancellationToken.None);
+        var newToken = await _identityAppService.RefreshTokenAsync(token["openId"].ToString(), CancellationToken.None);
         newToken.ShouldNotBeNull();
-        newToken["OpenId"].ShouldBe(token["OpenId"].ToString());
-        newToken["PhoneNumber"].ToString().ShouldNotBeNullOrEmpty();
+        newToken["openId"].ShouldBe(token["openId"].ToString());
+        newToken["phoneNumber"].ToString().ShouldNotBeNullOrEmpty();
     }
 
     /// <summary>
@@ -265,8 +265,8 @@ public class SnapshotApplicationTest : TestBase
     [Fact]
     public async Task GetThirdToken_Test()
     {
-        var result = await _identityAppService.GetThirdTokenAsync(new ThirdTokenInDto { AppType = EAppType.Snapshot, ThirdType = EThirdType.WeChat,   LoginCode = "0c3Adhll2zDMBe413rnl2KvEym2AdhlH" }, CancellationToken.None);
-        result["PhoneNumber"].ToString().ShouldNotBeNullOrEmpty();
+        var result = await _identityAppService.GetThirdTokenAsync(new ThirdTokenInDto { AppType = EAppType.Snapshot, ThirdType = EThirdType.Test,   LoginCode = "测试生成的OpenId" }, CancellationToken.None);
+        result["phoneNumber"].ToString().ShouldNotBeNullOrEmpty();
     }
 
     [Fact]
@@ -527,7 +527,8 @@ public class SnapshotApplicationTest : TestBase
     [Fact]
     public async Task Snapshot_Test()
     {
-        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin)
+        SetSettingCache(SettingConstants.OvertimeBack, "4");
+        var order = _orderServiceMock.CreateSnapshotOrder(SetWeiXin, "安全隐患")
             .办理到网格员(SetZuoXi)
             .GetCreateResult();
         await _snapshotApplication.PostOrderGuiderSystemAsync(order.Id, CancellationToken.None);

+ 1 - 1
test/Hotline.Tests/Application/SystemSettingCacheManagerTest.cs

@@ -35,7 +35,7 @@ public class SystemSettingCacheManagerTest : TestBase
     public void CancelPublishOrderEnabled_Test()
     {
         var dd = DateTime.Parse("11/19/2024 18:08:00");
-        _systemSettingCacheManager.CallSyncUnPushDateTime.ShouldBe(DateTime.Parse("2024/11/19 18:08:00"));
+        _systemSettingCacheManager.CallSyncUnPushDateTime.ShouldBe(DateTime.Parse("2025/04/11 15:40:00"));
         //var result = _systemSettingCacheManager.CancelPublishOrderEnabled;
         //result.ShouldBeTrue();
         var seconds = _systemSettingCacheManager.VisitCallDelaySecond;

+ 3 - 3
test/Hotline.Tests/Controller/OrderControllerTest.cs

@@ -249,7 +249,7 @@ public class OrderControllerTest : TestBase
     [Fact]
     public async Task OrderSnapshot_Test()
     {
-        var industryItems = await _industryRepository.Queryable().Select(d => new { d.Id, d.Name, }).ToListAsync();
+        var industryItems = await _industryRepository.Queryable().Where(m => m.Name == "文化旅游").Select(d => new { d.Id, d.Name, }).ToListAsync();
         var industry = industryItems.First();
 
         SetZuoXi();
@@ -447,8 +447,8 @@ public class OrderControllerTest : TestBase
             Remark = "单元测试标注",
             OrderIds = new List<string>()
         };
-        inDto.OrderIds.Add(_orderServiceMock.CreateSnapshotOrder(SetWeiXin).办理到工单标注(SetZuoXi).GetCreateResult().Id);
-        inDto.OrderIds.Add(_orderServiceMock.CreateSnapshotOrder(SetWeiXin).办理到工单标注(SetZuoXi).GetCreateResult().Id);
+        inDto.OrderIds.Add(_orderServiceMock.CreateSnapshotOrder(SetWeiXin, "安全隐患").办理到工单标注(SetZuoXi).GetCreateResult().Id);
+        inDto.OrderIds.Add(_orderServiceMock.CreateSnapshotOrder(SetWeiXin, "安全隐患").办理到工单标注(SetZuoXi).GetCreateResult().Id);
         Set班长();
         await _orderController.GetNextStepsWithRecommend(inDto.OrderIds.First());
         var result = await _orderController.OrderSignBathAsync(inDto);

+ 1 - 1
test/Hotline.Tests/Infrastructure/TianQueTest.cs

@@ -29,7 +29,7 @@ public class TianQueTest
         // b55dfdedba900437d486e70e5fb78ed50afaeb910a2346f16ef03af656f8bb0b
     }
 
-    [Fact]
+    //[Fact]
     public async Task PostAcceptInfo_Test()
     {
         // Arrange

+ 18 - 3
test/Hotline.Tests/Mock/OrderServiceMock.cs

@@ -30,6 +30,7 @@ using System;
 using DocumentFormat.OpenXml.Bibliography;
 using Hotline.Snapshot;
 using Hotline.Share.Enums.Settings;
+using Npgsql;
 
 namespace Hotline.Tests.Mock;
 public class OrderServiceMock
@@ -130,7 +131,21 @@ public class OrderServiceMock
         AddOrderDto.Title = "单元测试" + DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
         if (tags.NotNullOrEmpty())
             AddOrderDto.Tags = tags;
-        CreateOrderOutDto = _orderController.Add(AddOrderDto).GetAwaiter().GetResult().ToJson().FromJson<CreateOrderOutDto>();
+        RetryHelper.Retry(() =>
+            {
+                try
+                {
+                    CreateOrderOutDto = _orderController.Add(AddOrderDto).GetAwaiter().GetResult().ToJson().FromJson<CreateOrderOutDto>();
+                }
+                catch (PostgresException e)
+                {
+                    if (e.Message.Contains("unique_order_no"))
+                    {
+                        return false;
+                    }
+                }
+                return true;
+            }, 10);
         _orderServiceStartWorkflow.orderServiceMock = this;
         return _orderServiceStartWorkflow;
     }
@@ -657,7 +672,7 @@ public class OrderServiceMock
         return this;
 
     }
-    
+
     public string 申请延期(Action action = null)
     {
         action?.Invoke();
@@ -669,7 +684,7 @@ public class OrderServiceMock
             {
                 NextStepCode = step.Key,
                 NextStepName = step.Value,
-                NextHandlers = new List<StepAssignInfo>{step.Items.First()},
+                NextHandlers = new List<StepAssignInfo> { step.Items.First() },
                 Opinion = "单元测试延期申请",
                 FlowDirection = step.FlowDirection,
                 BackToCountersignEnd = step.BackToCountersignEnd,