Explorar o código

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

qinchaoyue hai 3 semanas
pai
achega
f5c933bcab
Modificáronse 37 ficheiros con 881 adicións e 110 borrados
  1. 69 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/UserExamController.cs
  4. 2 1
      src/Hotline.Api/Controllers/WebPortalController.cs
  5. 2 0
      src/Hotline.Application/Exam/Constants/ApiRoutes/UserExamApiRoute.cs
  6. 7 0
      src/Hotline.Application/Exam/Interface/ExamManages/IUserExamService.cs
  7. 1 0
      src/Hotline.Application/Exam/QueryExtensions/Practices/PracticeQueryExtensions.cs
  8. 1 0
      src/Hotline.Application/Exam/QueryExtensions/Questions/QuestionQueryExtesions.cs
  9. 9 9
      src/Hotline.Application/Exam/Service/ExamManages/ExamManageService.cs
  10. 1 1
      src/Hotline.Application/Exam/Service/ExamManages/ExtractRuleService.cs
  11. 178 22
      src/Hotline.Application/Exam/Service/ExamManages/UserExamService.cs
  12. 3 0
      src/Hotline.Application/Exam/Service/Practices/PracticeService.cs
  13. 1 1
      src/Hotline.Application/Exam/Service/Questions/QuestionService.cs
  14. 5 5
      src/Hotline.Application/Exam/Service/TestPapers/TestPaperService.cs
  15. 23 17
      src/Hotline.Application/Exam/Service/Trains/TrainRecordService.cs
  16. 33 18
      src/Hotline.Application/OrderApp/IOrderApplication.cs
  17. 115 0
      src/Hotline.Application/OrderApp/OrderApplication.cs
  18. 63 1
      src/Hotline.Application/StatisticalReport/CallReport/CallReportApplicationBase.cs
  19. 53 1
      src/Hotline.Application/StatisticalReport/CallReport/YiBinCallReportApplication.cs
  20. 7 0
      src/Hotline.Application/StatisticalReport/ICallReportApplication.cs
  21. 3 2
      src/Hotline.Repository.SqlSugar/Exam/Validators/ExamManages/ExamManageValidator.cs
  22. 18 0
      src/Hotline.Share/Dtos/CallCenter/BiQueryCallsDto.cs
  23. 1 1
      src/Hotline.Share/Dtos/ExamManages/ExamManageDto.cs
  24. 1 1
      src/Hotline.Share/Dtos/ExamManages/ExamQuestionDto.cs
  25. 1 1
      src/Hotline.Share/Dtos/ExamManages/GradingExamQuestionDto.cs
  26. 80 2
      src/Hotline.Share/Dtos/Order/OrderVisitDto.cs
  27. 5 0
      src/Hotline.Share/Dtos/TrCallCenter/TrTelDao.cs
  28. 8 0
      src/Hotline.Share/Dtos/Trains/TrainRecordAnswerDto.cs
  29. 7 0
      src/Hotline.Share/Dtos/Trains/TrainRecordDto.cs
  30. 1 1
      src/Hotline.Share/Enums/Exams/EDifficultyLevel.cs
  31. 7 2
      src/Hotline.Share/Enums/Exams/EMethod.cs
  32. 1 1
      src/Hotline.Share/Requests/Exam/ExamManagePagedRequest.cs
  33. 9 0
      src/Hotline.Share/Requests/Exam/ExamQuestionGroupRequest.cs
  34. 6 0
      src/Hotline.Share/Requests/Question/QuestionPagedRequest.cs
  35. 4 4
      src/Hotline.Share/ViewResponses/Exam/ExamManageViewResponse.cs
  36. 28 0
      src/Hotline.Share/ViewResponses/Exam/UnExamUserViewResponse.cs
  37. 1 1
      src/Hotline/Exams/ExamManages/ExamManage.cs

+ 69 - 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,72 @@ 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>
+    /// 坐席话务统计分析明细
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("seats/list")]
+    public async Task<PagedDto<CallRecordOutDto>> QuerySeatCallsListAsync([FromQuery] BiQueryCallsListDto dto)
+    {
+        var (total, items) = await _callReportApplication.QuerySeatCallsListAsync(dto, false);
+        return new PagedDto<CallRecordOutDto>(total, items);
+    }
+
+    /// <summary>
+    /// 坐席话务统计分析明细导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("seats/list/export")]
+    [LogFilterAlpha("导出日志")]
+    public async Task<FileStreamResult> QuerySeatCallsListExportAsync([FromBody] ExportExcelDto<BiQueryCallsListDto> dto)
+    {
+        var (total, data) = await _callReportApplication.QuerySeatCallsListAsync(dto.QueryDto, dto.IsExportAll);
+
+        dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass<CallRecordOutDto>(dto.ColumnInfos);
+
+        var dtos = data
+            .Select(stu => _mapper.Map(stu, typeof(CallRecordOutDto), dynamicClass))
+            .Cast<object>()
+            .ToList();
+
+        var stream = ExcelHelper.CreateStream(dtos);
+
+
+        return ExcelStreamResult(
+            _exportApplication.GetExcelStream(
+            dto,
+            (await _callReportApplication.QuerySeatCallsListAsync(dto.QueryDto, dto.IsExportAll)).Item2
+            ),
+            "坐席话务统计分析明细");
+    }
+
 
     /// <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/UserExamController.cs

@@ -67,6 +67,17 @@ namespace Hotline.Api.Controllers.Exam
             return await _userExamService.GetExamQuestionViewResponses(examQuestionGroupRequest);
         }
 
