OrderDomainService.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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. var skip = 0;
  265. for (var i = 0; i < schedulings.Count; i++)
  266. {
  267. var scheduling = schedulings[i];
  268. var size = avg + (i < remaining ? 1 : 0);
  269. if (size > 0)
  270. {
  271. handlers.Add(new(
  272. scheduling.SchedulingUser.UserId,
  273. scheduling.SchedulingUser.UserName,
  274. scheduling.SchedulingUser.OrgId,
  275. scheduling.SchedulingUser.OrgIdName,
  276. null, null,
  277. steps.Skip(skip).Take(size).ToList()));
  278. skip += size;
  279. scheduling.SendOrderNum += size;
  280. await _schedulingRepository.Updateable()
  281. .SetColumns(s => new Scheduling() { SendOrderNum = scheduling.SendOrderNum })
  282. .Where(s => s.Id == scheduling.Id).ExecuteCommandAsync(cancellationToken);
  283. }
  284. }
  285. if (handlers.Any())
  286. await _workflowDomainService.ChangeHandlerBatchAsync(handlers, cancellationToken);
  287. }
  288. }
  289. }
  290. #endregion
  291. #region 工单校验- 交通类工单
  292. /// <summary>
  293. /// 工单校验 - 交通类工单
  294. /// </summary>
  295. /// <returns></returns>
  296. public async Task<OrderValidation> OrderValidation(AddOrderDto dto, CancellationToken cancellationToken)
  297. {
  298. var valid = new OrderValidation { Validation = false, Result = "" };
  299. var hotspot = await _hotspotRepository.GetAsync(dto.HotspotId, cancellationToken);
  300. if (hotspot.TrunkNum.Equals(AppDefaults.TrafficTrunkNum))
  301. {
  302. switch (dto.AcceptTypeCode)
  303. {
  304. //投诉举报
  305. case "30":
  306. case "35":
  307. valid.Validation = dto.Title.Contains("意见") || dto.Title.Contains("建议") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  308. valid.Validation = dto.Content.Contains("意见") || dto.Content.Contains("建议") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  309. if (dto.Content.Length < 25)
  310. {
  311. valid.Validation = true;
  312. valid.Result = "保存失败,受理内容字数不足!";
  313. }
  314. break;
  315. // 意见
  316. case "1":
  317. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  318. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  319. if (dto.Content.Length < 5)
  320. {
  321. valid.Validation = true;
  322. valid.Result = "保存失败,受理内容字数不足!";
  323. }
  324. break;
  325. //建议求助
  326. case "15":
  327. case "20":
  328. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("信息") || dto.Title.Contains("咨询");
  329. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("信息") || dto.Content.Contains("咨询");
  330. if (dto.Content.Length < 25)
  331. {
  332. valid.Validation = true;
  333. valid.Result = "保存失败,受理内容字数不足!";
  334. }
  335. break;
  336. // 咨询
  337. case "10":
  338. valid.Validation = dto.Title.Contains("投诉") || dto.Title.Contains("举报") || dto.Title.Contains("意见") || dto.Title.Contains("建议");
  339. valid.Validation = dto.Content.Contains("投诉") || dto.Content.Contains("举报") || dto.Content.Contains("意见") || dto.Content.Contains("建议");
  340. if (dto.Content.Length < 5)
  341. {
  342. valid.Validation = true;
  343. valid.Result = "保存失败,受理内容字数不足!";
  344. }
  345. break;
  346. // 表扬
  347. case "25":
  348. if (dto.Content.Length < 25)
  349. {
  350. valid.Validation = true;
  351. valid.Result = "保存失败,受理内容字数不足!";
  352. }
  353. break;
  354. default:
  355. if (dto.Content.Length < 5)
  356. {
  357. valid.Validation = true;
  358. valid.Result = "保存失败,受理内容字数不足!";
  359. }
  360. break;
  361. }
  362. }
  363. if (valid.Validation && string.IsNullOrEmpty(valid.Result))
  364. valid.Result = "标题或受理内容出现限制词,请检查!";
  365. return valid;
  366. }
  367. #endregion
  368. #region private
  369. private async Task<Order> GetOrderByFlowIdAsync(string workflowId, CancellationToken cancellationToken)
  370. {
  371. if (string.IsNullOrEmpty(workflowId))
  372. throw UserFriendlyException.SameMessage("无效流程编号");
  373. var order = await _orderRepository.Queryable()
  374. .Includes(d => d.Hotspot)
  375. .FirstAsync(d => d.WorkflowId == workflowId);
  376. if (order == null)
  377. throw new UserFriendlyException($"无效流程编号, workflowId: {workflowId}", "无效流程编号");
  378. return order;
  379. }
  380. private CacheOrderNO GetCurrentOrderNo()
  381. {
  382. var today = DateTime.Today;
  383. var cacheKey = today.ToString("yyyyMMdd");
  384. var cacheOrderNo = _cacheOrderNo.GetOrSet(cacheKey, f =>
  385. {
  386. var todayOrderCount = _orderRepository.Queryable(includeDeleted: true)
  387. .CountAsync(d => d.CreationTime.Date == today.Date)
  388. .GetAwaiter()
  389. .GetResult();
  390. return new CacheOrderNO { TotalCount = todayOrderCount };
  391. }, TimeSpan.FromDays(1));
  392. return cacheOrderNo;
  393. }
  394. private string GenerateNewOrderNo()
  395. {
  396. var today = DateTime.Today;
  397. var cacheKey = $"{today:yyyyMMdd}";
  398. var cacheOrderNo = _cacheOrderNo.GetOrSet(cacheKey, f =>
  399. {
  400. var todayOrderCount = _orderRepository.Queryable(true)
  401. .CountAsync(d => d.CreationTime.Date == today.Date)
  402. .GetAwaiter()
  403. .GetResult();
  404. return new CacheOrderNO { TotalCount = todayOrderCount };
  405. }, TimeSpan.FromDays(1));
  406. cacheOrderNo!.TotalCount += 1;
  407. var no = GenerateOrderNo(today, cacheOrderNo.TotalCount);
  408. _cacheOrderNo.Set(cacheKey, cacheOrderNo);
  409. return no;
  410. }
  411. private string GenerateNewProvinceNo(string no, string sourceChannelCode)
  412. {
  413. //诉求渠道+成员单位标识+行政区划代码+年月日+流水号
  414. //成员单位标识 99
  415. //宜宾市 511500 市级
  416. var today = DateTime.Today;
  417. //var count = no.Substring(no.Length - 5);
  418. //todo 双系统并行暂时执行此方案
  419. var count = no.Substring(no.Length - 4);
  420. count = (Convert.ToInt32(count) + 50000).ToString();
  421. var provinceCodes = new[] { "RGDH", "WX", "WB", "AP", "WZ", "YJ", "SCZWFWW", "XCX", "QT" };
  422. var prefix = provinceCodes.Any(d => d.Equals(sourceChannelCode))
  423. ? sourceChannelCode
  424. : "QT";
  425. return $"{prefix}99511500{today:yyMMdd}{count}";
  426. }
  427. private string GenerateOrderNo(DateTime today, int count)
  428. {
  429. return $"{today:yyyyMMdd}{count:000000}";
  430. }
  431. #endregion
  432. }
  433. public class CacheOrderNO
  434. {
  435. public int TotalCount { get; set; }
  436. }
  437. public class OrderValidation
  438. {
  439. public bool Validation { get; set; }
  440. public string Result { get; set; }
  441. }