Browse Source

新增话务统计分析报表

tangjiang 9 months ago
parent
commit
059eef1885

+ 254 - 5
src/Hotline.Api/Controllers/Bi/BiCallController.cs

@@ -1,13 +1,13 @@
-using Hotline.Caching.Interfaces;
-using Hotline.Caching.Services;
+using Hotline.Application.StatisticalReport;
+using Hotline.Caching.Interfaces;
 using Hotline.CallCenter.Calls;
 using Hotline.CallCenter.Tels;
-using Hotline.Orders;
 using Hotline.Repository.SqlSugar.Extensions;
 using Hotline.Settings;
 using Hotline.Share.Dtos;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Dtos.Order;
+using Hotline.Share.Dtos.TrCallCenter;
 using Hotline.Share.Enums.CallCenter;
 using Hotline.Share.Enums.User;
 using Hotline.Share.Requests;
@@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using SqlSugar;
 using System.Data;
-using XF.Domain.Constants;
 using XF.Domain.Exceptions;
 using XF.Domain.Repository;
 
@@ -37,6 +36,7 @@ public class BiCallController : BaseController
     private readonly ISystemSettingCacheManager _systemSettingCacheManager;
     private readonly IRepository<Work> _workRepository;
     private readonly ISystemDicDataCacheManager _sysDicDataCacheManager;
+    private readonly ICallReportApplication _callReportApplication;
 
 
 
@@ -48,7 +48,8 @@ public class BiCallController : BaseController
         ITrCallRecordRepository trCallRecordRepositoryEx,
         ISystemSettingCacheManager systemSettingCacheManager,
         ISystemDicDataCacheManager sysDicDataCacheManager,
-        IRepository<Work> workRepository)
+        IRepository<Work> workRepository,
+        ICallReportApplication callReportApplication)
     {
         _trCallRecordRepository = trCallRecordRepository;
         _userRepository = userRepository;
@@ -58,6 +59,7 @@ public class BiCallController : BaseController
         _systemSettingCacheManager = systemSettingCacheManager;
         _workRepository = workRepository;
         _sysDicDataCacheManager = sysDicDataCacheManager;
+        _callReportApplication = callReportApplication;
 
     }
 
@@ -161,6 +163,253 @@ public class BiCallController : BaseController
         return ExcelStreamResult(stream, "话务统计分析");
     }
 