+        /// <summary>
+        /// 获取用户列表
+        /// </summary>
+        /// <param name="examUserQueryRequest"></param>
+        /// <returns></returns>
+        [HttpGet(UserExamApiRoute.GetUserList)]
+        public async Task<List<ExamUserViewResponse>> GetUserList([FromQuery] ExamUserQueryRequest examUserQueryRequest)
+        {
+            return await _userExamService.GetUserListAsync(examUserQueryRequest);
+        }
+
         /// <summary>
         /// 获取试题详情
         /// </summary>

+ 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/UserExamApiRoute.cs

@@ -40,5 +40,7 @@ namespace Hotline.Application.Exam.Constants.ApiRoutes
         public const string GetUserExamResults = "GetUserExamResults";
 
         public const string GetGradingPagedList = "GetGradingPagedList";
+
+        public const string GetUserList = "GetUserList";
     }
 }

+ 7 - 0
src/Hotline.Application/Exam/Interface/ExamManages/IUserExamService.cs

@@ -110,5 +110,12 @@ namespace Hotline.Application.Exam.Interface.ExamManages
         /// <param name="requestAborted"></param>
         /// <returns></returns>
         Task BatchGradingAsync(BatchGradingExamItemDto batchGradingExamItemDto, CancellationToken requestAborted);
+
+        /// <summary>
+        /// 获取用户列表
+        /// </summary>
+        /// <param name="examUserQueryRequest"></param>
+        /// <returns></returns>
+        Task<List<ExamUserViewResponse>> GetUserListAsync(ExamUserQueryRequest examUserQueryRequest);
     }
 }

+ 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;
         }

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

