OrderDomainService.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. using DotNetCore.CAP;
  2. using Hotline.Caching.Interfaces;
  3. using Hotline.Share.Dtos.Order;
  4. using Hotline.Share.Enums.Order;
  5. using MapsterMapper;
  6. using Microsoft.Extensions.Logging;
  7. using XF.Domain.Authentications;
  8. using XF.Domain.Cache;
  9. using XF.Domain.Dependency;
  10. using XF.Domain.Exceptions;
  11. using XF.Domain.Repository;
  12. using Hotline.CallCenter.Calls;
  13. using Hotline.File;
  14. using Hotline.FlowEngine.Workflows;
  15. using Hotline.Schedulings;
  16. using Hotline.SeedData;
  17. using Hotline.Users;
  18. using Hotline.Share.Dtos;
  19. using Hotline.Settings.Hotspots;
  20. using Hotline.Share.Dtos.FlowEngine;
  21. using Microsoft.AspNetCore.Http;
  22. using Hotline.Settings;
  23. namespace Hotline.Orders;
  24. public class OrderDomainService : IOrderDomainService, IScopeDependency
  25. {
  26. private readonly IOrderRepository _orderRepository;
  27. private readonly IRepository<OrderRedo> _orderRedoRepository;
  28. private readonly IRepository<OrderPublish> _orderPublishRepository;
  29. private readonly IRepository<OrderVisit> _orderVisitRepository;
  30. private readonly IRepository<OrderExtension> _orderExtensionRepository;
  31. private readonly IRepository<OrderComplement> _orderComplementRepository;
  32. private readonly ITypedCache<CacheOrderNO> _cacheOrderNo;
  33. private readonly ISessionContext _sessionContext;
  34. private readonly ICapPublisher _capPublisher;
  35. private readonly IMapper _mapper;
  36. private readonly ILogger<OrderDomainService> _logger;
  37. private readonly IFileRepository _fileRepository;
  38. private readonly IRepository<Scheduling> _schedulingRepository;
  39. private readonly IRepository<User> _userRepository;
  40. private readonly ISystemSettingCacheManager _systemSettingCacheManager;
  41. private readonly IWorkflowDomainService _workflowDomainService;
  42. private readonly IRepository<Hotspot> _hotspotRepository;
  43. public OrderDomainService(
  44. IOrderRepository orderRepository,
  45. IRepository<OrderRedo> orderRedoRepository,
  46. IRepository<OrderPublish> orderPublishRepository,
  47. IRepository<OrderVisit> orderVisitRepository,
  48. IRepository<OrderExtension> orderExtensionRepository,
  49. IRepository<OrderComplement> orderComplementRepository,
  50. ITypedCache<CacheOrderNO> cacheOrderNo,
  51. ISessionContext sessionContext,
  52. ICapPublisher capPublisher,
  53. IMapper mapper,
  54. ILogger<OrderDomainService> logger,
  55. IFileRepository fileRepository,
  56. IRepository<WexCallRecord> wexCallRecordRepository,
  57. IRepository<User> userRepository,
  58. ISystemSettingCacheManager systemSettingCacheManager,
  59. IRepository<Scheduling> schedulingRepository,
  60. IWorkflowDomainService workflowDomainService,
  61. IRepository<Hotspot> hotspotRepository)
  62. {
  63. _orderRepository = orderRepository;
  64. _orderRedoRepository = orderRedoRepository;
  65. _orderPublishRepository = orderPublishRepository;
  66. _orderVisitRepository = orderVisitRepository;
  67. _orderExtensionRepository = orderExtensionRepository;
  68. _orderComplementRepository = orderComplementRepository;
  69. _cacheOrderNo = cacheOrderNo;
  70. _sessionContext = sessionContext;
  71. _capPublisher = capPublisher;
  72. _mapper = mapper;
  73. _logger = logger;
  74. _fileRepository = fileRepository;
  75. _userRepository = userRepository;
  76. _schedulingRepository = schedulingRepository;
  77. _systemSettingCacheManager = systemSettingCacheManager;
  78. _workflowDomainService = workflowDomainService;
  79. _hotspotRepository = hotspotRepository;
  80. }
  81. public async Task<Order> GetOrderAsync(string? orderId, bool withHotspot = false, bool withAcceptor = false,
  82. bool withExtension = false, CancellationToken cancellationToken = default)
  83. {
  84. if (string.IsNullOrEmpty(orderId))
  85. throw UserFriendlyException.SameMessage("无效工单编号");
  86. var query = _orderRepository.Queryable();
  87. if (withHotspot)
  88. query = query.Includes(d => d.Hotspot);
  89. if (withAcceptor)
  90. query = query.Includes(d => d.Acceptor);
  91. if (withExtension)
  92. query = query.Includes(d => d.OrderExtension);
  93. var order = await query.FirstAsync(d => d.Id == orderId, cancellationToken);
  94. if (order == null)
  95. throw new UserFriendlyException($"无效工单编号, orderId: {orderId}", "无效工单编号");
  96. return order;
  97. }
  98. public async Task<string> AddAsync(Order order, bool autoAccept = false, CancellationToken cancellationToken = default)
  99. {
  100. if (autoAccept)
  101. {
  102. order.AutoAccept(_sessionContext.RequiredUserId, _sessionContext.UserName, _sessionContext.StaffNo);
  103. order.Sign(_sessionContext.RequiredUserId, _sessionContext.UserName);
  104. }
  105. order.Init();
  106. order.No = GenerateNewOrderNo();
  107. order.Password = Random.Shared.Next(100000, 1000000).ToString();
  108. order.ProvinceNo = string.IsNullOrEmpty(order.ProvinceNo) ? GenerateNewProvinceNo(order.No, order.SourceChannelCode) : order.ProvinceNo;
  109. return await _orderRepository.AddOrderNavAsync(order, cancellationToken);
  110. }
  111. /// <summary>
  112. /// 撤回或跳转前处理数据及校验
  113. /// <remarks>
  114. ///工单撤回时需校验工单当前是否存在待发布记录、待回访记录,若存在需删除对应记录(跳转同理)
  115. ///工单撤回时需校验工单是否存在甄别中记录,若存在不允许撤回当前工单(跳转同理)
  116. /// </remarks>
  117. /// </summary>
  118. public async Task ReadyToRecallAsync(string orderId, CancellationToken cancellationToken)
  119. {
  120. var order = await _orderRepository.Queryable()
  121. .Includes(d => d.OrderPublish)
  122. .Includes(d => d.OrderVisits.Where(d => d.VisitState == EVisitState.WaitForVisit))
  123. .Includes(d => d.OrderScreens)
  124. .FirstAsync(d => d.Id == orderId, cancellationToken);
  125. if (order.OrderScreens.Any())
  126. throw UserFriendlyException.SameMessage("已甄别工单无法撤回或跳转");
  127. //todo 需求调整:P72, 4,5,6,7,8,9,10
  128. if (order.OrderPublish is not null)
  129. await _orderPublishRepository.RemoveAsync(order.OrderPublish, cancellationToken: cancellationToken);
  130. if (order.OrderVisits.Any())
  131. await _orderVisitRepository.RemoveNav(order.OrderVisits)
  132. .Include(d => d.OrderVisitDetails)
  133. .ExecuteCommandAsync();
  134. }
  135. public Task<string> AddOrderRedoAsync(OrderRedo orderRedo, CancellationToken cancellationToken) =>
  136. _orderRedoRepository.AddAsync(orderRedo, cancellationToken);
  137. public Task RemoveOrderRedoAsync(string id, CancellationToken cancellationToken) =>
  138. _orderRedoRepository.RemoveAsync(id, cancellationToken: cancellationToken);
  139. public Task UpdateOrderRedoAsync(OrderRedo orderRedo, CancellationToken cancellationToken) =>
  140. _orderRedoRepository.UpdateAsync(orderRedo, cancellationToken);
  141. #region 工单扩展信息
  142. public async Task<OrderExtension?> GetOrderExtensionsAsync(string provinceNo, CancellationToken cancellationToken) =>
  143. await _orderExtensionRepository.GetAsync(d => d.ProvinceNo == provinceNo, cancellationToken);
  144. public Task UpdateExtensionAsync(OrderExtension orderExtension, CancellationToken cancellationToken) =>
  145. _orderExtensionRepository.UpdateAsync(orderExtension, cancellationToken);
  146. /// <summary>
  147. /// 新增工单扩展信息
  148. /// </summary>
  149. public async Task AddExtensionAsync(OrderExtension extension, CancellationToken cancellationToken) =>
  150. await _orderExtensionRepository.AddAsync(extension, cancellationToken);
  151. /// <summary>
  152. /// 新增工单补充信息
  153. /// </summary>
  154. public async Task<string> AddOrderComplementAsync(AddOrderComplementDto dto, CancellationToken cancellationToken)
  155. {
  156. var complement = _mapper.Map<OrderComplement>(dto);
  157. complement.InitId();
  158. complement.IsProComplement = true;
  159. if (dto.Files.Any()) complement.FileJson = await _fileRepository.AddFileAsync(dto.Files, complement.Id, "", cancellationToken);
  160. return await _orderComplementRepository.AddAsync(complement, cancellationToken);
  161. }
  162. #endregion
  163. #region 平均派单
  164. /// <summary>
  165. /// 平均派单
  166. /// </summary>
  167. /// <returns></returns>
  168. public async Task<FlowStepHandler> AverageOrder(CancellationToken cancellationToken)
  169. {
  170. var time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd"));
  171. var scheduling = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser)
  172. .Where(x => x.SchedulingTime == time && x.WorkingTime <= DateTime.Now.TimeOfDay && x.OffDutyTime >= DateTime.Now.TimeOfDay && x.AtWork == true)
  173. .OrderBy(x => x.SendOrderNum).FirstAsync(cancellationToken);
  174. if (scheduling is null)
  175. return new FlowStepHandler
  176. {
  177. Key = AppDefaults.SendPoolId,
  178. Value = "待派单池",
  179. UserId = AppDefaults.SendPoolId,
  180. Username = "待派单池",
  181. OrgId = OrgSeedData.CenterId,
  182. OrgName = "市民热线服务中心"
  183. };
  184. scheduling.SendOrderNum++;
  185. await _schedulingRepository.UpdateAsync(scheduling, cancellationToken);
  186. var user = scheduling.SchedulingUser;
  187. return new FlowStepHandler
  188. {
  189. Key = user.UserId,
  190. Value = user.UserName,
  191. UserId = user.UserId,
  192. Username = user.UserName,
  193. OrgId = user.OrgId,
  194. OrgName = user.OrgIdName
  195. };
  196. }
  197. /// <summary>
  198. /// 登录平均派单
  199. /// </summary>
  200. /// <param name="userId"></param>
  201. /// <returns></returns>
  202. public async Task LogAverageOrder(string userId, Scheduling scheduling, CancellationToken cancellationToken)
  203. {
  204. //1.获取默认派单员所属的工单
  205. //2.获取今天上班的人员
  206. //3.给当前这个用户平均派单
  207. var steps = await _workflowDomainService.GetStepsBelongsToAsync(AppDefaults.SendPoolId,
  208. cancellationToken);
  209. var user = await _userRepository.Queryable()
  210. .Includes(d => d.Organization)
  211. .FirstAsync(d => d.Id == userId, cancellationToken);
  212. var time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd"));
  213. var schedulings = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser)
  214. .Where(x => x.SchedulingTime == time).CountAsync(cancellationToken);
  215. if (schedulings > 0)
  216. {
  217. var sendNum = steps.Count / schedulings;
  218. scheduling.SendOrderNum += sendNum;
  219. await _schedulingRepository.Updateable()
  220. .SetColumns(s => new Scheduling() { SendOrderNum = scheduling.SendOrderNum, AtWork = scheduling.AtWork })
  221. .Where(s => s.Id == scheduling.Id).ExecuteCommandAsync(cancellationToken);
  222. if (sendNum <= 0) return;
  223. var sendSteps = steps.Take(sendNum).ToList();
  224. await _workflowDomainService.ChangeHandlerBatchAsync(new List<(string userId, string username, string orgId, string orgName, string? roleId, string? roleName, ICollection<WorkflowStep> steps)>
  225. {
  226. new(user.Id, user.Name, user.OrgId, user.Organization.Name,null,null, sendSteps)
  227. }, cancellationToken);
  228. }
  229. }
  230. /// <summary>
  231. /// 触发平均派单
  232. /// </summary>
  233. /// <returns></returns>
  234. public async Task TriggerAverageOrder(CancellationToken cancellationToken)
  235. {
  236. //1.从排班里面获取今天上班的人
  237. //2.获取默认派单员剩下的工单
  238. //3.平均分配剩下的工单给今天上班的人
  239. DateTime time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd"));
  240. var schedulings = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser)
  241. .Where(x => x.SchedulingTime!.Value == time && x.WorkingTime!.Value <= DateTime.Now.TimeOfDay && x.OffDutyTime!.Value >= DateTime.Now.TimeOfDay && x.AtWork == true)
  242. .OrderBy(x => x.SendOrderNum).ToListAsync(cancellationToken);
  243. if (schedulings.Any())
  244. {
  245. var steps = await _workflowDomainService.GetStepsBelongsToAsync(AppDefaults.SendPoolId,
  246. cancellationToken);
  247. if (steps.Any())
  248. {
  249. List<(string userId, string username, string orgId, string orgName, string? roleId, string? roleName, ICollection<WorkflowStep> steps)> handlers = new();
  250. var avg = steps.Count / schedulings.Count;
  251. var remaining = steps.Count % schedulings.Count;
  252. var skip = 0;
  253. for (var i = 0; i < schedulings.Count; i++)
  254. {
  255. var scheduling = schedulings[i];
  256. var size = avg + (i < remaining ? 1 : 0);
  257. if (size > 0)
  258. {
  259. handlers.Add(new(
  260. scheduling.SchedulingUser.UserId,
  261. scheduling.SchedulingUser.UserName,
  262. scheduling.SchedulingUser.OrgId,
  263. scheduling.SchedulingUser.OrgIdName,
  264. null, null,
  265. steps.Skip(skip).Take(size).ToList()));
  266. skip += size;
  267. scheduling.SendOrderNum += size;
  268. await _schedulingRepository.Updateable()
  269. .SetColumns(s => new Scheduling() { SendOrderNum = scheduling.SendOrderNum })
  270. .Where(s => s.Id == scheduling.Id).ExecuteCommandAsync(cancellationToken);
  271. }
  272. }
  273. if (handlers.Any())
  274. await _workflowDomainService.ChangeHandlerBatchAsync(handlers, cancellationToken);
  275. }
  276. }
  277. }
  278. #endregion
  279. #region 工单校验- 交通类工单
  280. /// <summary>
  281. /// 工单校验 - 交通类工单
  282. /// </summary>
  283. /// <returns></returns>
  284. public async Task<OrderValidation> OrderValidation(AddOrderDto dto, CancellationToken cancellationToken)
  285. {
  286. var valid = new OrderValidation { Validation = false, Result = "" };
  287. var hotspot = await _hotspotRepository.GetAsync(dto.HotspotId, cancellationToken);
  288. if (hotspot.TrunkNum.Equals(AppDefaults.TrafficTrunkNum))
  289. {
  290. switch (dto.AcceptTypeCode)
  291. {
  292. //投诉举报
  293. case "30":
  294. case "35":
  295. valid.Validation = dto.Title.Contains("意见") || dto.Title.Contains("建议") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  296. valid.Validation = dto.Content.Contains("意见") || dto.Content.Contains("建议") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  297. if (dto.Content.Length < 25)
  298. {
  299. valid.Validation = true;
  300. valid.Result = "保存失败,受理内容字数不足!";
  301. }
  302. break;
  303. // 意见
  304. case "1":
  305. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  306. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  307. if (dto.Content.Length < 5)
  308. {
  309. valid.Validation = true;
  310. valid.Result = "保存失败,受理内容字数不足!";
  311. }
  312. break;
  313. //建议求助
  314. case "15":
  315. case "20":
  316. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  317. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  318. if (dto.Content.Length < 25)
  319. {
  320. valid.Validation = true;
  321. valid.Result = "保存失败,受理内容字数不足!";
  322. }
  323. break;
  324. // 咨询
  325. case "10":
  326. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("意见") || dto.Title.Contains("建议");
  327. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("意见") || dto.Content.Contains("建议");
  328. if (dto.Content.Length < 5)
  329. {
  330. valid.Validation = true;
  331. valid.Result = "保存失败,受理内容字数不足!";
  332. }
  333. break;
  334. // 表扬
  335. case "25":
  336. if (dto.Content.Length < 25)
  337. {
  338. valid.Validation = true;
  339. valid.Result = "保存失败,受理内容字数不足!";
  340. }
  341. break;
  342. default:
  343. if (dto.Content.Length < 5)
  344. {
  345. valid.Validation = true;
  346. valid.Result = "保存失败,受理内容字数不足!";
  347. }
  348. break;
  349. }
  350. }
  351. if (valid.Validation && string.IsNullOrEmpty(valid.Result))
  352. valid.Result = "标题或受理内容出现限制词,请检查!";
  353. return valid;
  354. }
  355. #endregion
  356. #region private
  357. private async Task<Order> GetOrderByFlowIdAsync(string workflowId, CancellationToken cancellationToken)
  358. {
  359. if (string.IsNullOrEmpty(workflowId))
  360. throw UserFriendlyException.SameMessage("无效流程编号");
  361. var order = await _orderRepository.Queryable()
  362. .Includes(d => d.Hotspot)
  363. .FirstAsync(d => d.WorkflowId == workflowId);
  364. if (order == null)
  365. throw new UserFriendlyException($"无效流程编号, workflowId: {workflowId}", "无效流程编号");
  366. return order;
  367. }
  368. private CacheOrderNO GetCurrentOrderNo()
  369. {
  370. var today = DateTime.Today;
  371. var cacheKey = today.ToString("yyyyMMdd");
  372. var cacheOrderNo = _cacheOrderNo.GetOrSet(cacheKey, f =>
  373. {
  374. var todayOrderCount = _orderRepository.Queryable(includeDeleted: true)
  375. .CountAsync(d => d.CreationTime.Date == today.Date)
  376. .GetAwaiter()
  377. .GetResult();
  378. return new CacheOrderNO { TotalCount = todayOrderCount };
  379. }, TimeSpan.FromDays(1));
  380. return cacheOrderNo;
  381. }
  382. private string GenerateNewOrderNo()
  383. {
  384. var today = DateTime.Today;
  385. var cacheKey = $"{today:yyyyMMdd}";
  386. var cacheOrderNo = _cacheOrderNo.GetOrSet(cacheKey, f =>
  387. {
  388. var todayOrderCount = _orderRepository.Queryable(true)
  389. .CountAsync(d => d.CreationTime.Date == today.Date)
  390. .GetAwaiter()
  391. .GetResult();
  392. return new CacheOrderNO { TotalCount = todayOrderCount };
  393. }, TimeSpan.FromDays(1));
  394. cacheOrderNo!.TotalCount += 1;
  395. var no = GenerateOrderNo(today, cacheOrderNo.TotalCount);
  396. _cacheOrderNo.Set(cacheKey, cacheOrderNo);
  397. return no;
  398. }
  399. private string GenerateNewProvinceNo(string no, string sourceChannelCode)
  400. {
  401. //诉求渠道+成员单位标识+行政区划代码+年月日+流水号
  402. //成员单位标识 99
  403. //宜宾市 511500 市级
  404. var today = DateTime.Today;
  405. //var count = no.Substring(no.Length - 5);
  406. var setting = _systemSettingCacheManager.GetSetting(SettingConstants.VersionsAreaCode);
  407. var versionsAreaCode = setting?.SettingValue[0];
  408. //todo 双系统并行暂时执行此方案
  409. var count = no.Substring(no.Length - 4);
  410. count = (Convert.ToInt32(count) + 50000).ToString();
  411. var provinceCodes = new[] { "RGDH", "WX", "WB", "AP", "WZ", "YJ", "SCZWFWW", "XCX", "QT" };
  412. var prefix = provinceCodes.Any(d => d.Equals(sourceChannelCode))
  413. ? sourceChannelCode
  414. : "QT";
  415. return $"{prefix}99{versionsAreaCode}{today:yyMMdd}{count}";
  416. }
  417. private string GenerateOrderNo(DateTime today, int count)
  418. {
  419. return $"{today:yyyyMMdd}{count:000000}";
  420. }
  421. #endregion
  422. }
  423. public class CacheOrderNO
  424. {
  425. public int TotalCount { get; set; }
  426. }
  427. public class OrderValidation
  428. {
  429. public bool Validation { get; set; }
  430. public string Result { get; set; }
  431. }