OrderDomainService.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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 ??= GenerateNewProvinceNo(order.No, order.SourceChannelCode);
  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. /// <summary>
  164. /// 验证是否是管理员
  165. /// </summary>
  166. /// <returns></returns>
  167. public bool IsCheckAdmin()
  168. {
  169. var systemAdministrator = _systemSettingCacheManager.GetSetting(SettingConstants.SystemAdministrator)?.SettingValue[0];
  170. if (!string.IsNullOrEmpty(systemAdministrator) && (_sessionContext.Roles.Contains(systemAdministrator) || _sessionContext.Roles.Contains(RoleSeedData.AdminRole)))
  171. return true;
  172. else
  173. return false;
  174. }
  175. #region 平均派单
  176. /// <summary>
  177. /// 平均派单
  178. /// </summary>
  179. /// <returns></returns>
  180. public async Task<FlowStepHandler> AverageOrder(CancellationToken cancellationToken)
  181. {
  182. var time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd"));
  183. var scheduling = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser)
  184. .Where(x => x.SchedulingTime == time && x.WorkingTime <= DateTime.Now.TimeOfDay && x.OffDutyTime >= DateTime.Now.TimeOfDay && x.AtWork == true)
  185. .OrderBy(x => x.SendOrderNum).FirstAsync(cancellationToken);
  186. if (scheduling is null)
  187. return new FlowStepHandler
  188. {
  189. Key = AppDefaults.SendPoolId,
  190. Value = "待派单池",
  191. UserId = AppDefaults.SendPoolId,
  192. Username = "待派单池",
  193. OrgId = OrgSeedData.CenterId,
  194. OrgName = "市民热线服务中心"
  195. };
  196. scheduling.SendOrderNum++;
  197. await _schedulingRepository.UpdateAsync(scheduling, cancellationToken);
  198. var user = scheduling.SchedulingUser;
  199. return new FlowStepHandler
  200. {
  201. Key = user.UserId,
  202. Value = user.UserName,
  203. UserId = user.UserId,
  204. Username = user.UserName,
  205. OrgId = user.OrgId,
  206. OrgName = user.OrgIdName
  207. };
  208. }
  209. /// <summary>
  210. /// 登录平均派单
  211. /// </summary>
  212. /// <param name="userId"></param>
  213. /// <returns></returns>
  214. public async Task LogAverageOrder(string userId, Scheduling scheduling, CancellationToken cancellationToken)
  215. {
  216. //1.获取默认派单员所属的工单
  217. //2.获取今天上班的人员
  218. //3.给当前这个用户平均派单
  219. var steps = await _workflowDomainService.GetStepsBelongsToAsync(AppDefaults.SendPoolId,
  220. cancellationToken);
  221. var user = await _userRepository.Queryable()
  222. .Includes(d => d.Organization)
  223. .FirstAsync(d => d.Id == userId, cancellationToken);
  224. var time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd"));
  225. var schedulings = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser)
  226. .Where(x => x.SchedulingTime == time).CountAsync(cancellationToken);
  227. if (schedulings > 0)
  228. {
  229. var sendNum = steps.Count / schedulings;
  230. scheduling.SendOrderNum += sendNum;
  231. await _schedulingRepository.Updateable()
  232. .SetColumns(s => new Scheduling() { SendOrderNum = scheduling.SendOrderNum, AtWork = scheduling.AtWork })
  233. .Where(s => s.Id == scheduling.Id).ExecuteCommandAsync(cancellationToken);
  234. if (sendNum <= 0) return;
  235. var sendSteps = steps.Take(sendNum).ToList();
  236. await _workflowDomainService.ChangeHandlerBatchAsync(new List<(string userId, string username, string orgId, string orgName, string? roleId, string? roleName, ICollection<WorkflowStep> steps)>
  237. {
  238. new(user.Id, user.Name, user.OrgId, user.Organization.Name,null,null, sendSteps)
  239. }, cancellationToken);
  240. }
  241. }
  242. /// <summary>
  243. /// 触发平均派单
  244. /// </summary>
  245. /// <returns></returns>
  246. public async Task TriggerAverageOrder(CancellationToken cancellationToken)
  247. {
  248. //1.从排班里面获取今天上班的人
  249. //2.获取默认派单员剩下的工单
  250. //3.平均分配剩下的工单给今天上班的人
  251. DateTime time = DateTime.Parse(DateTime.Now.ToString("yyyy-MM-dd"));
  252. var schedulings = await _schedulingRepository.Queryable().Includes(x => x.SchedulingUser)
  253. .Where(x => x.SchedulingTime!.Value == time && x.WorkingTime!.Value <= DateTime.Now.TimeOfDay && x.OffDutyTime!.Value >= DateTime.Now.TimeOfDay && x.AtWork == true)
  254. .OrderBy(x => x.SendOrderNum).ToListAsync(cancellationToken);
  255. if (schedulings.Any())
  256. {
  257. var steps = await _workflowDomainService.GetStepsBelongsToAsync(AppDefaults.SendPoolId,
  258. cancellationToken);
  259. if (steps.Any())
  260. {
  261. List<(string userId, string username, string orgId, string orgName, string? roleId, string? roleName, ICollection<WorkflowStep> steps)> handlers = new();
  262. var avg = steps.Count / schedulings.Count;
  263. var remaining = steps.Count % schedulings.Count;
  264. for (var i = 0; i < schedulings.Count; i++)
  265. {
  266. var scheduling = schedulings[i];
  267. var size = avg + (i < remaining ? 1 : 0);
  268. handlers.Add(new(
  269. scheduling.SchedulingUser.UserId,
  270. scheduling.SchedulingUser.UserName,
  271. scheduling.SchedulingUser.OrgId,
  272. scheduling.SchedulingUser.OrgIdName,
  273. null, null,
  274. steps.Take(size).ToList()));
  275. scheduling.SendOrderNum += size;
  276. await _schedulingRepository.Updateable()
  277. .SetColumns(s => new Scheduling() { SendOrderNum = scheduling.SendOrderNum })
  278. .Where(s => s.Id == scheduling.Id).ExecuteCommandAsync(cancellationToken);
  279. }
  280. if (handlers.Any())
  281. await _workflowDomainService.ChangeHandlerBatchAsync(handlers, cancellationToken);
  282. }
  283. }
  284. }
  285. #endregion
  286. #region 工单校验- 交通类工单
  287. /// <summary>
  288. /// 工单校验 - 交通类工单
  289. /// </summary>
  290. /// <returns></returns>
  291. public async Task<OrderValidation> OrderValidation(AddOrderDto dto, CancellationToken cancellationToken)
  292. {
  293. var valid = new OrderValidation { Validation = false, Result = "" };
  294. var hotspot = await _hotspotRepository.GetAsync(dto.HotspotId, cancellationToken);
  295. if (hotspot.TrunkNum.Equals(AppDefaults.TrafficTrunkNum))
  296. {
  297. switch (dto.AcceptTypeCode)
  298. {
  299. //投诉举报
  300. case "30":
  301. case "35":
  302. valid.Validation = dto.Title.Contains("意见") || dto.Title.Contains("建议") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  303. valid.Validation = dto.Content.Contains("意见") || dto.Content.Contains("建议") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  304. if (dto.Content.Length < 25)
  305. {
  306. valid.Validation = true;
  307. valid.Result = "保存失败,受理内容字数不足!";
  308. }
  309. break;
  310. // 意见
  311. case "1":
  312. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  313. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  314. if (dto.Content.Length < 5)
  315. {
  316. valid.Validation = true;
  317. valid.Result = "保存失败,受理内容字数不足!";
  318. }
  319. break;
  320. //建议求助
  321. case "15":
  322. case "20":
  323. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  324. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  325. if (dto.Content.Length < 25)
  326. {
  327. valid.Validation = true;
  328. valid.Result = "保存失败,受理内容字数不足!";
  329. }
  330. break;
  331. // 咨询
  332. case "10":
  333. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("意见") || dto.Title.Contains("建议");
  334. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("意见") || dto.Content.Contains("建议");
  335. if (dto.Content.Length < 5)
  336. {
  337. valid.Validation = true;
  338. valid.Result = "保存失败,受理内容字数不足!";
  339. }
  340. break;
  341. // 表扬
  342. case "25":
  343. if (dto.Content.Length < 25)
  344. {
  345. valid.Validation = true;
  346. valid.Result = "保存失败,受理内容字数不足!";
  347. }
  348. break;
  349. default:
  350. if (dto.Content.Length < 5)
  351. {
  352. valid.Validation = true;
  353. valid.Result = "保存失败,受理内容字数不足!";
  354. }
  355. break;
  356. }
  357. }
  358. if (valid.Validation && string.IsNullOrEmpty(valid.Result))
  359. valid.Result = "标题或受理内容出现限制词,请检查!";
  360. return valid;
  361. }
  362. #endregion
  363. #region private
  364. private async Task<Order> GetOrderByFlowIdAsync(string workflowId, CancellationToken cancellationToken)
  365. {
  366. if (string.IsNullOrEmpty(workflowId))
  367. throw UserFriendlyException.SameMessage("无效流程编号");
  368. var order = await _orderRepository.Queryable()
  369. .Includes(d => d.Hotspot)
  370. .FirstAsync(d => d.WorkflowId == workflowId);
  371. if (order == null)
  372. throw new UserFriendlyException($"无效流程编号, workflowId: {workflowId}", "无效流程编号");
  373. return order;
  374. }
  375. private CacheOrderNO GetCurrentOrderNo()
  376. {
  377. var today = DateTime.Today;
  378. var cacheKey = today.ToString("yyyyMMdd");
  379. var cacheOrderNo = _cacheOrderNo.GetOrSet(cacheKey, f =>
  380. {
  381. var todayOrderCount = _orderRepository.Queryable(includeDeleted: true)
  382. .CountAsync(d => d.CreationTime.Date == today.Date)
  383. .GetAwaiter()
  384. .GetResult();
  385. return new CacheOrderNO { TotalCount = todayOrderCount };
  386. }, TimeSpan.FromDays(1));
  387. return cacheOrderNo;
  388. }
  389. private string GenerateNewOrderNo()
  390. {
  391. var today = DateTime.Today;
  392. var cacheKey = $"{today:yyyyMMdd}";
  393. var cacheOrderNo = _cacheOrderNo.GetOrSet(cacheKey, f =>
  394. {
  395. var todayOrderCount = _orderRepository.Queryable(true)
  396. .CountAsync(d => d.CreationTime.Date == today.Date)
  397. .GetAwaiter()
  398. .GetResult();
  399. return new CacheOrderNO { TotalCount = todayOrderCount };
  400. }, TimeSpan.FromDays(1));
  401. cacheOrderNo!.TotalCount += 1;
  402. var no = GenerateOrderNo(today, cacheOrderNo.TotalCount);
  403. _cacheOrderNo.Set(cacheKey, cacheOrderNo);
  404. return no;
  405. }
  406. private string GenerateNewProvinceNo(string no, string sourceChannelCode)
  407. {
  408. //诉求渠道+成员单位标识+行政区划代码+年月日+流水号
  409. //成员单位标识 99
  410. //宜宾市 511500 市级
  411. var today = DateTime.Today;
  412. //var count = no.Substring(no.Length - 5);
  413. //todo 双系统并行暂时执行此方案
  414. var count = no.Substring(no.Length - 4);
  415. count = (Convert.ToInt32(count) + 50000).ToString();
  416. var provinceCodes = new[] { "RGDH", "WX", "WB", "AP", "WZ", "YJ", "SCZWFWW", "XCX", "QT" };
  417. var prefix = provinceCodes.Any(d => d.Equals(sourceChannelCode))
  418. ? sourceChannelCode
  419. : "QT";
  420. return $"{prefix}99511500{today:yyMMdd}{count}";
  421. }
  422. private string GenerateOrderNo(DateTime today, int count)
  423. {
  424. return $"{today:yyyyMMdd}{count:000000}";
  425. }
  426. #endregion
  427. }
  428. public class CacheOrderNO
  429. {
  430. public int TotalCount { get; set; }
  431. }
  432. public class OrderValidation
  433. {
  434. public bool Validation { get; set; }
  435. public string Result { get; set; }
  436. }