@@ -110,7 +110,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             if (questionDto != null)
             {
                 questionDto.ExamQuestionScoreDtos = await GetExamQuestionScores(entityQueryRequest, entity);
-                if (entity.Mode == Share.Enums.Exams.EExamMode.Random)
+                if (entity.Mode == Share.Enums.Exams.EMethod.Random)
                 {
                     questionDto.TestPaperId = entity.ExtractRuleId;
 
@@ -169,8 +169,8 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             await GenerateExamQuestion(new GenerateExamTestPaperRequest
             {
-                TestPaperId = actionRequest.Mode == Share.Enums.Exams.EExamMode.Manual ? actionRequest.TestPaperId : null,
-                ExtractRuleId = actionRequest.Mode == Share.Enums.Exams.EExamMode.Random ? actionRequest.TestPaperId : null,
+                TestPaperId = actionRequest.Mode == Share.Enums.Exams.EMethod.Unified ? actionRequest.TestPaperId : null,
+                ExtractRuleId = actionRequest.Mode == Share.Enums.Exams.EMethod.Random ? actionRequest.TestPaperId : null,
                 ExamManageId = id
             }, cancellationToken);
 
@@ -179,7 +179,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
         private void ResolveRandomExtractRuleId(AddExamManageDto actionRequest)
         {
-            if (actionRequest.Mode == Share.Enums.Exams.EExamMode.Random)
+            if (actionRequest.Mode == Share.Enums.Exams.EMethod.Random)
             {
                 base.Entity.ExtractRuleId = actionRequest.TestPaperId;
                 base.Entity.TestPaperId = string.Empty;
@@ -207,8 +207,8 @@ namespace Hotline.Application.Exam.Service.ExamManages
             await base.Complete(base.Entity, OperationConstant.Update);
 
             await GenerateExamQuestion(new GenerateExamTestPaperRequest { 
-                TestPaperId = actionRequest.Mode == Share.Enums.Exams.EExamMode.Manual? actionRequest.TestPaperId:null,
-                ExtractRuleId = actionRequest.Mode == Share.Enums.Exams.EExamMode.Random ? actionRequest.TestPaperId : null,
+                TestPaperId = actionRequest.Mode == Share.Enums.Exams.EMethod.Unified? actionRequest.TestPaperId:null,
+                ExtractRuleId = actionRequest.Mode == Share.Enums.Exams.EMethod.Random ? actionRequest.TestPaperId : null,
                 ExamManageId = actionRequest.Id
             }, cancellationToken);
 
@@ -300,7 +300,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             if (examManage != null)
             {
                 var questions = new List<ExamQuestion>();
-                if (examManage.Mode == Share.Enums.Exams.EExamMode.Random)
+                if (examManage.Mode == Share.Enums.Exams.EMethod.Random)
                 {
                     var tagQuestionCounts = await GetTagQuestions(examManage);
                     questions = await GetQuestions(tagQuestionCounts);
@@ -570,7 +570,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 Score = q.Score
             });
 
-            if(examManage.Mode == Share.Enums.Exams.EExamMode.Random)
+            if(examManage.Mode == Share.Enums.Exams.EMethod.Random)
             {
                 var tagQuestionRepository = new ExamRepository<ExamTagQuestion>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
                 var tagQuestionTable = tagQuestionRepository.Queryable().Where(x => x.RuleId == examManage.ExtractRuleId);
@@ -663,7 +663,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 var questionQuerables = new List<ISugarQueryable<ExamQuestion>>();
                 tagQuestionCounts.ForEach(item =>
                 {
-                    ISugarQueryable<ExamQuestion> queryable = questionTable.InnerJoin(questionTagTable, (q, t) => t.Id == t.QuestionId)
+                    ISugarQueryable<ExamQuestion> queryable = questionRepository.Queryable().InnerJoin(questionTagTable, (q, t) => q.Id == t.QuestionId)
                         .Where((q, t) => q.QuestionType == item.QuestionType && t.TagId == item.TagId).Take(item.Count).Select((q, t) => q);
 
                     questionQuerables.Add(queryable);

+ 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;
                 });

+ 178 - 22
src/Hotline.Application/Exam/Service/ExamManages/UserExamService.cs

@@ -40,6 +40,8 @@ using SqlSugar;
 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
 {
@@ -49,6 +51,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
         private readonly IUserExamItemRepository _userExamItemRepository;
         private readonly IUserExamItemOptionRepository _userExamItemOptionRepository;
         private readonly IExamAnswerRepository _examAnswerRepository;
+        private readonly IExamManageRepository _examManageRepository;
         private readonly IDataPermissionFilterBuilder _dataPermissionFilterBuilder;
         private readonly IServiceProvider _serviceProvider;
         private readonly IMapper _mapper;
@@ -59,6 +62,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             IUserExamItemRepository userExamItemRepository,
             IUserExamItemOptionRepository userExamItemOptionRepository,
             IExamAnswerRepository examAnswerRepository,
+            IExamManageRepository examManageRepository,
             IDataPermissionFilterBuilder dataPermissionFilterBuilder, IServiceProvider serviceProvider,
             IMapper mapper, ISessionContext sessionContext) : base(repository, mapper, sessionContext)
         {
@@ -66,6 +70,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             this._userExamItemRepository = userExamItemRepository;
             this._userExamItemOptionRepository = userExamItemOptionRepository;
             this._examAnswerRepository = examAnswerRepository;
+            this._examManageRepository = examManageRepository;
             this._dataPermissionFilterBuilder = dataPermissionFilterBuilder;
             this._serviceProvider = serviceProvider;
             this._mapper = mapper;
@@ -192,7 +197,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
         public async Task<List<GradingQuestionViewResponce>> GetGradingQuestionViewResponces(ExamQuestionGroupRequest examQuestionGroupRequest)
         {
             var expression = examQuestionGroupRequest.GetExpression();
-            var examManageTable = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable().Where(expression);
+            var examManageTable = _examManageRepository.Queryable().Where(expression);
             var testPaperItemTable = new ExamRepository<Exams.ExamManages.ExamQuestionBak>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
 
             var queryable = await examManageTable.InnerJoin(testPaperItemTable, (e, i) => e.Id == i.ExamId)
@@ -255,7 +260,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
             {
                 userExamItem = _mapper.Map<GradingExamItemDto, ExamUserExamItem>(gradingExtamItemDto, userExamItem);
 
-                await _userExamItemRepository.ValidateUpdateAsync(userExamItem, cancellationToken);
+                await _userExamItemRepository.UpdateWithValidateAsync(userExamItem, cancellationToken);
             }
 
             return await GetNextExamQuestion(gradingExtamItemDto);
@@ -269,7 +274,25 @@ namespace Hotline.Application.Exam.Service.ExamManages
             {
                 userExam = _mapper.Map<SubmitExamDto, ExamUserExam>(submitExamDto, userExam);
 
-                await _repository.ValidateUpdateAsync(userExam, cancellationToken);
+                userExam.ExamStatus = EExamStatus.Complete;
+
+                await _repository.UpdateWithValidateAsync(userExam, cancellationToken);
+
+                await CompleteExamManage(userExam.ExamId, cancellationToken);
+            }
+        }
+
+        private async Task CompleteExamManage(string examId, CancellationToken cancellationToken)
+        {
+            var userExams = await _repository.Queryable().Where(x => x.ExamId == examId).ToListAsync();
+
+            if (userExams.All(x => x.IsSubmit))
+            {
+                var examManage = await _examManageRepository.GetAsync(x=>x.Id == examId);
+
+                examManage.ExamStatus = EExamStatus.Complete;
+
+                await _examManageRepository.UpdateWithValidateAsync(examManage, cancellationToken);
             }
         }
 
@@ -277,7 +300,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
         {
             var userExam = await new ExamRepository<ExamUserExam>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable().Where(x => x.Id == addUserExamItemDto.UserExamId).FirstAsync();
 
-            var startExamViewResponse = await CheckExamValid(userExam);
+            var startExamViewResponse = await CheckExamValid(userExam, cancellationToken);
 
             if (!startExamViewResponse.CheckValidate())
             {
@@ -311,20 +334,23 @@ namespace Hotline.Application.Exam.Service.ExamManages
             if (userExam.StartTime == null)
                 userExam.StartTime = DateTime.Now;
 
-            var startExamViewResponse = await CheckExamValid(userExam);
+            var startExamViewResponse = await CheckExamValid(userExam, cancellationToken);
 
             if (!startExamViewResponse.CheckValidate())
             {
                 return startExamViewResponse;
             }
 
-            userExam.ExamStatus = Share.Enums.Exams.EExamStatus.Executing;
+            if (userExam.ExamStatus == EExamStatus.NoStart)
+            {
+                userExam.ExamStatus = Share.Enums.Exams.EExamStatus.Executing;
 
-            userExam.ToUpdate(_sessionContext);
+                userExam.ToUpdate(_sessionContext);
 
-            await _repository.UpdateWithValidateAsync(userExam, cancellationToken);
+                await _repository.UpdateWithValidateAsync(userExam, cancellationToken);
 
-            var examManage = await new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider).GetAsync(x => x.Id == userExam.ExamId);
+            }
+            var examManage = await _examManageRepository.GetAsync(x => x.Id == userExam.ExamId);
 
             return new StartExamViewResponse
             {
@@ -334,11 +360,10 @@ namespace Hotline.Application.Exam.Service.ExamManages
             };
         }
 
-        private async Task<StartExamViewResponse> CheckExamValid(ExamUserExam examUserExam)
+        private async Task<StartExamViewResponse> CheckExamValid(ExamUserExam examUserExam, CancellationToken cancellationToken)
         {
-            var examManageRepository = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider);
 
-            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
             {
@@ -365,9 +390,25 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             startExamViewResponse.IsStart = true;
 
+            if (examManage.ExamStatus== EExamStatus.NoStart)
+            {
+                await UpdateExamStatus(examManage, cancellationToken);
+            }
+
+            startExamViewResponse.TimeSpan = examManage?.TimeSpan ?? 0;
+            startExamViewResponse.StartTime = examManage?.StartTime;
             return startExamViewResponse;
         }
 
+        private async Task UpdateExamStatus(ExamManage? examManage, CancellationToken cancellationToken)
+        {
+            examManage.ExamStatus = EExamStatus.Executing;
+
+            examManage.ToUpdate(_sessionContext);
+
+            await _examManageRepository.UpdateWithValidateAsync(examManage, cancellationToken);
+        }
+
         private StartExamViewResponse AddCheckValidateCountStrategy(ExamUserExam examUserExam, ExamManage examManage, StartExamViewResponse startExamViewResponse, List<IExamStrategy> examStrategys)
         {
             if (examManage.ExamType == Share.Enums.Exams.EExamType.Simulate)
@@ -489,7 +530,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
                 userExam.ToUpdate(_sessionContext);
 
-                await _repository.ValidateUpdateAsync(userExam, cancellationToken);
+                await _repository.UpdateWithValidateAsync(userExam, cancellationToken);
             }
         }
 
@@ -509,13 +550,13 @@ namespace Hotline.Application.Exam.Service.ExamManages
             var questionScoreTable = questionScoreRepository.Queryable();
 
             var queryable = userExamTable.InnerJoin(userExamItemTable, (e, i) => e.Id == i.UserExamId)
-                .InnerJoin(questionTable, (e, i, q) => i.QuestionId == q.Id)
+                .InnerJoin(questionTable, (e, i, q) => i.QuestionId == q.QuestionId)
                 .LeftJoin(userExamItemOptionTable, (e, i, q, o) => i.Id == o.UserExamItemId)
                 .LeftJoin(quesitonOptionTable, (e, i, q, o, qo) => o.QuestionOptionId == qo.Id)
                 .LeftJoin(examAnswerTable, (e, i, q, o, qo, a) => i.Id == a.UserExamItemId)
                 .LeftJoin(testPaperItemAnswerTable, (e, i, q, o, qo, a, ta) => ta.QuestionId == qo.QuestionId)
                 .InnerJoin(questionScoreTable, (e, i, q, o, qo, a, ta, s) => q.QuestionType == s.QuestionType && e.ExamId == s.ExamManageId)
-            .Where((e, i, q, o, qo, a, ta, s) => q.QuestionType == EQuestionType.Single || q.QuestionType == EQuestionType.Multi || q.QuestionType == EQuestionType.Judge)
+            .Where((e, i, q, o, qo, a, ta, s) => !(q.QuestionType == EQuestionType.Single || q.QuestionType == EQuestionType.Multi || q.QuestionType == EQuestionType.Judge))
             .Select(
             (e, i, q, o, qo, a, ta, s) => new GradingExamQuestionTempDto
             {
@@ -547,7 +588,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
                 Id = g.Key.Id,
                 Score = g.FirstOrDefault().Score,
                 Title = g.FirstOrDefault().Title,
-                CorrectAnswer = g.Key.QuestionType.CheckSelectType() ? string.Join(",", g.Where(i => i.IsAnswer).Select(n => n.Label)) : g.FirstOrDefault()?.CorrectAnswer
+                CorrectAnswer = g.Key.QuestionType.CheckSelectType() ? string.Join(",", g.Where(i => i.IsAnswer).Select(n => n.Label).Distinct()) : g.FirstOrDefault()?.CorrectAnswer
             }).ToList();
 
             return gradingExamQuestionDtos;
@@ -636,7 +677,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);
+            var userExamTable =  _repository.Queryable().WhereIF(gradingPagedRequest.IsCheck!=null, x => x.IsCheck == gradingPagedRequest.IsCheck && x.IsSubmit);
 
             var examManageTable = new ExamRepository<ExamManage>(_uow, _dataPermissionFilterBuilder, _serviceProvider).Queryable();
 
@@ -680,10 +721,51 @@ 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);
+
+                    updateUserExamItems.Add(updateUserExamItem);
+                });
+
+                updateUserExamItems.ToUpdate(_sessionContext);
+
+                await _userExamItemRepository.UpdateWithValidateAsync(updateUserExamItems, cancellationToken);
+
+                var userExamId = userExamItems.FirstOrDefault()?.UserExamId;
+                // 计算本次考试得分
+                var userExamItemsInCheck = await _userExamItemRepository.Queryable().Where(x => x.UserExamId == userExamId).ToListAsync();
+                var updateExamItemDTOs = new List<UpdateUserExamItemDto>();
+                userExamItemsInCheck.ForEach(x =>
+                {
+                    var updateUserExamItem = _mapper.Map<ExamUserExamItem, UpdateUserExamItemDto>(x);
+
+                    updateExamItemDTOs.Add(updateUserExamItem);
+                });
+                await CalcuteExamItemScore(_userExamItemRepository, updateExamItemDTOs,cancellationToken);
+
+                await CalcuteTotalScore(_userExamItemRepository, userExamId, cancellationToken);
 
-                await _userExamItemRepository.ValidateUpdateAsync(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
+            {
+                ExamId = ux.ExamId,
+                UserId = ux.UserId,
+                UserName = u.Name,
+                Id = ux.Id
+            }).ToListAsync();
+
+            return queryable;
         }
         #endregion
 
