using DotNetCore.CAP; using Hotline.Caching.Interfaces; using Hotline.Share.Dtos.Order; using Hotline.Share.Enums.Order; using MapsterMapper; using Microsoft.Extensions.Logging; using XF.Domain.Authentications; using XF.Domain.Cache; using XF.Domain.Dependency; using XF.Domain.Exceptions; using XF.Domain.Repository; using Hotline.CallCenter.Calls; using Hotline.File; using Hotline.FlowEngine.Workflows; using Hotline.Schedulings; using Hotline.SeedData; using Hotline.Users; using Hotline.Share.Dtos; using Hotline.Settings.Hotspots; using Hotline.Share.Dtos.FlowEngine; using Microsoft.AspNetCore.Http; using Hotline.Settings; namespace Hotline.Orders; public class OrderDomainService : IOrderDomainService, IScopeDependency { private readonly IOrderRepository _orderRepository; private readonly IRepository _orderRedoRepository; private readonly IRepository _orderPublishRepository; private readonly IRepository _orderVisitRepository; private readonly IRepository _orderExtensionRepository; private readonly IRepository _orderComplementRepository; private readonly ITypedCache _cacheOrderNo; private readonly ISessionContext _sessionContext; private readonly ICapPublisher _capPublisher; private readonly IMapper _mapper; private readonly ILogger _logger; private readonly IFileRepository _fileRepository; private readonly IRepository _schedulingRepository; private readonly IRepository _userRepository; private readonly ISystemSettingCacheManager _systemSettingCacheManager; private readonly IWorkflowDomainService _workflowDomainService; private readonly IRepository _hotspotRepository; public OrderDomainService( IOrderRepository orderRepository, IRepository orderRedoRepository, IRepository orderPublishRepository, IRepository orderVisitRepository, IRepository orderExtensionRepository, IRepository orderComplementRepository, ITypedCache cacheOrderNo, ISessionContext sessionContext, ICapPublisher capPublisher, IMapper mapper, ILogger logger, IFileRepository fileRepository, IRepository wexCallRecordRepository, IRepository userRepository, ISystemSettingCacheManager systemSettingCacheManager, IRepository schedulingRepository, IWorkflowDomainService workflowDomainService, IRepository hotspotRepository) { _orderRepository = orderRepository; _orderRedoRepository = orderRedoRepository; _orderPublishRepository = orderPublishRepository; _orderVisitRepository = orderVisitRepository; _orderExtensionRepository = orderExtensionRepository; _orderComplementRepository = orderComplementRepository; _cacheOrderNo = cacheOrderNo; _sessionContext = sessionContext; _capPublisher = capPublisher; _mapper = mapper; _logger = logger; _fileRepository = fileRepository; _userRepository = userRepository; _schedulingRepository = schedulingRepository; _systemSettingCacheManager = systemSettingCacheManager; _workflowDomainService = workflowDomainService; _hotspotRepository = hotspotRepository; } public async Task GetOrderAsync(string? orderId, bool withHotspot = false, bool withAcceptor = false, bool withExtension = false, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(orderId)) throw UserFriendlyException.SameMessage("无效工单编号"); var query = _orderRepository.Queryable(); if (withHotspot) query = query.Includes(d => d.Hotspot); if (withAcceptor) query = query.Includes(d => d.Acceptor); if (withExtension) query = query.Includes(d => d.OrderExtension); var order = await query.FirstAsync(d => d.Id == orderId, cancellationToken); if (order == null) throw new UserFriendlyException($"无效工单编号, orderId: {orderId}", "无效工单编号"); return order; } public async Task AddAsync(Order order, bool autoAccept = false, CancellationToken cancellationToken = default) { if (autoAccept) { order.AutoAccept(_sessionContext.RequiredUserId, _sessionContext.UserName, _sessionContext.StaffNo); order.Sign(_sessionContext.RequiredUserId, _sessionContext.UserName); } order.Init(); order.No = GenerateNewOrderNo(); order.Password = Random.Shared.Next(100000, 1000000).ToString(); order.ProvinceNo = string.IsNullOrEmpty(order.ProvinceNo) ? GenerateNewProvinceNo(order.No, order.SourceChannelCode) : order.ProvinceNo; return await _orderRepository.AddOrderNavAsync(order, cancellationToken); } /// /// 撤回或跳转前处理数据及校验 /// ///工单撤回时需校验工单当前是否存在待发布记录、待回访记录,若存在需删除对应记录(跳转同理) ///工单撤回时需校验工单是否存在甄别中记录,若存在不允许撤回当前工单(跳转同理) /// /// public async Task ReadyToRecallAsync(string orderId, CancellationToken cancellationToken) { var order = await _orderRepository.Queryable() .Includes(d => d.OrderPublish) .Includes(d => d.OrderVisits.Where(d => d.VisitState == EVisitState.WaitForVisit)) .Includes(d => d.OrderScreens) .FirstAsync(d => d.Id == orderId, cancellationToken); if (order.OrderScreens.Any()) throw UserFriendlyException.SameMessage("已甄别工单无法撤回或跳转"); //todo 需求调整:P72, 4,5,6,7,8,9,10 if (order.OrderPublish is not null) await _orderPublishRepository.RemoveAsync(order.OrderPublish, cancellationToken: cancellationToken); if (order.OrderVisits.Any()) await _orderVisitRepository.RemoveNav(order.OrderVisits) .Include(d => d.OrderVisitDetails) .ExecuteCommandAsync(); } public Task AddOrderRedoAsync(OrderRedo orderRedo, CancellationToken cancellationToken) => _orderRedoRepository.AddAsync(orderRedo, cancellationToken); public Task RemoveOrderRedoAsync(string id, CancellationToken cancellationToken) => _orderRedoRepository.RemoveAsync(id, cancellationToken: cancellationToken); public Task UpdateOrderRedoAsync(OrderRedo orderRedo, CancellationToken cancellationToken) => _orderRedoRepository.UpdateAsync(orderRedo, cancellationToken); #region 工单扩展信息 public async Task GetOrderExtensionsAsync(string provinceNo, CancellationToken cancellationToken) => await _orderExtensionRepository.GetAsync(d => d.ProvinceNo == provinceNo, cancellationToken); public Task UpdateExtensionAsync(OrderExtension orderExtension, CancellationToken cancellationToken) => _orderExtensionRepository.UpdateAsync(orderExtension, cancellationToken); /// /// 新增工单扩展信息 /// public async Task AddExtensionAsync(OrderExtension extension, CancellationToken cancellationToken) => await _orderExtensionRepository.AddAsync(extension, cancellationToken); /// /// 新增工单补充信息 /// public async Task AddOrderComplementAsync(AddOrderComplementDto dto, CancellationToken cancellationToken) { var complement = _mapper.Map(dto); complement.InitId(); complement.IsProComplement = true; if (dto.Files.Any()) complement.FileJson = await _fileRepository.AddFileAsync(dto.Files, complement.Id, "", cancellationToken); return await _orderComplementRepository.AddAsync(complement, cancellationToken); } #endregion #region 平均派单 /// /// 平均派单 /// /// public async Task AverageOrder(CancellationToken cancellationToken) { var time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd")); var scheduling = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser) .Where(x => x.SchedulingTime == time && x.WorkingTime <= DateTime.Now.TimeOfDay && x.OffDutyTime >= DateTime.Now.TimeOfDay && x.AtWork == true) .OrderBy(x => x.SendOrderNum).FirstAsync(cancellationToken); if (scheduling is null) return new FlowStepHandler { Key = AppDefaults.SendPoolId, Value = "待派单池", UserId = AppDefaults.SendPoolId, Username = "待派单池", OrgId = OrgSeedData.CenterId, OrgName = "市民热线服务中心" }; scheduling.SendOrderNum++; await _schedulingRepository.UpdateAsync(scheduling, cancellationToken); var user = scheduling.SchedulingUser; return new FlowStepHandler { Key = user.UserId, Value = user.UserName, UserId = user.UserId, Username = user.UserName, OrgId = user.OrgId, OrgName = user.OrgIdName }; } /// /// 登录平均派单 /// /// /// public async Task LogAverageOrder(string userId, Scheduling scheduling, CancellationToken cancellationToken) { //1.获取默认派单员所属的工单 //2.获取今天上班的人员 //3.给当前这个用户平均派单 var steps = await _workflowDomainService.GetStepsBelongsToAsync(AppDefaults.SendPoolId, cancellationToken); var user = await _userRepository.Queryable() .Includes(d => d.Organization) .FirstAsync(d => d.Id == userId, cancellationToken); var time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd")); var schedulings = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser) .Where(x => x.SchedulingTime == time).CountAsync(cancellationToken); if (schedulings > 0) { var sendNum = steps.Count / schedulings; scheduling.SendOrderNum += sendNum; await _schedulingRepository.Updateable() .SetColumns(s => new Scheduling() { SendOrderNum = scheduling.SendOrderNum, AtWork = scheduling.AtWork }) .Where(s => s.Id == scheduling.Id).ExecuteCommandAsync(cancellationToken); if (sendNum <= 0) return; var sendSteps = steps.Take(sendNum).ToList(); await _workflowDomainService.ChangeHandlerBatchAsync(new List<(string userId, string username, string orgId, string orgName, string? roleId, string? roleName, ICollection steps)> { new(user.Id, user.Name, user.OrgId, user.Organization.Name,null,null, sendSteps) }, cancellationToken); } } /// /// 触发平均派单 /// /// public async Task TriggerAverageOrder(CancellationToken cancellationToken) { //1.从排班里面获取今天上班的人 //2.获取默认派单员剩下的工单 //3.平均分配剩下的工单给今天上班的人 DateTime time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd")); var schedulings = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser) .Where(x => x.SchedulingTime!.Value == time && x.WorkingTime!.Value <= DateTime.Now.TimeOfDay && x.OffDutyTime!.Value >= DateTime.Now.TimeOfDay && x.AtWork == true) .OrderBy(x => x.SendOrderNum).ToListAsync(cancellationToken); if (schedulings.Any()) { var steps = await _workflowDomainService.GetStepsBelongsToAsync(AppDefaults.SendPoolId, cancellationToken); if (steps.Any()) { List<(string userId, string username, string orgId, string orgName, string? roleId, string? roleName, ICollection steps)> handlers = new(); var avg = steps.Count / schedulings.Count; var remaining = steps.Count % schedulings.Count; var skip = 0; for (var i = 0; i < schedulings.Count; i++) { var scheduling = schedulings[i]; var size = avg + (i < remaining ? 1 : 0); if (size > 0) { handlers.Add(new( scheduling.SchedulingUser.UserId, scheduling.SchedulingUser.UserName, scheduling.SchedulingUser.OrgId, scheduling.SchedulingUser.OrgIdName, null, null, steps.Skip(skip).Take(size).ToList())); skip += size; scheduling.SendOrderNum += size; await _schedulingRepository.Updateable() .SetColumns(s => new Scheduling() { SendOrderNum = scheduling.SendOrderNum }) .Where(s => s.Id == scheduling.Id).ExecuteCommandAsync(cancellationToken); } } if (handlers.Any()) await _workflowDomainService.ChangeHandlerBatchAsync(handlers, cancellationToken); } } } #endregion #region 工单校验- 交通类工单 /// /// 工单校验 - 交通类工单 /// /// public async Task OrderValidation(AddOrderDto dto, CancellationToken cancellationToken) { var valid = new OrderValidation { Validation = false, Result = "" }; var hotspot = await _hotspotRepository.GetAsync(dto.HotspotId, cancellationToken); if (hotspot.TrunkNum.Equals(AppDefaults.TrafficTrunkNum)) { switch (dto.AcceptTypeCode) { //投诉举报 case "30": case "35": valid.Validation = dto.Title.Contains("意见") || dto.Title.Contains("建议") || dto.Title.Contains("信息") || dto.Title.Contains("咨询"); valid.Validation = dto.Content.Contains("意见") || dto.Content.Contains("建议") || dto.Content.Contains("信息") || dto.Content.Contains("咨询"); if (dto.Content.Length < 25) { valid.Validation = true; valid.Result = "保存失败,受理内容字数不足!"; } break; // 意见 case "1": valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("信息") || dto.Title.Contains("咨询"); valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("信息") || dto.Content.Contains("咨询"); if (dto.Content.Length < 5) { valid.Validation = true; valid.Result = "保存失败,受理内容字数不足!"; } break; //建议求助 case "15": case "20": valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("信息") || dto.Title.Contains("咨询"); valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("信息") || dto.Content.Contains("咨询"); if (dto.Content.Length < 25) { valid.Validation = true; valid.Result = "保存失败,受理内容字数不足!"; } break; // 咨询 case "10": valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("意见") || dto.Title.Contains("建议"); valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("意见") || dto.Content.Contains("建议"); if (dto.Content.Length < 5) { valid.Validation = true; valid.Result = "保存失败,受理内容字数不足!"; } break; // 表扬 case "25": if (dto.Content.Length < 25) { valid.Validation = true; valid.Result = "保存失败,受理内容字数不足!"; } break; default: if (dto.Content.Length < 5) { valid.Validation = true; valid.Result = "保存失败,受理内容字数不足!"; } break; } } if (valid.Validation && string.IsNullOrEmpty(valid.Result)) valid.Result = "标题或受理内容出现限制词,请检查!"; return valid; } #endregion #region private private async Task GetOrderByFlowIdAsync(string workflowId, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(workflowId)) throw UserFriendlyException.SameMessage("无效流程编号"); var order = await _orderRepository.Queryable() .Includes(d => d.Hotspot) .FirstAsync(d => d.WorkflowId == workflowId); if (order == null) throw new UserFriendlyException($"无效流程编号, workflowId: {workflowId}", "无效流程编号"); return order; } private CacheOrderNO GetCurrentOrderNo() { var today = DateTime.Today; var cacheKey = today.ToString("yyyyMMdd"); var cacheOrderNo = _cacheOrderNo.GetOrSet(cacheKey, f => { var todayOrderCount = _orderRepository.Queryable(includeDeleted: true) .CountAsync(d => d.CreationTime.Date == today.Date) .GetAwaiter() .GetResult(); return new CacheOrderNO { TotalCount = todayOrderCount }; }, TimeSpan.FromDays(1)); return cacheOrderNo; } private string GenerateNewOrderNo() { var today = DateTime.Today; var cacheKey = $"{today:yyyyMMdd}"; var cacheOrderNo = _cacheOrderNo.GetOrSet(cacheKey, f => { var todayOrderCount = _orderRepository.Queryable(true) .CountAsync(d => d.CreationTime.Date == today.Date) .GetAwaiter() .GetResult(); return new CacheOrderNO { TotalCount = todayOrderCount }; }, TimeSpan.FromDays(1)); cacheOrderNo!.TotalCount += 1; var no = GenerateOrderNo(today, cacheOrderNo.TotalCount); _cacheOrderNo.Set(cacheKey, cacheOrderNo); return no; } private string GenerateNewProvinceNo(string no, string sourceChannelCode) { //诉求渠道+成员单位标识+行政区划代码+年月日+流水号 //成员单位标识 99 //宜宾市 511500 市级 var today = DateTime.Today; //var count = no.Substring(no.Length - 5); var setting = _systemSettingCacheManager.GetSetting(SettingConstants.VersionsAreaCode); var versionsAreaCode = setting?.SettingValue[0]; //todo 双系统并行暂时执行此方案 var count = no.Substring(no.Length - 4); count = (Convert.ToInt32(count) + 50000).ToString(); var provinceCodes = new[] { "RGDH", "WX", "WB", "AP", "WZ", "YJ", "SCZWFWW", "XCX", "QT" }; var prefix = provinceCodes.Any(d => d.Equals(sourceChannelCode)) ? sourceChannelCode : "QT"; return $"{prefix}99{versionsAreaCode}{today:yyMMdd}{count}"; } private string GenerateOrderNo(DateTime today, int count) { return $"{today:yyyyMMdd}{count:000000}"; } #endregion } public class CacheOrderNO { public int TotalCount { get; set; } } public class OrderValidation { public bool Validation { get; set; } public string Result { get; set; } }