DefaultCallApplication.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. using DocumentFormat.OpenXml.Spreadsheet;
  2. using DotNetCore.CAP;
  3. using Hotline.Application.Systems;
  4. using Hotline.Caching.Interfaces;
  5. using Hotline.CallCenter.BlackLists;
  6. using Hotline.CallCenter.Calls;
  7. using Hotline.CallCenter.Tels;
  8. using Hotline.Orders;
  9. using Hotline.Repository.SqlSugar.CallCenter;
  10. using Hotline.Repository.SqlSugar.Extensions;
  11. using Hotline.Settings;
  12. using Hotline.Share.Attributes;
  13. using Hotline.Share.Dtos;
  14. using Hotline.Share.Dtos.CallCenter;
  15. using Hotline.Share.Dtos.Order;
  16. using Hotline.Share.Dtos.TrCallCenter;
  17. using Hotline.Share.Enums.CallCenter;
  18. using Hotline.Share.Enums.Order;
  19. using Hotline.Share.Mq;
  20. using Hotline.Share.Tools;
  21. using Hotline.Users;
  22. using JiebaNet.Segmenter.Common;
  23. using Mapster;
  24. using MapsterMapper;
  25. using Microsoft.AspNetCore.Http;
  26. using Microsoft.AspNetCore.Mvc;
  27. using Microsoft.Extensions.Logging;
  28. using SqlSugar;
  29. using XF.Domain.Authentications;
  30. using XF.Domain.Cache;
  31. using XF.Domain.Exceptions;
  32. using XF.Domain.Repository;
  33. namespace Hotline.Application.CallCenter;
  34. public abstract class DefaultCallApplication : ICallApplication
  35. {
  36. private readonly IRepository<Tel> _telRepository;
  37. private readonly ISystemSettingCacheManager _systemSettingCacheManager;
  38. private readonly ICapPublisher _capPublisher;
  39. private readonly IRepository<TelGroup> _telGroupRepository;
  40. private readonly IWorkRepository _workRepository;
  41. private readonly ITelRestRepository _telRestRepository;
  42. private readonly IRepository<CallNative> _callNativeRepository;
  43. private readonly IRepository<TelOperation> _telOperationRepository;
  44. private readonly IRepository<CallidRelation> _callIdRelationRepository;
  45. private readonly ITypedCache<Work> _cacheWork;
  46. private readonly IUserCacheManager _userCacheManager;
  47. private readonly ISessionContext _sessionContext;
  48. private readonly IMapper _mapper;
  49. private readonly ILogger<DefaultCallApplication> _logger;
  50. private readonly ICallDomainService _callDomainService;
  51. private readonly IOrderVisitDomainService _orderVisitDomainService;
  52. private readonly IOrderRepository _orderRepository;
  53. private readonly IOrderVisitRepository _orderVisitRepository;
  54. private readonly ISystemLogRepository _systemLogRepository;
  55. private readonly IRepository<TelActionRecord> _telActionRecordRepository;
  56. public DefaultCallApplication(
  57. IRepository<Tel> telRepository,
  58. IRepository<TelGroup> telGroupRepository,
  59. IWorkRepository workRepository,
  60. ITelRestRepository telRestRepository,
  61. IRepository<CallNative> callNativeRepository,
  62. IRepository<TelOperation> telOperationRepository,
  63. IRepository<CallidRelation> callIdRelationRepository,
  64. ITypedCache<Work> cacheWork,
  65. IUserCacheManager userCacheManager,
  66. ISessionContext sessionContext,
  67. IMapper mapper,
  68. ILogger<DefaultCallApplication> logger,
  69. IOrderVisitDomainService orderVisitDomainService,
  70. ICallDomainService callDomainService,
  71. IOrderVisitRepository orderVisitRepository,
  72. ISystemSettingCacheManager systemSettingCacheManager,
  73. ICapPublisher capPublisher,
  74. IOrderRepository orderRepository,
  75. ISystemLogRepository systemLogRepository,
  76. IRepository<TelActionRecord> telActionRecordRepository)
  77. {
  78. _telRepository = telRepository;
  79. _telGroupRepository = telGroupRepository;
  80. _workRepository = workRepository;
  81. _telRestRepository = telRestRepository;
  82. _callNativeRepository = callNativeRepository;
  83. _telOperationRepository = telOperationRepository;
  84. _callIdRelationRepository = callIdRelationRepository;
  85. _cacheWork = cacheWork;
  86. _userCacheManager = userCacheManager;
  87. _sessionContext = sessionContext;
  88. _mapper = mapper;
  89. _logger = logger;
  90. _orderVisitDomainService = orderVisitDomainService;
  91. _callDomainService = callDomainService;
  92. _orderVisitRepository = orderVisitRepository;
  93. _systemSettingCacheManager = systemSettingCacheManager;
  94. _capPublisher = capPublisher;
  95. _orderRepository = orderRepository;
  96. _systemLogRepository = systemLogRepository;
  97. _telActionRecordRepository = telActionRecordRepository;
  98. }
  99. public DefaultCallApplication()
  100. {
  101. }
  102. /// <summary>
  103. /// 查询分机组和分机
  104. /// </summary>
  105. /// <returns></returns>
  106. public virtual async Task<IReadOnlyList<TelGroupDto>> QueryGroupTel(CancellationToken cancellationToken)
  107. {
  108. var data = await _telGroupRepository.Queryable().Includes(d => d.Tels)
  109. // .Select<TelGroupDto>()
  110. .ToListAsync(cancellationToken);
  111. return _mapper.Map<IReadOnlyList<TelGroupDto>>(data);
  112. }
  113. /// <summary>
  114. /// 查询分机
  115. /// </summary>
  116. public virtual async Task<(int, IList<TelDto>)> QueryTelsAsync(QueryTelsInDto dto, CancellationToken cancellationToken)
  117. {
  118. var (total, items) = await _telRepository.Queryable()
  119. .Includes(x => x.Groups)
  120. .WhereIF(dto.No.NotNullOrEmpty(), x => x.No == dto.No.Trim())
  121. .Select<TelDto>()
  122. .ToPagedListAsync(dto.PageIndex, dto.PageSize, cancellationToken);
  123. return (total, items);
  124. }
  125. /// <summary>
  126. /// 查询分机组
  127. /// </summary>
  128. public virtual async Task<IReadOnlyList<TelGroupDto>> QueryTelGroupsAsync(CancellationToken cancellationToken)
  129. {
  130. return await _telGroupRepository.Queryable()
  131. .Select<TelGroupDto>()
  132. .ToListAsync(cancellationToken);
  133. }
  134. /// <summary>
  135. /// 记录分机结束动作时间
  136. /// </summary>
  137. /// <param name="dto"></param>
  138. /// <returns></returns>
  139. public async Task EndActionAsync(EndActionInDto dto)
  140. {
  141. var telOperation = await _telOperationRepository.Queryable()
  142. .Where(m => m.StaffNo == dto.StaffNo && m.TelNo == dto.TelNo && !m.EndTime.HasValue)
  143. .WhereIF(dto.Status != -1, m => m.OperateState != dto.Status)
  144. .WhereIF(dto.Status == -1, m => m.OperateStateText == "未知")
  145. .OrderByDescending(m => m.CreationTime)
  146. .FirstAsync();
  147. if (telOperation == null) return;
  148. telOperation.EndAction();
  149. await _telOperationRepository.UpdateAsync(telOperation);
  150. _logger.LogInformation($"分机动作结束成功: 入参 [{dto.ToJson()}] 受影响 TelOperationId: {telOperation.Id}");
  151. }
  152. /// <summary>
  153. /// 新增黑名单
  154. /// </summary>
  155. public abstract Task<string> AddBlackListAsync(AddBlacklistDto dto, CancellationToken cancellationToken);
  156. /// <summary>
  157. /// 删除黑名单
  158. /// </summary>
  159. public abstract Task RemoveBlackListAsync(string id, CancellationToken cancellationToken);
  160. /// <summary>
  161. /// 查询黑名单
  162. /// </summary>
  163. public abstract Task<List<Blacklist>> QueryBlackListsAsync(CancellationToken cancellationToken);
  164. /// <summary>
  165. /// 签入
  166. /// </summary>
  167. public virtual async Task<TrOnDutyResponseDto> SignInAsync(SignInDto dto, CancellationToken cancellationToken)
  168. {
  169. if (string.IsNullOrEmpty(dto.TelNo))
  170. throw UserFriendlyException.SameMessage("无效分机号");
  171. var work = _userCacheManager.GetWorkByUserNoExp(_sessionContext.RequiredUserId);
  172. if (work is not null)
  173. {
  174. //if (work.TelNo != dto.TelNo)
  175. //{
  176. // throw UserFriendlyException.SameMessage("当前用户已签入其他分机");
  177. //}
  178. throw UserFriendlyException.SameMessage("当前用户已签入");
  179. }
  180. var telWork = _userCacheManager.GetWorkByTelNoExp(dto.TelNo);
  181. if (telWork is not null)
  182. {
  183. throw UserFriendlyException.SameMessage("当前分机已被占用");
  184. }
  185. work = new Work(_sessionContext.RequiredUserId, _sessionContext.UserName,
  186. dto.TelNo, dto.TelNo, null, null,
  187. dto.GroupId, _sessionContext.StaffNo, null);
  188. await _workRepository.AddAsync(work, cancellationToken);
  189. try
  190. {
  191. //签入是否处理是否有小休
  192. var signInCheckRest = bool.Parse(_systemSettingCacheManager.GetSetting(SettingConstants.SignInCheckRest).SettingValue[0]);
  193. if (signInCheckRest)
  194. {
  195. //如果有未结束的小休,先结束
  196. var telRest = await _telRestRepository.GetAsync(x => x.TelNo == work.TelNo && !x.EndTime.HasValue, cancellationToken);
  197. if (telRest != null)
  198. {
  199. telRest.EndRest();
  200. await _telRestRepository.UpdateAsync(telRest, cancellationToken);
  201. }
  202. //如果有未结束的话机动作先结束话机动作
  203. var telAction = await _telActionRecordRepository.GetAsync(x => x.TelNo == work.TelNo && x.ActionType == EActionType.TelRest && !x.EndTime.HasValue, cancellationToken);
  204. if (telAction != null)
  205. {
  206. telAction.EndAction();
  207. await _telActionRecordRepository.UpdateAsync(telAction);
  208. }
  209. }
  210. //记录签入日志
  211. var actionRecord = new TelActionRecord(work.UserId, work.UserName, work.TelNo, work.QueueId, EActionType.SignIn);
  212. await _telActionRecordRepository.AddAsync(actionRecord, cancellationToken);
  213. }
  214. catch (Exception)
  215. {
  216. }
  217. return new TrOnDutyResponseDto
  218. {
  219. TelNo = dto.TelNo,
  220. QueueId = dto.GroupId,
  221. StartTime = work.StartTime,
  222. };
  223. }
  224. /// <summary>
  225. /// 签出
  226. /// </summary>
  227. public virtual async Task SingOutAsync(CancellationToken cancellationToken)
  228. {
  229. var work = _userCacheManager.GetWorkByUserNoExp(_sessionContext.RequiredUserId);
  230. if (work is null) return;
  231. var telRest =
  232. await _telRestRepository.GetAsync(x => x.TelNo == work.TelNo && !x.EndTime.HasValue, cancellationToken);
  233. if (telRest is not null)
  234. {
  235. telRest.EndRest();
  236. await _telRestRepository.UpdateAsync(telRest, cancellationToken);
  237. }
  238. work.OffDuty();
  239. await _workRepository.UpdateAsync(work, cancellationToken);
  240. await _cacheWork.RemoveAsync(work.GetKey(KeyMode.UserId), cancellationToken);
  241. await _cacheWork.RemoveAsync(work.GetKey(KeyMode.TelNo), cancellationToken);
  242. try
  243. {
  244. var list = await _telActionRecordRepository.Queryable().Where(x => x.TelNo == work.TelNo && !x.EndTime.HasValue).ToListAsync();
  245. foreach (var item in list)
  246. {
  247. item.EndAction();
  248. await _telActionRecordRepository.UpdateAsync(item);
  249. }
  250. }
  251. catch (Exception)
  252. {
  253. }
  254. }
  255. /// <summary>
  256. /// 签出
  257. /// </summary>
  258. public virtual async Task SingOutAsync(string telNo, CancellationToken cancellationToken)
  259. {
  260. var work = _userCacheManager.GetWorkByTelNoExp(telNo);
  261. if (work is null) return;
  262. var telRest =
  263. await _telRestRepository.GetAsync(x => x.TelNo == work.TelNo && !x.EndTime.HasValue, cancellationToken);
  264. if (telRest is not null)
  265. {
  266. telRest.EndRest();
  267. await _telRestRepository.UpdateAsync(telRest, cancellationToken);
  268. }
  269. work.OffDuty();
  270. await _workRepository.UpdateAsync(work, cancellationToken);
  271. await _cacheWork.RemoveAsync(work.GetKey(KeyMode.UserId), cancellationToken);
  272. await _cacheWork.RemoveAsync(work.GetKey(KeyMode.TelNo), cancellationToken);
  273. var list = await _telActionRecordRepository.Queryable().Where(x => x.TelNo == work.TelNo && !x.EndTime.HasValue).ToListAsync();
  274. foreach (var item in list)
  275. {
  276. item.EndAction();
  277. await _telActionRecordRepository.UpdateAsync(item);
  278. }
  279. }
  280. /// <summary>
  281. /// 查询当前用户的分机状态
  282. /// </summary>
  283. /// <param name="cancellationToken"></param>
  284. /// <returns></returns>
  285. public virtual async Task<TrOnDutyResponseDto> GetTelStateAsync(CancellationToken cancellationToken)
  286. {
  287. var userId = _sessionContext.RequiredUserId;
  288. var work = _userCacheManager.GetWorkByUserNoExp(userId);
  289. if (work is not null)
  290. {
  291. //小休
  292. bool isRest = await _telRestRepository.AnyAsync(x => x.TelNo == work.TelNo && x.UserId == userId && !x.EndTime.HasValue, cancellationToken);
  293. //保持
  294. bool isCallHold = await _telActionRecordRepository.AnyAsync(x => x.TelNo == work.TelNo && x.UserId == userId && x.ActionType == EActionType.CallHold
  295. && !x.EndTime.HasValue, cancellationToken);
  296. //整理
  297. bool isCallEndArrange = await _telActionRecordRepository.AnyAsync(x => x.TelNo == work.TelNo && x.UserId == userId &&
  298. x.ActionType == EActionType.CallEndArrange && !x.EndTime.HasValue, cancellationToken);
  299. return new TrOnDutyResponseDto()
  300. {
  301. TelNo = work.TelNo,
  302. StaffNo = work.StaffNo,
  303. Description = work.Description,
  304. QueueId = work.QueueId,
  305. StartTime = work.StartTime,
  306. IsRest = isRest,
  307. IsCallHold = isCallHold,
  308. IsCallEndArrange = isCallEndArrange,
  309. ExtensionStatus = work.ExtensionStatus,
  310. OldExtensionStatus = work.OldExtensionStatus
  311. };
  312. }
  313. return null;
  314. }
  315. /// <summary>
  316. /// 定量查询通话记录
  317. /// </summary>
  318. public virtual ISugarQueryable<CallNativeDto> QueryCallsFixedAsync(QueryCallsFixedDto dto, CancellationToken cancellationToken)
  319. {
  320. if (dto.SortField.NotNullOrEmpty())
  321. dto.SortField = dto.SortField.ToLower();
  322. var query = _callNativeRepository.Queryable(includeDeleted: true)
  323. .LeftJoin<Order>((d, o) => d.Id == o.CallId)
  324. .LeftJoin<OrderVisit>((d, o, v) => d.Id == v.CallId)
  325. .Where((d, o, v) => d.IsDeleted == false)
  326. // .WhereIF(string.IsNullOrEmpty(dto.ToNo), (d, o, v) => d.GroupId != "0")
  327. .WhereIF(!string.IsNullOrEmpty(dto.OrderNo), (d, o, v) => o.No == dto.OrderNo)
  328. .WhereIF(!string.IsNullOrEmpty(dto.FromNo), d => d.FromNo.Contains(dto.FromNo!))
  329. .WhereIF(!string.IsNullOrEmpty(dto.ToNo), d => d.ToNo.Contains(dto.ToNo!))
  330. .WhereIF(!string.IsNullOrEmpty(dto.UserName), d => d.UserName == dto.UserName)
  331. .WhereIF(!string.IsNullOrEmpty(dto.TelNo), d => d.TelNo == dto.TelNo)
  332. .WhereIF(!string.IsNullOrEmpty(dto.CallNo), d => d.CallNo.Contains(dto.CallNo))
  333. .WhereIF(dto.EndBy != null, d => d.EndBy == dto.EndBy)
  334. .WhereIF(dto.CallStartTimeStart != null, d => d.BeginIvrTime >= dto.CallStartTimeStart)
  335. .WhereIF(dto.CallStartTimeEnd != null, d => d.BeginIvrTime <= dto.CallStartTimeEnd)
  336. .WhereIF(dto.IsConnected != null, d => d.AnsweredTime != null)
  337. .WhereIF(dto.Direction != null, d => d.Direction == dto.Direction)
  338. .WhereIF(dto.WaitDurationStart != null && dto.WaitDurationStart > 0, d => d.WaitDuration >= dto.WaitDurationStart)
  339. .WhereIF(dto.WaitDurationEnd != null && dto.WaitDurationEnd > 0, d => d.WaitDuration <= dto.WaitDurationEnd)
  340. .WhereIF(dto.IsMissOrder != null && dto.IsMissOrder.Value == true, (d, o, v) => string.IsNullOrEmpty(o.Id) == true)
  341. .WhereIF(dto.IsMissOrder != null && dto.IsMissOrder.Value == false, (d, o, v) => string.IsNullOrEmpty(o.Id) == false)
  342. .OrderByIF(dto.SortField.IsNullOrEmpty(), (d, o, v) => d.BeginIvrTime, OrderByType.Desc)
  343. .OrderByIF(dto is { SortField: "waitduration", SortRule: 0 }, (d, o, v) => d.WaitDuration, OrderByType.Asc)
  344. .OrderByIF(dto is { SortField: "waitduration", SortRule: 1 }, (d, o, v) => d.WaitDuration, OrderByType.Desc);
  345. query = query.WhereIF(dto.Type == 3, (d, o, v) => d.AnsweredTime == null);
  346. query = query.WhereIF(dto.Type == 1, (d, o, v) => d.Direction == ECallDirection.In && d.AnsweredTime != null);
  347. query = query.WhereIF(dto.Type == 2, (d, o, v) => d.Direction == ECallDirection.Out && d.AnsweredTime != null);
  348. query = query.WhereIF(dto.Type == 4, (d, o, v) => d.CallIdentity == ECallIdentity.White);
  349. query = query.WhereIF(dto.Type == 5, (d, o, v) => d.CallIdentity == ECallIdentity.Black);
  350. query = query.WhereIF(dto.Type != 3 && !string.IsNullOrEmpty(dto.StaffNo), d => d.StaffNo == dto.StaffNo);
  351. if (dto.Type == 2)
  352. {
  353. var d = query.Select((d, o, v) => new CallNativeDto
  354. {
  355. OrderId = SqlFunc.IsNull(v.OrderId, v.OrderId),
  356. OrderNo = SqlFunc.IsNull(v.Order.No, o.No),
  357. Title = SqlFunc.IsNull(v.Order.Title, o.Title),
  358. CallState = d.CallState,
  359. IsVisit = !SqlFunc.IsNullOrEmpty(v.Id),
  360. IsOrder = !SqlFunc.IsNullOrEmpty(o.Id),
  361. }, true);
  362. #if DEBUG
  363. var sql = d.ToSqlString();
  364. #endif
  365. return d;
  366. }
  367. if (dto.Type == 3 || dto.Type == 5)
  368. {
  369. return query.Select((d, o, v) => new CallNativeDto
  370. {
  371. OrderId = o.Id,
  372. OrderNo = o.No,
  373. CallState = d.CallState,
  374. Title = o.Title,
  375. IsVisit = !SqlFunc.IsNullOrEmpty(v.Id),
  376. IsOrder = !SqlFunc.IsNullOrEmpty(o.Id),
  377. }, true)
  378. .WhereIF(!string.IsNullOrEmpty(dto.StaffNo), d => d.StaffNo == dto.StaffNo);
  379. }
  380. return query.Select((d, o, v) => new CallNativeDto
  381. {
  382. OrderId = o.Id,
  383. OrderNo = o.No,
  384. CallState = d.CallState,
  385. Title = o.Title,
  386. IsVisit = !SqlFunc.IsNullOrEmpty(v.Id),
  387. IsOrder = !SqlFunc.IsNullOrEmpty(o.Id),
  388. }, true);
  389. }
  390. /// <summary>
  391. /// 查询分机操作记录(定量)
  392. /// </summary>
  393. [ExportExcel("分机操作记录")]
  394. public virtual ISugarQueryable<TelOperation> QueryTelOperationsAsync(QueryTelOperationsFixedDto dto)
  395. {
  396. return _telOperationRepository.Queryable()
  397. .Where(m => m.OperateStateText != "未知")
  398. .WhereIF(!string.IsNullOrEmpty(dto.UserName), d => d.UserName.Contains(dto.UserName))
  399. .WhereIF(!string.IsNullOrEmpty(dto.StaffNo), d => d.StaffNo.Contains(dto.StaffNo))
  400. .WhereIF(!string.IsNullOrEmpty(dto.GroupId), d => d.GroupId == dto.GroupId)
  401. .WhereIF(dto.TelNo != null, d => d.TelNo.Contains(dto.TelNo))
  402. .WhereIF(dto.OperateState != null, d => d.OperateState == dto.OperateState)
  403. .Where(x => x.OperateTime >= dto.StartTime && x.OperateTime <= dto.EndTime)
  404. .OrderByDescending(d => d.Id);
  405. }
  406. /// <summary>
  407. /// 依据通话记录编号获取映射后的callId
  408. /// </summary>
  409. public virtual async Task<string?> GetOrSetCallIdAsync(string? callNo, CancellationToken cancellationToken)
  410. {
  411. if (callNo == null) return null;
  412. var callOrder = await _callIdRelationRepository.GetAsync(callNo, cancellationToken);
  413. if (callOrder == null)
  414. {
  415. callOrder = new CallidRelation
  416. {
  417. Id = callNo,
  418. CallId = Ulid.NewUlid().ToString(),
  419. };
  420. try
  421. {
  422. await _callIdRelationRepository.AddAsync(callOrder, cancellationToken);
  423. }
  424. catch (Exception e)
  425. {
  426. _logger.LogError($"写入callidRelation失败:{e.Message}");
  427. return await GetOrSetCallIdAsync(callNo, cancellationToken);
  428. }
  429. }
  430. return callOrder.CallId;
  431. }
  432. public async Task<CallidRelation> GetRelationAsync(string callNo, CancellationToken cancellation)
  433. {
  434. return await _callIdRelationRepository.GetAsync(callNo, cancellation);
  435. }
  436. public async Task AddRelationAsync(CallidRelation relation, CancellationToken cancellation)
  437. {
  438. await _callIdRelationRepository.AddAsync(relation, cancellation);
  439. }
  440. /// <summary>
  441. /// 乐观并发更新映射关系
  442. /// </summary>
  443. public virtual async Task<int> UpdateRelationOptLockAsync(CallidRelation relation, CancellationToken cancellationToken)
  444. {
  445. return await _callIdRelationRepository.Updateable(relation).ExecuteCommandWithOptLockAsync();
  446. }
  447. /// <summary>
  448. /// 查询通话记录
  449. /// </summary>
  450. public virtual async Task<CallNative> GetCallAsync(string callId, CancellationToken cancellationToken)
  451. {
  452. if (string.IsNullOrEmpty(callId)) return null;
  453. if (callId.StartsWith("2024"))
  454. return await _callNativeRepository.GetAsync(m => m.CallNo == callId && m.IsDeleted == false, cancellationToken);
  455. return await _callNativeRepository.GetAsync(m => m.Id == callId && m.IsDeleted == false, cancellationToken);
  456. }
  457. /// <summary>
  458. /// 查询通话记录
  459. /// 因为自贡的系统中有回访和通话记录未关联的异常数据, 使用此方法查询通话记录
  460. /// </summary>
  461. public virtual async Task<CallNative> GetCallByTimeAndToNoAsync(string toNo, DateTime time, CancellationToken cancellationToken)
  462. {
  463. var beginTime = time.AddMinutes(-5);
  464. var items = await _callNativeRepository.Queryable()
  465. .Where(m => m.BeginIvrTime >= beginTime && m.BeginIvrTime <= time)
  466. .Where(m => m.ToNo == toNo && m.Direction == ECallDirection.Out && !string.IsNullOrEmpty(m.AudioFile) && m.IsDeleted == false)
  467. .OrderBy(m => m.BeginIvrTime)
  468. .ToListAsync(cancellationToken);
  469. if (items.NotNullOrEmpty())
  470. return items.First();
  471. return null;
  472. }
  473. /// <summary>
  474. /// 查询通话记录
  475. /// 因为自贡的系统中有回访和通话记录未关联的异常数据, 使用此方法查询通话记录
  476. /// </summary>
  477. public virtual async Task<CallNative> GetCallByCallNoAsync(string callNo, CancellationToken cancellationToken)
  478. {
  479. return await _callNativeRepository.Queryable()
  480. .Where(m => m.CallNo == callNo && !string.IsNullOrEmpty(m.AudioFile))
  481. .FirstAsync(cancellationToken);
  482. }
  483. /// <summary>
  484. /// 查询通话记录列表
  485. /// </summary>
  486. /// <param name="callId"></param>
  487. /// <param name="cancellationToken"></param>
  488. /// <returns></returns>
  489. public virtual async Task<List<CallNative>> GetCallListAsync(string callId, CancellationToken cancellationToken)
  490. {
  491. if (string.IsNullOrEmpty(callId)) return null;
  492. return await _callNativeRepository.Queryable().Where(x => x.Id == callId).ToListAsync(cancellationToken);
  493. }
  494. /// <summary>
  495. /// 查询通话记录
  496. /// </summary>
  497. public virtual async Task<List<CallNative>> QueryCallsAsync(
  498. string? phone,
  499. ECallDirection? direction,
  500. DateTime? callStartTimeStart,
  501. DateTime? callStartTimeEnd,
  502. CancellationToken cancellationToken)
  503. {
  504. if (string.IsNullOrEmpty(phone))
  505. return new List<CallNative>();
  506. return await _callNativeRepository.Queryable()
  507. .WhereIF(direction.HasValue, d => d.Direction == direction)
  508. .WhereIF(callStartTimeStart != null, d => d.BeginIvrTime >= callStartTimeStart)
  509. .WhereIF(callStartTimeEnd != null, d => d.BeginIvrTime <= callStartTimeEnd)
  510. .Where(d => d.FromNo == phone || d.ToNo == phone)
  511. .OrderBy(d => d.CreationTime)
  512. .ToListAsync(cancellationToken);
  513. }
  514. /// <summary>
  515. /// 根据 OrderId 返回用户电话评价枚举
  516. /// 默认返回 默认满意
  517. /// </summary>
  518. /// <param name="orderId"></param>
  519. /// <returns></returns>
  520. public virtual async Task<EVoiceEvaluate> GetReplyVoiceOrDefaultByOrderIdAsync(string orderId)
  521. {
  522. var callNative = await _callDomainService.GetByOrderIdAsync(orderId);
  523. if (callNative is null || callNative.ReplyTxt.IsNullOrEmpty())
  524. return EVoiceEvaluate.DefaultSatisfied;
  525. try
  526. {
  527. var smsReply = _orderVisitDomainService.GetVisitEvaluateByReplyTxt(callNative.ReplyTxt!.Trim());
  528. return smsReply!.VoiceEvaluate!.Value;
  529. }
  530. catch (UserFriendlyException)
  531. {
  532. return EVoiceEvaluate.DefaultSatisfied;
  533. }
  534. }
  535. /// <summary>
  536. /// 发送延迟消息让回访去关联通话记录
  537. /// 如果前端入参中的CallId为空, 就根据回访Id在systemLog中查询.前端在回访界面点击"回访"按钮拨号时会在systemLog中保存回访的号码,callNo,回访Id;
  538. /// 通过systemLog修复前端没有传callId这种情况;
  539. /// </summary>
  540. /// <param name="cancellationToken"></param>
  541. /// <returns></returns>
  542. public virtual async Task<string> PublishVisitRelevanceCallIdAsync(OrderRelevanceCallIdDto dto, CancellationToken cancellationToken)
  543. {
  544. if (dto.CallId.IsNullOrEmpty())
  545. {
  546. try
  547. {
  548. var log = await _systemLogRepository.Queryable()
  549. .Where(m => m.IpUrl == dto.Id && m.Name == "回访外呼已经接通")
  550. .FirstAsync(cancellationToken);
  551. if (log is null || log.Remark.IsNullOrEmpty()) return null;
  552. var callRemark = log.Remark.FromJson<CallRemarkDto>();
  553. dto.CallId = callRemark.CallId;
  554. }
  555. catch (Exception e)
  556. {
  557. _logger.LogError($"PublishVisitRelevanceCallIdAsync: {e.ToJson()}");
  558. }
  559. }
  560. var seconds = _systemSettingCacheManager.VisitCallDelaySecond;
  561. await _capPublisher.PublishDelayAsync(TimeSpan.FromSeconds(seconds), EventNames.VisitCallDelay, dto, cancellationToken: cancellationToken);
  562. return dto.CallId;
  563. }
  564. /// <summary>
  565. /// 处理: EventNames.VisitCallDelay 消息
  566. /// 保存回访详情时发送延迟消息同步通话记录
  567. /// 如果回访通话记录有多条, 需要关联通话时长最长的那条
  568. /// </summary>
  569. public virtual async Task OrderVisitRelevanceCallIdAsync(VisitDto dto, CancellationToken cancellationToken)
  570. {
  571. var visit = await _orderVisitRepository.GetAsync(dto.Id);
  572. if (visit == null) return;
  573. var callId = await _callNativeRepository.Queryable()
  574. .Where(m => m.CallNo == dto.CallId && string.IsNullOrEmpty(m.AudioFile) == false)
  575. .OrderByDescending(m => m.Duration)
  576. .Select(m => m.Id)
  577. .FirstAsync(cancellationToken);
  578. if (callId == null) return;
  579. visit.CallId = callId;
  580. await _callIdRelationRepository.Updateable()
  581. .SetColumns(m => m.CallId == callId)
  582. .Where(m => m.Id == dto.CallId)
  583. .ExecuteCommandAsync(cancellationToken);
  584. await _orderVisitRepository.UpdateAsync(visit);
  585. }
  586. #region tianrun 临时方案
  587. public virtual Task<TrCallRecord?> GetTianrunCallAsync(string callId, CancellationToken cancellationToken)
  588. {
  589. throw new NotImplementedException();
  590. }
  591. /// <summary>
  592. /// 根据转写ID获取通话信息
  593. /// </summary>
  594. /// <returns></returns>
  595. public virtual async Task<TrCallRecord?> GetTianrunCallTransliterationAsync(string transliterationId, CancellationToken cancellationToken)
  596. {
  597. throw new NotImplementedException();
  598. }
  599. public virtual async Task EditTransliterationStateAsync(string callId, ECallTransliterationState state, string transliterationId, CancellationToken cancellationToken)
  600. {
  601. throw new NotImplementedException();
  602. }
  603. /// <summary>
  604. /// 关联通话记录与order
  605. /// </summary>
  606. public virtual async Task RelateTianrunCallWithOrderAsync(string callId, string orderId,
  607. CancellationToken cancellationToken)
  608. {
  609. // 延迟一段时间后去通话记录中查询有通话音频记录文件的那通电话记录关联回工单.
  610. var second = _systemSettingCacheManager.VisitCallDelaySecond;
  611. await _capPublisher.PublishDelayAsync(TimeSpan.FromSeconds(second), EventNames.OrderRelateCall, orderId, cancellationToken: cancellationToken);
  612. }
  613. /// <summary>
  614. /// 处理工单和通话记录关联问题
  615. /// </summary>
  616. /// <param name="orderId"></param>
  617. /// <param name="cancellationToken"></param>
  618. /// <returns></returns>
  619. public virtual async Task<string> OrderRelateCallHandlerAsync(string orderId, CancellationToken cancellationToken)
  620. {
  621. var orderCall = await _orderRepository.Queryable()
  622. .LeftJoin<CallNative>((o, c) => o.CallId == c.Id)
  623. .Where((o, c) => o.Id == orderId)
  624. .Select((o, c) => new { o.CallId, c.CallNo })
  625. .FirstAsync(cancellationToken);
  626. //#if DEBUG
  627. // var order = await _orderRepository.GetAsync(orderId);
  628. // var oldName = _sessionContext.UserName;
  629. // _sessionContext.ChangeSession("08dcfe32-c260-40b4-839a-aeca1f76244c");
  630. // var newName = _sessionContext.UserName;
  631. //#endif
  632. if (orderCall is null || orderCall.CallNo.IsNullOrEmpty())
  633. {
  634. string message = $"延迟更新工单通话, 工单: {orderId} 根据 order.id left join call_native 信息为空; 消息队列无须重试";
  635. _logger.LogInformation(message);
  636. return message;
  637. }
  638. var call = await _callNativeRepository.Queryable()
  639. .Where(m => m.Id == orderCall.CallId)
  640. .FirstAsync(cancellationToken);
  641. // Order已经关联的通话记录是呼入的,并且有通话录音, 所以不需要再次关联
  642. if (call != null && call.Direction == ECallDirection.In && call.AudioFile.NotNullOrEmpty())
  643. {
  644. // 推省上
  645. await _capPublisher.PublishAsync(EventNames.HotlineCallConnectWithOrder, new PublishCallRecrodDto()
  646. {
  647. Order = (await _orderRepository.GetAsync(orderId, cancellationToken)).Adapt<OrderDto>(),
  648. TrCallRecordDto = call.Adapt<TrCallDto>()
  649. }, cancellationToken: cancellationToken);
  650. var message = "Order已经关联的通话记录是呼入的,并且有通话录音, 所以不需要再次关联.(完成推省上)";
  651. _systemLogRepository.Add("延迟更新工单通话", orderId, message, status: 1, ipUrl: orderCall.CallId);
  652. return message;
  653. }
  654. call = await _callNativeRepository.Queryable()
  655. .Where(m => m.CallNo == orderCall.CallNo && m.Direction == ECallDirection.In)
  656. .OrderByDescending(m => m.Duration)
  657. .FirstAsync(cancellationToken);
  658. if (call == null)
  659. {
  660. var message = $"延迟更新工单通话, 工单: {orderId} 根据 CallNo: {orderCall.CallNo} direction = 0 查询 call_native 信息为空; 等兴唐把数据同步过来, 队列重试;";
  661. throw new ArgumentNullException(message);
  662. }
  663. // 需要更新的callId 和 order.callId 相同, 不处理
  664. if (orderCall.CallId == call.Id)
  665. {
  666. // 推省上
  667. await _capPublisher.PublishAsync(EventNames.HotlineCallConnectWithOrder, new PublishCallRecrodDto()
  668. {
  669. Order = (await _orderRepository.GetAsync(orderId, cancellationToken)).Adapt<OrderDto>(),
  670. TrCallRecordDto = call.Adapt<TrCallDto>()
  671. }, cancellationToken: cancellationToken);
  672. return "需要更新的callId 和 order.callId 相同(完成推省上)";
  673. }
  674. await _orderRepository.Updateable()
  675. .SetColumns(m => m.CallId == call.Id)
  676. .Where(m => m.Id == orderId)
  677. .ExecuteCommandAsync(cancellationToken);
  678. await _callIdRelationRepository.Updateable()
  679. .SetColumns(m => m.CallId == call.Id)
  680. .Where(m => m.Id == orderCall.CallId)
  681. .ExecuteCommandAsync(cancellationToken);
  682. // 推省上
  683. await _capPublisher.PublishAsync(EventNames.HotlineCallConnectWithOrder, new PublishCallRecrodDto()
  684. {
  685. Order = (await _orderRepository.GetAsync(orderId, cancellationToken)).Adapt<OrderDto>(),
  686. TrCallRecordDto = call.Adapt<TrCallDto>()
  687. }, cancellationToken: cancellationToken);
  688. var msg = $"原CallId: {orderCall.CallId}, 更新CallId: {call.Id}";
  689. _systemLogRepository.Add("延迟更新工单通话", orderId, msg, status: 1, ipUrl: orderCall.CallId);
  690. return msg + "(完成推省上)";
  691. }
  692. /// <summary>
  693. /// 查询通话记录
  694. /// </summary>
  695. public virtual Task<List<TrCallRecord>> QueryTianrunCallsAsync(
  696. string? phone = null,
  697. ECallDirection? direction = null,
  698. DateTime? callStartTimeStart = null,
  699. DateTime? callStartTimeEnd = null,
  700. CancellationToken cancellationToken = default)
  701. {
  702. throw new NotImplementedException();
  703. }
  704. /// <summary>
  705. /// 查询分机操作选项
  706. /// </summary>
  707. /// <returns></returns>
  708. public abstract List<Kv> GetTelOperationOptions();
  709. public virtual Task<ESeatEvaluate> GetSeatDefaultByOrderIdAsync(string orderId)
  710. {
  711. throw new NotImplementedException();
  712. }
  713. public virtual Task ChangeTelModel(bool isCallOut, CancellationToken requestAborted)
  714. {
  715. throw new NotImplementedException();
  716. }
  717. #endregion
  718. }