+    /// <summary>
+    /// 话务日期明细
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("query_calls_detail")]
+    public async Task<object> QueryCallsDetailAsync([FromQuery] BiQueryCallsDto dto)
+    {
+        if (!dto.StartTime.HasValue || !dto.EndTime.HasValue)
+            throw UserFriendlyException.SameMessage("请选择时间!");
+        dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+
+        var items = await _callReportApplication.QueryCallsDetailAsync(dto);
+
+        var total = new QueryCallsDetailDto
+        {
+            Date = "合计",
+            Hour = "",
+            InTotal = items.Sum(p => p.InTotal),
+            InConnectionQuantity = items.Sum(p => p.InConnectionQuantity),
+            NotAcceptedHang = items.Sum(p => p.NotAcceptedHang),
+            TotalDurationIncomingCalls = items.Sum(p => p.TotalDurationIncomingCalls),
+            InAvailableAnswer = items.Sum(p => p.InAvailableAnswer),
+            InHangupImmediateWhenAnswered = items.Sum(p => p.InHangupImmediateWhenAnswered),
+            TimeoutConnection = items.Sum(p => p.TimeoutConnection),
+            TimeoutSuspension = items.Sum(p => p.TimeoutSuspension),
+            QueueByeCount = items.Sum(p => p.QueueByeCount),
+            IvrByeCount = items.Sum(p => p.IvrByeCount),
+            OutTotal = items.Sum(p => p.OutTotal),
+            OutConnectionQuantity = items.Sum(p => p.OutConnectionQuantity)
+        };
+
+        return new { List = items, Total = total };
+
+    }
+
+    /// <summary>
+    /// 话务日期明细--导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("query_calls_detail_export")]
+    public async Task<FileStreamResult> QueryCallsDetailExportAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
+    {
+        if (!dto.QueryDto.StartTime.HasValue || !dto.QueryDto.EndTime.HasValue)
+            throw UserFriendlyException.SameMessage("请选择时间!");
+        dto.QueryDto.EndTime = dto.QueryDto.EndTime.Value.AddDays(1).AddSeconds(-1);
+
+        var items = await _callReportApplication.QueryCallsDetailAsync(dto.QueryDto);
+
+        var total = new QueryCallsDetailDto
+        {
+            Date = "合计",
+            Hour = "",
+            InTotal = items.Sum(p => p.InTotal),
+            InConnectionQuantity = items.Sum(p => p.InConnectionQuantity),
+            NotAcceptedHang = items.Sum(p => p.NotAcceptedHang),
+            TotalDurationIncomingCalls = items.Sum(p => p.TotalDurationIncomingCalls),
+            InAvailableAnswer = items.Sum(p => p.InAvailableAnswer),
+            InHangupImmediateWhenAnswered = items.Sum(p => p.InHangupImmediateWhenAnswered),
+            TimeoutConnection = items.Sum(p => p.TimeoutConnection),
+            TimeoutSuspension = items.Sum(p => p.TimeoutSuspension),
+            QueueByeCount = items.Sum(p => p.QueueByeCount),
+            IvrByeCount = items.Sum(p => p.IvrByeCount),
+            OutTotal = items.Sum(p => p.OutTotal),
+            OutConnectionQuantity = items.Sum(p => p.OutConnectionQuantity)
+        };
+        items.Add(total);
+
+        dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+        var dtos = items
+            .Select(stu => _mapper.Map(stu, typeof(QueryCallsDetailDto), dynamicClass))
+            .Cast<object>()
+            .ToList();
+
+        var stream = ExcelHelper.CreateStream(dtos);
+
+        return ExcelStreamResult(stream, "话务日期明细数据");
+
+    }
+
+    /// <summary>
+    /// 话务日期明细--呼入明细
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("query_incall_calls_list")]
+    public async Task<PagedDto<TrCallDto>> GetInCallCallListAsync([FromQuery] BiQueryCallsDto dto)
+    {
+        if (!dto.StartTime.HasValue || !dto.EndTime.HasValue)
+            throw UserFriendlyException.SameMessage("请选择时间!");
+        dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+
+        var (total, items) = await _callReportApplication.QueryCallsDetailInTotalAsync(dto)
+            .ToPagedListAsync(dto.PageIndex, dto.PageSize, HttpContext.RequestAborted);
+
+        return new PagedDto<TrCallDto>(total, _mapper.Map<IReadOnlyList<TrCallDto>>(items));
+    }
+
+    /// <summary>
+    /// 话务日期明细----导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("query_incall_calls_list_export")]
+    public async Task<FileStreamResult> GetInCallCallListExportAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
+    {
+        if (!dto.QueryDto.StartTime.HasValue || !dto.QueryDto.EndTime.HasValue)
+            throw UserFriendlyException.SameMessage("请选择时间!");
+        dto.QueryDto.EndTime = dto.QueryDto.EndTime.Value.AddDays(1).AddSeconds(-1);
+
+        var query = _callReportApplication.QueryCallsDetailInTotalAsync(dto.QueryDto);
+        List<TrCallRecord> data;
+        if (dto.IsExportAll)
+        {
+            data = await query.ToListAsync(HttpContext.RequestAborted);
+        }
+        else
+        {
+            var (_, items) = await query.ToPagedListAsync(dto.QueryDto, HttpContext.RequestAborted);
+            data = items;
+        }
+
+        var dataDtos = _mapper.Map<ICollection<TrCallDto>>(data);
+
+        dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+        var dtos = dataDtos
+            .Select(stu => _mapper.Map(stu, typeof(TrCallDto), dynamicClass))
+            .Cast<object>()
+            .ToList();
+
+        var stream = ExcelHelper.CreateStream(dtos);
+
+        string name = dto.QueryDto.TypeCode == "2" ? "话务日期-接通明细数据" : "话务日期-总量明细数据";
+
+        return ExcelStreamResult(stream, name);
+
+    }
+
+    /// <summary>
+    /// 话务日期明细--时间段
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("query_calls_hour_detail_list")]
+    public async Task<object> QueryCallsHourDetailListAsync([FromQuery] BiQueryCallsDto dto)
+    {
+        if (!dto.StartTime.HasValue || !dto.EndTime.HasValue)
+            throw UserFriendlyException.SameMessage("请选择时间!");
+        dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+        //超时接通量
+        int CallInOverConnRingTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.CallInOverConnRingTime)?.SettingValue[0]);
+        //坐席超时挂断时间
+        int SeatChaoTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.SeatChaoTime)?.SettingValue[0]);
+
+        //未接秒挂时间
+        int noConnectByeTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.NoConnectByeTimes)?.SettingValue[0]);
+        //呼入有效时间
+        int effectiveTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.EffectiveTimes)?.SettingValue[0]);
+        //接通秒挂时间
+        int connectByeTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.ConnectByeTimes)?.SettingValue[0]);
+
+        var items = await _trCallRecordRepositoryEx.QueryCallsHourDetail(dto.StartTime.Value, dto.EndTime.Value, noConnectByeTimes, effectiveTimes
+              , connectByeTimes, CallInOverConnRingTime, SeatChaoTime, dto.Keyword);
+
+        var total = new QueryCallsDetailDto
+        {
+            Date = "",
+            Hour = "合计",
+            InTotal = items.Sum(p => p.InTotal),
+            InConnectionQuantity = items.Sum(p => p.InConnectionQuantity),
+            NotAcceptedHang = items.Sum(p => p.NotAcceptedHang),
+            TotalDurationIncomingCalls = items.Sum(p => p.TotalDurationIncomingCalls),
+            InAvailableAnswer = items.Sum(p => p.InAvailableAnswer),
+            InHangupImmediateWhenAnswered = items.Sum(p => p.InHangupImmediateWhenAnswered),
+            TimeoutConnection = items.Sum(p => p.TimeoutConnection),
+            TimeoutSuspension = items.Sum(p => p.TimeoutSuspension),
+            QueueByeCount = items.Sum(p => p.QueueByeCount),
+            IvrByeCount = items.Sum(p => p.IvrByeCount),
+            OutTotal = items.Sum(p => p.OutTotal),
+            OutConnectionQuantity = items.Sum(p => p.OutConnectionQuantity)
+        };
+
+        return new { List = items, Total = total };
+
+    }
+
+    /// <summary>
+    /// 话务日期明细--时间段--导出
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("query_calls_hour_detail_list_export")]
+    public async Task<FileStreamResult> QueryCallsHourDetailListExportAsync([FromBody] ExportExcelDto<BiQueryCallsDto> dto)
+    {
+        if (!dto.QueryDto.StartTime.HasValue || !dto.QueryDto.EndTime.HasValue)
+            throw UserFriendlyException.SameMessage("请选择时间!");
+        dto.QueryDto.EndTime = dto.QueryDto.EndTime.Value.AddDays(1).AddSeconds(-1);
+        //超时接通量
+        int CallInOverConnRingTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.CallInOverConnRingTime)?.SettingValue[0]);
+        //坐席超时挂断时间
+        int SeatChaoTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.SeatChaoTime)?.SettingValue[0]);
+
+        //未接秒挂时间
+        int noConnectByeTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.NoConnectByeTimes)?.SettingValue[0]);
+        //呼入有效时间
+        int effectiveTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.EffectiveTimes)?.SettingValue[0]);
+        //接通秒挂时间
+        int connectByeTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.ConnectByeTimes)?.SettingValue[0]);
+
+        var items = await _trCallRecordRepositoryEx.QueryCallsHourDetail(dto.QueryDto.StartTime.Value, dto.QueryDto.EndTime.Value, noConnectByeTimes, effectiveTimes
+              , connectByeTimes, CallInOverConnRingTime, SeatChaoTime, dto.QueryDto.Keyword);
+
+        var total = new QueryCallsDetailDto
+        {
+            Date = "",
+            Hour = "合计",
+            InTotal = items.Sum(p => p.InTotal),
+            InConnectionQuantity = items.Sum(p => p.InConnectionQuantity),
+            NotAcceptedHang = items.Sum(p => p.NotAcceptedHang),
+            TotalDurationIncomingCalls = items.Sum(p => p.TotalDurationIncomingCalls),
+            InAvailableAnswer = items.Sum(p => p.InAvailableAnswer),
+            InHangupImmediateWhenAnswered = items.Sum(p => p.InHangupImmediateWhenAnswered),
+            TimeoutConnection = items.Sum(p => p.TimeoutConnection),
+            TimeoutSuspension = items.Sum(p => p.TimeoutSuspension),
+            QueueByeCount = items.Sum(p => p.QueueByeCount),
+            IvrByeCount = items.Sum(p => p.IvrByeCount),
+            OutTotal = items.Sum(p => p.OutTotal),
+            OutConnectionQuantity = items.Sum(p => p.OutConnectionQuantity)
+        };
+
+        items.Add(total);
+
+        dynamic? dynamicClass = DynamicClassHelper.CreateDynamicClass(dto.ColumnInfos);
+
+        var dtos = items
+            .Select(stu => _mapper.Map(stu, typeof(QueryCallsDetailDto), dynamicClass))
+            .Cast<object>()
+            .ToList();
+
+        var stream = ExcelHelper.CreateStream(dtos);
+
+        return ExcelStreamResult(stream, "话务日期明细-时间段");
+
+    }
 
     /// <summary>
     /// 坐席话务统计分析