@@ -720,6 +802,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);
@@ -881,7 +1037,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
                     examAnswer.ToUpdate(_sessionContext);
 
-                    await _examAnswerRepository.ValidateUpdateAsync(examAnswer, cancellationToken);
+                    await _examAnswerRepository.UpdateWithValidateAsync(examAnswer, cancellationToken);
 
                     examAnswers.Add(examAnswer);
                 }
@@ -962,7 +1118,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
                 entities.ToUpdate(_sessionContext);
 
-                await _userExamItemOptionRepository.ValidateUpdateAsync(entities, cancellationToken);
+                await _userExamItemOptionRepository.UpdateWithValidateAsync(entities, cancellationToken);
             }
 
 
@@ -978,7 +1134,7 @@ namespace Hotline.Application.Exam.Service.ExamManages
 
             userExamItem.ToUpdate(_sessionContext);
 
-            await _userExamItemRepository.ValidateUpdateAsync(userExamItem, cancellationToken);
+            await _userExamItemRepository.UpdateWithValidateAsync(userExamItem, cancellationToken);
 
             if (updateUserExamItemDto.QuestionType.CheckSelectType())
             {

+ 3 - 0
src/Hotline.Application/Exam/Service/Practices/PracticeService.cs

@@ -38,6 +38,7 @@ using XF.Domain.Authentications;
 using XF.Domain.Dependency;
 using Hotline.Repository.SqlSugar.Exam.Service;
 using Hotline.Repository.SqlSugar.Exam.Extensions;
+using Exam.Infrastructure.Extensions;
 
 namespace Hotline.Application.Exam.Service.Practices
 {
@@ -321,6 +322,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);

+ 1 - 1
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);
 