+ 97 - 0
src/Hotline.Application/StatisticalReport/CallReportApplication.cs

@@ -0,0 +1,97 @@
+using Hotline.Caching.Interfaces;
+using Hotline.CallCenter.Calls;
+using Hotline.Settings;
+using Hotline.Share.Dtos.CallCenter;
+using Hotline.Share.Enums.CallCenter;
+using SqlSugar;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Repository;
+
+namespace Hotline.Application.StatisticalReport
+{
+    public class CallReportApplication : ICallReportApplication, IScopeDependency
+    {
+        private readonly IRepository<TrCallRecord> _trCallRecordRepository;
+        private readonly ISystemSettingCacheManager _systemSettingCacheManager;
+
+        public CallReportApplication(
+            IRepository<TrCallRecord> trCallRecordRepository,
+            ISystemSettingCacheManager systemSettingCacheManager)
+        {
+            _trCallRecordRepository = trCallRecordRepository;
+            _systemSettingCacheManager = systemSettingCacheManager;
+
+        }
+
+        /// <summary>
+        /// 话务日期明细
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        public async Task<List<QueryCallsDetailDto>> QueryCallsDetailAsync(BiQueryCallsDto dto)
+        {
+            if (!dto.StartTime.HasValue || !dto.EndTime.HasValue)
+                throw UserFriendlyException.SameMessage("请选择时间!");
+            dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+            //超时接通量
+            int CallInOverConnRingTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.CallInOverConnRingTime)?.SettingValue[0]);
+            //坐席超时挂断时间
+            int SeatChaoTime = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.SeatChaoTime)?.SettingValue[0]);
+
+            //未接秒挂时间
+            int noConnectByeTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.NoConnectByeTimes)?.SettingValue[0]);
+            //呼入有效时间
+            int effectiveTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.EffectiveTimes)?.SettingValue[0]);
+            //接通秒挂时间
+            int connectByeTimes = int.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.ConnectByeTimes)?.SettingValue[0]);
+
+            var callData = await _trCallRecordRepository.Queryable()
+                    .Where(p => p.CreatedTime >= dto.StartTime && p.CreatedTime <= dto.EndTime)
+                    .Where(p => p.Gateway != "82826886" && SqlFunc.Length(p.Gateway) != 4)
+                     .WhereIF(!string.IsNullOrEmpty(dto.Keyword), p => p.Gateway == dto.Keyword)
+                     .GroupBy(p => p.CreatedTime.ToString("yyyy-MM-dd"))
+                    .Select(p => new QueryCallsDetailDto
+                    {
+                        Date = p.CreatedTime.ToString("yyyy-MM-dd"),
+                        InTotal = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In, 1, 0)),//呼入总量
+                        InConnectionQuantity = SqlFunc.AggregateSum(SqlFunc.IIF(p.OnState == EOnState.On && p.CallDirection == ECallDirection.In && p.AnsweredTime != null, 1, 0)),//呼入接通量
+                        NotAcceptedHang = SqlFunc.AggregateSum(SqlFunc.IIF(p.Duration == 0 && p.RingTimes <= noConnectByeTimes && p.RingTimes > 0, 1, 0)), //未接通秒挂
+                        TotalDurationIncomingCalls = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.OnState == EOnState.On, p.Duration, 0)), //呼入总时长
+                        InAvailableAnswer = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.Duration >= effectiveTimes, 1, 0)),//有效接通量
+                        InHangupImmediateWhenAnswered = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.Duration > 0 && p.Duration <= connectByeTimes, 1, 0)), //呼入接通秒挂
+                        TimeoutConnection = SqlFunc.AggregateSum(SqlFunc.IIF(p.OnState == EOnState.On && p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.RingTimes >= CallInOverConnRingTime, 1, 0)),//超时接通量
+                        TimeoutSuspension = SqlFunc.AggregateSum(SqlFunc.IIF(p.OnState == EOnState.On && p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.Duration >= SeatChaoTime, 1, 0)),//超时挂断量
+                        QueueByeCount = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.QueueTims > 0 && p.RingTimes == 0 && p.OnState == EOnState.NoOn, 1, 0)), //队列挂断
+                        IvrByeCount = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.BeginIvrTime.HasValue && !p.BeginQueueTime.HasValue && !p.BeginRingTime.HasValue && p.OnState == EOnState.NoOn, 1, 0)), //IVR挂断
+                        OutTotal = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.Out, 1, 0)),//呼出总量
+                        OutConnectionQuantity = SqlFunc.AggregateSum(SqlFunc.IIF(p.OnState == EOnState.On && p.CallDirection == ECallDirection.Out && p.AnsweredTime != null, 1, 0))
+                    })
+                    .ToListAsync();
+
+            return callData;
+        }
+
+        /// <summary>
+        /// 话务日期明细-呼入总量/接通总量
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        public ISugarQueryable<TrCallRecord> QueryCallsDetailInTotalAsync(BiQueryCallsDto dto)
+        {
+            if (!dto.StartTime.HasValue || !dto.EndTime.HasValue)
+                throw UserFriendlyException.SameMessage("请选择时间!");
+            dto.EndTime = dto.EndTime.Value.AddDays(1).AddSeconds(-1);
+
+            return _trCallRecordRepository.Queryable()
+                     .Includes(p => p.Order)
+                    .Where(p => p.CreatedTime >= dto.StartTime && p.CreatedTime <= dto.EndTime && p.CallDirection == ECallDirection.In)
+                    .WhereIF(dto.TypeCode == "2", p => p.OnState == EOnState.On && p.AnsweredTime != null)
+                    .Where(p => p.Gateway != "82826886" && SqlFunc.Length(p.Gateway) != 4)
+                     .WhereIF(!string.IsNullOrEmpty(dto.Keyword), p => p.Gateway == dto.Keyword)
+                     .OrderByDescending(p => p.CreatedTime)
+                    .MergeTable();
+        }
+
+    }
+}

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

@@ -0,0 +1,24 @@
+using Hotline.CallCenter.Calls;
+using Hotline.Share.Dtos.CallCenter;
+using SqlSugar;
+
+namespace Hotline.Application.StatisticalReport
+{
+    public interface ICallReportApplication
+    {
+        /// <summary>
+        /// 话务日期明细
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        Task<List<QueryCallsDetailDto>> QueryCallsDetailAsync(BiQueryCallsDto dto);
+
+        /// <summary>
+        /// 话务日期明细-呼入总量/接通总量
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        ISugarQueryable<TrCallRecord> QueryCallsDetailInTotalAsync(BiQueryCallsDto dto);
+
+    }
+}

+ 69 - 9
src/Hotline.Repository.SqlSugar/CallCenter/TrCallRecordRepository.cs

@@ -1,16 +1,8 @@
-using Hotline.Caching.Interfaces;
-using Hotline.CallCenter.Calls;
-using Hotline.Orders;
+using Hotline.CallCenter.Calls;
 using Hotline.Repository.SqlSugar.DataPermissions;
 using Hotline.Share.Dtos.CallCenter;
 using Hotline.Share.Enums.CallCenter;
 using SqlSugar;
-using System;
-using System.Collections.Generic;
-using System.Drawing.Printing;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using XF.Domain.Dependency;
 
 namespace Hotline.Repository.SqlSugar.CallCenter