+ 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)

+ 23 - 17
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);
 
         }
 
@@ -144,6 +144,7 @@ namespace Hotline.Application.Exam.Service.Trains
             if (trainPracticeKnowladge != null)
             {
                 addTrainDto.TrainKnowladgeId = trainPracticeKnowladge.Id;
+                addTrainDto.QuestionId = trainPracticeKnowladge.QuestionId;
             }
         }
 
@@ -275,7 +276,7 @@ namespace Hotline.Application.Exam.Service.Trains
 
             trainRecordItem.ToInsert(_sessionContext);
 
-            await _trainRecordItemRepository.ValidateAddAsync(trainRecordItem, cancellationToken);
+            await _trainRecordItemRepository.AddWithValidateAsync(trainRecordItem, cancellationToken);
         }
 
         private SqlSugar.ISugarQueryable<TrainRecordViewResponse> QueryResult(TrainRecordPagedRequest queryRequest)
@@ -363,27 +364,30 @@ namespace Hotline.Application.Exam.Service.Trains
         }
        
 
-        private async Task AddTrainRecordAnswer(AddTrainDto addTrainRecordDto, CancellationToken cancellationToken)
+        private async Task AddTrainRecordAnswer(AddTrainDto addTrainDto, CancellationToken cancellationToken)
         {
-            if (addTrainRecordDto.AddTrainRecordOptionDtos == null && addTrainRecordDto.AddTrainRecordAnswerDto == null) return;
+            if (addTrainDto.AddTrainRecordOptionDtos == null && addTrainDto.AddTrainRecordAnswerDto == null) return;
 
             var trainRecordAnswers = new List<ExamTrainRecordAnswer>();
 
-            addTrainRecordDto.AddTrainRecordOptionDtos?.ForEach(item =>
+            addTrainDto.AddTrainRecordOptionDtos?.ForEach(item =>
             {
-                item.TrainRecordId = addTrainRecordDto.TrainRecordId;
-                trainRecordAnswers.Add(_mapper.Map<ExamTrainRecordAnswer>(item));
+                item.TrainRecordId = addTrainDto.TrainRecordId;
+                var traiRecordAnswer = _mapper.Map<ExamTrainRecordAnswer>(item);
+                traiRecordAnswer.QuestionId = addTrainDto.QuestionId;
+                trainRecordAnswers.Add(traiRecordAnswer);
             });
 
-            if (addTrainRecordDto.AddTrainRecordAnswerDto != null)
+            if (addTrainDto.AddTrainRecordAnswerDto != null)
             {
-                addTrainRecordDto.AddTrainRecordAnswerDto.TrainRecordId = addTrainRecordDto.TrainRecordId;
-                trainRecordAnswers.Add(_mapper.Map<ExamTrainRecordAnswer>(addTrainRecordDto.AddTrainRecordAnswerDto));
+                addTrainDto.AddTrainRecordAnswerDto.TrainRecordId = addTrainDto.TrainRecordId;
+                addTrainDto.AddTrainRecordAnswerDto.QuestionId = addTrainDto.QuestionId;
+                trainRecordAnswers.Add(_mapper.Map<ExamTrainRecordAnswer>(addTrainDto.AddTrainRecordAnswerDto));
             }
 
             trainRecordAnswers.ToInsert(_sessionContext);
 
-            await _trainRecordAnswerRepository.ValidateAddAsync(trainRecordAnswers,cancellationToken);
+            await _trainRecordAnswerRepository.AddWithValidateAsync(trainRecordAnswers,cancellationToken);
         }
 
         //private async Task<TrainPracticeDto> GetTrainQuestionOptions(AddTrainDto addTrainDto)
@@ -441,12 +445,14 @@ namespace Hotline.Application.Exam.Service.Trains
                 .Where((tpo, tp)=> tp.Id == trainPracticeRequest.TrainPracticeId)
                 .Select((tpo, tp) => tpo).ToListAsync();
 
-            var trainRecordOptions = await 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)=> x.Id == tp.TrainTemplateId)
+            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)
+                .Select((t, x, p, tp) => t);
+
+            var trainRecordOptions = await queryable
                 .ToListAsync();
 
             var trainPracticeOptionsDtos = new List<SimpleTrainPracticeOptionsDto>();