@@ -63,6 +55,74 @@ namespace Hotline.Repository.SqlSugar.CallCenter
             return listCall;
         }
 
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="beginDate"></param>
+        /// <param name="endDate"></param>
+        /// <param name="noConnectByeTimes"></param>
+        /// <param name="effectiveTimes"></param>
+        /// <param name="connectByeTimes"></param>
+        /// <param name="CallInOverConnRingTime"></param>
+        /// <param name="SeatChaoTime"></param>
+        /// <param name="Line"></param>
+        /// <returns></returns>
+        public async Task<List<QueryCallsDetailDto>> QueryCallsHourDetail(DateTime beginDate, DateTime endDate, int noConnectByeTimes, int effectiveTimes, int connectByeTimes, int CallInOverConnRingTime, int SeatChaoTime, string? Line)
+        {
+            List<int> dts = new List<int>();
+            for (int i = 0; i < 24; i++)
+            {
+                dts.Add(i);
+            }
+
+            var listHour = Db.Reportable(dts).ToQueryable<int>();
+
+            var list = Db.Queryable<TrCallRecord>()
+                  .Where(p => p.CreatedTime >= beginDate && p.CreatedTime <= endDate)
+                  .Where(p => p.Gateway != "82826886" && SqlFunc.Length(p.Gateway) != 4)
+                  .WhereIF(!string.IsNullOrEmpty(Line), p => p.Gateway == Line)
+                   .GroupBy(p => p.CreatedTime.Hour)
+                   .Select(p => new
+                   {
+                       Hour = p.CreatedTime.Hour, //小时段
+                       InTotal = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In, 1, 0)),//呼入总量
+                       InConnectionQuantity = SqlFunc.AggregateSum(SqlFunc.IIF(p.OnState == EOnState.On && p.CallDirection == ECallDirection.In && p.AnsweredTime != null, 1, 0)),//呼入接通量
+                       NotAcceptedHang = SqlFunc.AggregateSum(SqlFunc.IIF(p.Duration == 0 && p.RingTimes <= noConnectByeTimes && p.RingTimes > 0, 1, 0)), //未接通秒挂
+                       TotalDurationIncomingCalls = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.OnState == EOnState.On, p.Duration, 0)), //呼入总时长
+                       InAvailableAnswer = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.Duration >= effectiveTimes, 1, 0)),//有效接通量
+                       InHangupImmediateWhenAnswered = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.Duration > 0 && p.Duration <= connectByeTimes, 1, 0)), //呼入接通秒挂
+                       TimeoutConnection = SqlFunc.AggregateSum(SqlFunc.IIF(p.OnState == EOnState.On && p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.RingTimes >= CallInOverConnRingTime, 1, 0)),//超时接通量
+                       TimeoutSuspension = SqlFunc.AggregateSum(SqlFunc.IIF(p.OnState == EOnState.On && p.CallDirection == ECallDirection.In && p.AnsweredTime != null && p.Duration >= SeatChaoTime, 1, 0)),//超时挂断量
+                       QueueByeCount = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.QueueTims > 0 && p.RingTimes == 0 && p.OnState == EOnState.NoOn, 1, 0)), //队列挂断
+                       IvrByeCount = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.In && p.BeginIvrTime.HasValue && !p.BeginQueueTime.HasValue && !p.BeginRingTime.HasValue && p.OnState == EOnState.NoOn, 1, 0)), //IVR挂断
+                       OutTotal = SqlFunc.AggregateSum(SqlFunc.IIF(p.CallDirection == ECallDirection.Out, 1, 0)),//呼出总量
+                       OutConnectionQuantity = SqlFunc.AggregateSum(SqlFunc.IIF(p.OnState == EOnState.On && p.CallDirection == ECallDirection.Out && p.AnsweredTime != null, 1, 0))
+                   })
+                   .MergeTable();
+
+            var listCall = await listHour.LeftJoin(list, (x, p) => x.ColumnName == p.Hour)
+                .OrderBy(x => x.ColumnName)
+               .Select((x, p) => new QueryCallsDetailDto()
+               {
+                   Hour = x.ColumnName.ToString() + ":00 - " + x.ColumnName.ToString() + ":59",
+                   InTotal = p.InTotal,
+                   InConnectionQuantity = p.InConnectionQuantity,
+                   NotAcceptedHang = p.NotAcceptedHang,
+                   TotalDurationIncomingCalls = p.TotalDurationIncomingCalls,
+                   InAvailableAnswer = p.InAvailableAnswer,
+                   InHangupImmediateWhenAnswered = p.InHangupImmediateWhenAnswered,
+                   TimeoutConnection = p.TimeoutConnection,
+                   TimeoutSuspension = p.TimeoutSuspension,
+                   QueueByeCount = p.QueueByeCount,
+                   IvrByeCount = p.IvrByeCount,
+                   OutTotal = p.OutTotal,
+                   OutConnectionQuantity = p.OutConnectionQuantity
+               })
+               .ToListAsync();
+
+            return listCall;
+        }
+
         public async Task<List<TrCallHourDto>?> GetCallHourList(DateTime beginDate, DateTime? endDate, int noConnectByeTimes, int effectiveTimes, int connectByeTimes, string source)
         {
             //计算小时差

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

@@ -8,4 +8,9 @@ public record BiQueryCallsDto: ReportPagedRequest
     /// 线路
     /// </summary>
     public string? Line { get; set; }
+
+    /// <summary>
+    /// 查询类型 1:呼入总量明细;2:接通总量明细
+    /// </summary>
+    public string? TypeCode { get; set; } = "1";
 }

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