+ 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);
 
-	}
+    }
 }

+ 115 - 0
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;
@@ -4571,6 +4574,118 @@ 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)
+             .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)
+            .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>

+ 63 - 1
src/Hotline.Application/StatisticalReport/CallReport/CallReportApplicationBase.cs

@@ -19,6 +19,8 @@ using Hotline.Share.Tools;
 using JiebaNet.Segmenter.Common;
 using XingTang.Sdk;
 using Mapster;
+using NPOI.SS.Formula.Functions;
+using System.Linq;
 
 namespace Hotline.Application.StatisticalReport.CallReport;
 
@@ -146,9 +148,69 @@ 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)
+        {
+            return (0, await query.ToListAsync());
+        }
+
+        return await query.ToPagedListAsync(dto.PageIndex, dto.PageSize);
+    }
+
+    /// <summary>
+    /// 话务日期明细-呼入总量/接通总量
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    public virtual async Task<(int, List<CallRecordOutDto>)> QuerySeatCallsListAsync(BiQueryCallsListDto dto, bool isAll)
+    {
+        //超时接通量
+        int CallInOverConnRingTime = _systemSettingCacheManager.CallInOverConnRingTime;
+        //坐席超时挂断时间
+        int SeatChaoTime = _systemSettingCacheManager.SeatChaoTime;
+        //未接秒挂时间
+        int noConnectByeTimes = _systemSettingCacheManager.NoConnectByeTimes;
+        //呼入有效时间
+        int effectiveTimes = _systemSettingCacheManager.EffectiveTimes;
+        //接通秒挂时间
+        int connectByeTimes = _systemSettingCacheManager.ConnectByeTimes;
+        var setting = _systemSettingCacheManager.GetSetting(SettingConstants.RoleZuoXi).SettingValue;
+
+        var recordPrefix = _systemSettingCacheManager.RecordPrefix;
+        var query = _callNativeRepository.Queryable(includeDeleted: true)
+                .LeftJoin<Order>((p, o) => p.Id == o.CallId)
+                .RightJoin<User>((p, o, u) => p.UserId == u.Id && !u.IsDeleted)
+                .Where((p, o) => p.BeginIvrTime >= dto.StartTime && p.BeginIvrTime <= dto.EndTime && p.CallState != ECallState.Invalid)
+                .Where((p, o, u) => u.Roles.Any(x => setting.Contains(x.Name)))
+                .WhereIF(!string.IsNullOrEmpty(dto.UserId), (p, o) => p.UserId == dto.UserId)
+                .WhereIF(!string.IsNullOrEmpty(dto.UserName), (p, o) => p.UserName == dto.UserName)
+                .WhereIF(dto.FieldName == "inTotal", (p, o) => p.Direction == ECallDirection.In)                                                                                   //呼入总量
+                .WhereIF(dto.FieldName == "inAnswered", (p, o) => p.Direction == ECallDirection.In && p.AnsweredTime != null)                                                      //接通总量
+                .WhereIF(dto.FieldName == "inHangupImmediate", (p, o) => p.Direction == ECallDirection.In && p.AnsweredTime == null && p.RingDuration < noConnectByeTimes)         //呼入秒挂
+                .WhereIF(dto.FieldName == "inHanguped", (p, o) => p.Direction == ECallDirection.In && p.AnsweredTime == null && p.RingDuration >= noConnectByeTimes)               //呼入超时未接
+                .WhereIF(dto.FieldName == "inAvailableAnswer", (p, o) => p.Direction == ECallDirection.In && p.AnsweredTime != null && p.Duration >= effectiveTimes)               //有效接通量
+                .WhereIF(dto.FieldName == "inHangupImmediateWhenAnswered", (p, o) => p.Direction == ECallDirection.In && p.AnsweredTime != null && p.Duration < connectByeTimes)   //接通秒挂
+                .WhereIF(dto.FieldName == "outTotal", (p, o) => p.Direction == ECallDirection.Out)                                                                                 //呼出总量
+                .WhereIF(dto.FieldName == "outAnswered", (p, o) => p.Direction == ECallDirection.Out && p.AnsweredTime != null)                                                    //呼出接通量
+                .OrderByDescending((p, o) => p.BeginIvrTime)
+                .Select((p, o, u) => new CallRecordOutDto
+                {
+                    OtherAccept = p.Id,
+                    OrderId = o.Id,
+                    OrderNo = o.No,
+                    OrderTitle = o.Title,
+                    Cdpn = p.ToNo,
+                    Cpn = p.FromNo,
+                    RecordingFileUrl = recordPrefix + p.AudioFile,
+                    RecordingFileName = p.AudioFile,
+                    RecordingBaseAddress = recordPrefix,
+                    RecordingAbsolutePath = p.AudioFile,
+                    CreatedTime = p.CreationTime
+                }, true);
+
         if (isAll)
         {
             return (0, await query.ToListAsync());

+ 53 - 1
src/Hotline.Application/StatisticalReport/CallReport/YiBinCallReportApplication.cs

@@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Http;
 using Hotline.Share.Dtos;
 using MapsterMapper;
 using System.Threading;
+using NPOI.SS.Formula.Functions;
 
 namespace Hotline.Application.StatisticalReport.CallReport;
 
@@ -126,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)
         {
@@ -135,6 +137,56 @@ public class YiBinCallReportApplication : CallReportApplicationBase, ICallReport
         return await query.ToPagedListAsync(dto.PageIndex, dto.PageSize);
     }
 
+    /// <summary>
+    /// 话务日期明细-呼入总量/接通总量
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    public virtual async Task<(int, List<CallRecordOutDto>)> QuerySeatCallsListAsync(BiQueryCallsListDto dto, bool isAll)
+    {
+        //获取配置
+        int noConnectByeTimes = _systemSettingCacheManager.NoConnectByeTimes;
+        int effectiveTimes = _systemSettingCacheManager.EffectiveTimes;
+        int connectByeTimes = _systemSettingCacheManager.ConnectByeTimes;
+        int ringTims = _systemSettingCacheManager.RingTimes;
+        var setting = _systemSettingCacheManager.GetSetting(SettingConstants.RoleZuoXi).SettingValue;
+
+        var recordPrefix = _systemSettingCacheManager.RecordPrefix;
+        var query = _trCallRecordRepository.Queryable()
+                 .Includes(p => p.Order)
+                 .LeftJoin<User>((p, u) => p.UserId == u.Id && !u.IsDeleted)
+                 .Where((p, u) => p.CreatedTime >= dto.StartTime && p.CreatedTime <= dto.EndTime)
+                 .Where((p, u) => u.Roles.Any(x => setting.Contains(x.Name)))
+                 .WhereIF(!string.IsNullOrEmpty(dto.UserId), (p, u) => p.UserId == dto.UserId)
+                 .WhereIF(!string.IsNullOrEmpty(dto.UserName), (p, u) => p.UserName == dto.UserName)
+                 .WhereIF(dto.FieldName == "inTotal", (p, u) => p.CallDirection == ECallDirection.In)                                                                                   //呼入总量
+                 .WhereIF(dto.FieldName == "inAnswered", (p, u) => p.CallDirection == ECallDirection.In && p.AnsweredTime != null)                                                      //接通总量
+                 .WhereIF(dto.FieldName == "inHangupImmediate", (p, u) => p.CallDirection == ECallDirection.In && p.AnsweredTime == null && p.RingTimes < noConnectByeTimes)            //呼入秒挂
+                 .WhereIF(dto.FieldName == "inHanguped", (p, u) => p.CallDirection == ECallDirection.In && p.AnsweredTime == null && p.RingTimes > ringTims)                            //呼入超时未接
+                 .WhereIF(dto.FieldName == "inAvailableAnswer", (p, u) => p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.Duration >= effectiveTimes)               //有效接通量
+                 .WhereIF(dto.FieldName == "inHangupImmediateWhenAnswered", (p, u) => p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.Duration < connectByeTimes)   //接通秒挂
+                 .WhereIF(dto.FieldName == "outTotal", (p, u) => p.CallDirection == ECallDirection.Out)                                                                                 //呼出总量
+                 .WhereIF(dto.FieldName == "outAnswered", (p, u) => p.CallDirection == ECallDirection.Out && p.AnsweredTime != null)                                                    //呼出接通量
+                 .OrderByDescending((p, u) => p.BeginIvrTime)
+                 .Select(p => new CallRecordOutDto()
+                 {
+                     FromNo = p.CPN,
+                     ToNo = p.CDPN,
+                     Direction = p.CallDirection,
+                     EndTime = p.OverTime,
+                     OrderId = p.Order.Id,
+                     OrderTitle = p.Order.Title,
+                     OrderNo = p.Order.No,
+                     CreatedTime = p.CreationTime
+                 }, true);
+
+        if (isAll)
+        {
+            return (0, await query.ToListAsync());
+        }
+        return await query.ToPagedListAsync(dto.PageIndex, dto.PageSize);
+    }
+
     /// <summary>
     /// 坐席话务统计分析
     /// </summary>

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

@@ -26,6 +26,13 @@ namespace Hotline.Application.StatisticalReport
         /// <returns></returns>
         Task<(int, List<CallRecordOutDto>)> QueryCallsDetailInTotalAsync(BiQueryCallsDto dto, bool isAll);
 
+        /// <summary>
+        /// 坐席话务统计分析明细
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task<(int, List<CallRecordOutDto>)> QuerySeatCallsListAsync(BiQueryCallsListDto dto, bool isAll);
+
         /// <summary>
         /// 坐席话务统计分析
         /// </summary>

+ 3 - 2
src/Hotline.Repository.SqlSugar/Exam/Validators/ExamManages/ExamManageValidator.cs

@@ -45,16 +45,17 @@ namespace Exam.Application
 
             RuleFor(m => m.Name).NotEmpty().WithMessage(x=>string.Format(ExamErrorMessage.IsRequired,x.GetType().GetDescription(nameof(ExamManage.Name))));
             RuleFor(m => m.Code).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.Code))));
-            RuleFor(m => m.CutoffScore).NotNull().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.CutoffScore))));
+            RuleFor(m => m.CutoffScore).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.CutoffScore))));
             RuleFor(m => m.ExamType).NotNull().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.ExamType))));
             RuleFor(m => m.Method).NotNull().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.Method))));
-            RuleFor(m => m.TestPaperId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, typeof(ExamTestPaper).GetDescription()));
+            //RuleFor(m => m.TestPaperId).NotEmpty().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, typeof(ExamTestPaper).GetDescription()));
             RuleFor(m => m.Count).NotNull().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.Count))));
             RuleFor(m => m.TimeSpan).NotNull().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.TimeSpan))));
             RuleFor(m => m.StartTime).NotNull().WithMessage(x => string.Format(ExamErrorMessage.IsRequired, x.GetType().GetDescription(nameof(ExamManage.StartTime))));
             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()

+ 18 - 0
src/Hotline.Share/Dtos/CallCenter/BiQueryCallsDto.cs

@@ -19,6 +19,24 @@ public record BiQueryCallsDto : ReportRequiredPagedRequest
     public string? TypeCode { get; set; } = "1";
 }
 
+public record BiQueryCallsListDto : ReportRequiredPagedRequest
+{
+    /// <summary>
+    /// 用户Id
+    /// </summary>
+    public string? UserId { get; set; }
+
+    /// <summary>
+    /// 用户名称
+    /// </summary>
+    public string? UserName { get; set; }
+
+    /// <summary>
+    /// 字段类型
+    /// </summary>
+    public string? FieldName { get; set; }
+}
+
 public record QueryCallListDto : PagedRequest
 {
     public DateTime StartTime { get; set; }

+ 1 - 1
src/Hotline.Share/Dtos/ExamManages/ExamManageDto.cs

@@ -52,7 +52,7 @@ namespace Exam.Share
         /// 组卷方式
         /// </summary>
         [Description("组卷方式")]
-        public EExamMode Mode { get; set; }
+        public EMethod Mode { get; set; }
 
         /// <summary>
         /// 考核方式

+ 1 - 1
src/Hotline.Share/Dtos/ExamManages/ExamQuestionDto.cs

@@ -26,7 +26,7 @@ namespace Hotline.Share.Dtos.ExamManages
         /// 分数
         /// </summary>
         [Description("分数")]
-        public int Score { get; set; }
+        public int? Score { get; set; }
 
         /// <summary>
         /// 考试选项

+ 1 - 1
src/Hotline.Share/Dtos/ExamManages/GradingExamQuestionDto.cs

@@ -95,7 +95,7 @@ namespace Exam.Application.Interface.Exam
         /// 分数
         /// </summary>
         [Description("分数")]
-        public int Score { get; set; }
+        public int? Score { get; set; }
     }
 
     public class UserExamQuestionDto:IActionRequest