@@ -0,0 +1,128 @@
+namespace Hotline.Share.Dtos.CallCenter
+{
+    public class QueryCallsDetailDto
+    {
+        /// <summary>
+        /// 日期
+        /// </summary>
+        public string Date { get; set; }
+
+        /// <summary>
+        /// 时间
+        /// </summary>
+        public string Hour { get; set; }
+
+        /// <summary>
+        /// 呼入总量
+        /// </summary>
+        public int InTotal { get; set; }
+
+        /// <summary>
+        /// 呼入接通量
+        /// </summary>
+        public int InConnectionQuantity { get; set; }
+
+        /// <summary>
+        /// 未接秒挂
+        /// </summary>
+        public int NotAcceptedHang { get; set; }
+
+        /// <summary>
+        /// 呼入接通率
+        /// </summary>
+        public string InConnectionRate => CalcSatisfiedRate(InTotal - NotAcceptedHang, InConnectionQuantity);
+
+        /// <summary>
+        /// 呼入总时长
+        /// </summary>
+        public double TotalDurationIncomingCalls { get; set; }
+
+        /// <summary>
+        /// 平均时长
+        /// </summary>
+        public double AverageDuration => CalcAvg(TotalDurationIncomingCalls, InConnectionQuantity);
+
+        /// <summary>
+        /// 有效接通量
+        /// </summary>
+        public int InAvailableAnswer { get; set; }
+
+        /// <summary>
+        /// 呼入接通秒挂
+        /// </summary>
+        public int InHangupImmediateWhenAnswered { get; set; }
+
+        /// <summary>
+        /// 有效接通率
+        /// </summary>
+        public string EffectiveConnectionRate => CalcSatisfiedRate(InConnectionQuantity, InAvailableAnswer - InHangupImmediateWhenAnswered);
+
+        /// <summary>
+        /// 超时接通
+        /// </summary>
+        public int TimeoutConnection { get; set; }
+
+        /// <summary>
+        /// 超时挂断
+        /// </summary>
+        public int TimeoutSuspension { get; set; }
+
+        /// <summary>
+        /// 按时接通率
+        /// </summary>
+        public string OnTimeConnectionRate => CalcSatisfiedRate(InConnectionQuantity, InConnectionQuantity - TimeoutConnection - TimeoutSuspension);
+
+        /// <summary>
+        /// 队列挂断
+        /// </summary>
+        public int QueueByeCount { get; set; }
+
+        /// <summary>
+        /// IVR挂断
+        /// </summary>
+        public int IvrByeCount { get; set; }
+
+        /// <summary>
+        /// 呼出总量
+        /// </summary>
+        public int OutTotal { get; set; }
+
+        /// <summary>
+        /// 呼出接通量
+        /// </summary>
+        public int OutConnectionQuantity { get; set; }
+
+        /// <summary>
+        /// 呼出接通率
+        /// </summary>
+        public string OutConnectionRate => CalcSatisfiedRate(OutTotal, OutConnectionQuantity);
+
+        /// <summary>
+        /// 计算平均
+        /// </summary>
+        /// <param name="Count">总数</param>
+        /// <param name="Quantity"></param>
+        /// <returns></returns>
+        public double CalcAvg(double Count, int Quantity)
+        {
+            if (Count <= 0 || Quantity <= 0)
+                return 0;
+
+            return Math.Round((Quantity / (double)Count) * 100, 3);
+        }
+
+        /// <summary>
+        /// 计算满意度
+        /// </summary>
+        /// <param name="Count">总数</param>
+        /// <param name="Quantity"></param>
+        /// <returns></returns>
+        public string CalcSatisfiedRate(int Count, int Quantity)
+        {
+            if (Count <= 0 || Quantity <= 0)
+                return 0 + "%";
+
+            return Math.Round((Quantity / (double)Count) * 100, 3) + "%";
+        }
+    }
+}

+ 13 - 0
src/Hotline/CallCenter/Calls/ITrCallRecordRepository.cs

@@ -5,6 +5,19 @@ namespace Hotline.CallCenter.Calls
 {
     public interface ITrCallRecordRepository
     {
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="beginDate"></param>
+        /// <param name="endDate"></param>
+        /// <param name="noConnectByeTimes"></param>
+        /// <param name="effectiveTimes"></param>
+        /// <param name="connectByeTimes"></param>
+        /// <param name="CallInOverConnRingTime"></param>
+        /// <param name="SeatChaoTime"></param>
+        /// <param name="Line"></param>
+        /// <returns></returns>
+        Task<List<QueryCallsDetailDto>> QueryCallsHourDetail(DateTime beginDate, DateTime endDate, int noConnectByeTimes, int effectiveTimes, int connectByeTimes, int CallInOverConnRingTime, int SeatChaoTime, string? Line);
 
         Task<List<BiCallDto>?> GetQueryCalls(DateTime beginDate, DateTime endDate, string? Line);
         Task<List<TrCallHourDto>?> GetCallHourList(DateTime beginDate, DateTime? endDate, int noConnectByeTimes, int effectiveTimes,int connectByeTimes, string source);