+ 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>
         /// 开始等待时间

+ 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>

+ 7 - 0
src/Hotline.Share/Dtos/Trains/TrainRecordDto.cs

@@ -128,6 +128,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>
         /// 培训习题Id
         /// </summary>

+ 1 - 1
src/Hotline.Share/Enums/Exams/EDifficultyLevel.cs

@@ -16,7 +16,7 @@ namespace Hotline.Share.Enums.Exams
         /// <summary>
         /// 正常
         /// </summary>
-        [Description("正常")]
+        [Description("适中")]
         Normal=1,
         /// <summary>
         /// 难

+ 7 - 2
src/Hotline.Share/Enums/Exams/EMethod.cs

@@ -8,9 +8,14 @@ namespace Hotline.Share.Enums.Exams
     public enum EMethod
     {
         /// <summary>
-        /// 统一
+        /// 统一
         /// </summary>
         [Description("统一阅卷")]
-        Unified=0
+        Unified = 0,
+        /// <summary>
+        /// 随机试题
+        /// </summary>
+        [Description("随机试题")]
+        Random = 1,
     }
 }

+ 1 - 1
src/Hotline.Share/Requests/Exam/ExamManagePagedRequest.cs

@@ -16,7 +16,7 @@ namespace Hotline.Share.Requests.Exam
         /// 考核方式
         /// </summary>
         [Description("考核方式")]
-        public EExamMode? Mode { get; set; }
+        public EMethod? Mode { get; set; }
 
         /// <summary>
         /// 考试编号

+ 9 - 0
src/Hotline.Share/Requests/Exam/ExamQuestionGroupRequest.cs

@@ -17,4 +17,13 @@ namespace Hotline.Share.Requests.Exam
         [Description("用户ID")]
         public string UserId { get; set; }
     }
+
+    public class ExamUserQueryRequest : IQueryRequest
+    {
+        /// <summary>
+        /// 考试Id
+        /// </summary>
+        [Description("考试Id")]
+        public string ExamId { 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; }
     }
 }

+ 4 - 4
src/Hotline.Share/ViewResponses/Exam/ExamManageViewResponse.cs

@@ -48,13 +48,13 @@ namespace Exam.Share.ViewResponses.Exam
                 return Mode.GetDescription();
             }
         }
-       
+
 
         /// <summary>
-        /// 组卷方式
+        /// 考核方式
         /// </summary>
-        [Description("组卷方式")]
-        public EExamMode Mode { get; set; }
+        [Description("考核方式")]
+        public EMethod Mode { get; set; }
 
         /// <summary>
         /// 试卷Id

+ 28 - 0
src/Hotline.Share/ViewResponses/Exam/UnExamUserViewResponse.cs

@@ -30,4 +30,32 @@ namespace Exam.Share.ViewResponses.Exam
 
         public string Id { get; set; }
     }
+
+    public class ExamUserViewResponse : IViewResponse
+    {
+        /// <summary>
+        /// 缺考人员
+        /// </summary>
+        [Description("缺考人员")]
+        public string UserName { get; set; }
+
+        /// <summary>
+        /// 考试Id
+        /// </summary>
+        [Description("考试Id")]
+        public string ExamId { get; set; }
+
+        /// <summary>
+        /// 用户Id
+        /// </summary>
+        [Description("用户Id")]
+        public string UserId { get; set; }
+
+        /// <summary>
+        /// 主键
+        /// </summary>
+        [Description("主键")]
+
+        public string Id { get; set; }
+    }
 }

+ 1 - 1
src/Hotline/Exams/ExamManages/ExamManage.cs

@@ -44,7 +44,7 @@ namespace Hotline.Exams.ExamManages
         /// </summary>
         [SugarColumn(ColumnDescription = "组卷方式")]
         [Description("组卷方式")]
-        public EExamMode Mode { get; set; }
+        public EMethod Mode { get; set; }
 
         /// <summary>
         /// 试卷Id