| | | 1 | | using OzonDomains; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using Services.DataServices; |
| | | 4 | | |
| | | 5 | | namespace Services.TransactionUtilsServices; |
| | | 6 | | |
| | | 7 | | public interface ITransactionTimeTrackingService |
| | | 8 | | { |
| | | 9 | | Task<string> StartSessionAsync( |
| | | 10 | | string userName, |
| | | 11 | | int? accessProfileId, |
| | | 12 | | TransactionType transactionType, |
| | | 13 | | IEnumerable<int> orderIds, |
| | | 14 | | string? sourceUrl, |
| | | 15 | | string? clientIp, |
| | | 16 | | string? userAgent); |
| | | 17 | | |
| | | 18 | | Task<long?> CompleteSessionAsync( |
| | | 19 | | string trackingKey, |
| | | 20 | | long transactionId, |
| | | 21 | | int ordersCountFinal, |
| | | 22 | | string? comment, |
| | | 23 | | string transactionUrl); |
| | | 24 | | |
| | | 25 | | Task<int> MarkAbandonedAsync(TimeSpan olderThan); |
| | | 26 | | } |
| | | 27 | | |
| | | 28 | | public class TransactionTimeTrackingService : ITransactionTimeTrackingService |
| | | 29 | | { |
| | | 30 | | // Длины колонок TransactionTimeTrackingSessions/Results (см. [MaxLength] в TransactionTimeTrackingModels). |
| | | 31 | | // URL грида содержит закодированное состояние фильтров и легко превышает 1000 символов (#1825, #2353). |
| | | 32 | | private const int SourceUrlMaxLength = 1000; |
| | | 33 | | private const int ClientIpMaxLength = 64; |
| | | 34 | | private const int UserAgentMaxLength = 512; |
| | | 35 | | private const int TransactionUrlMaxLength = 1000; |
| | | 36 | | private const int CommentMaxLength = 1000; |
| | | 37 | | |
| | | 38 | | private readonly TransactionTimeTrackingDataServices _dataServices; |
| | | 39 | | |
| | 30 | 40 | | public TransactionTimeTrackingService(TransactionTimeTrackingDataServices dataServices) |
| | | 41 | | { |
| | 30 | 42 | | _dataServices = dataServices; |
| | 30 | 43 | | } |
| | | 44 | | |
| | | 45 | | public async Task<string> StartSessionAsync( |
| | | 46 | | string userName, |
| | | 47 | | int? accessProfileId, |
| | | 48 | | TransactionType transactionType, |
| | | 49 | | IEnumerable<int> orderIds, |
| | | 50 | | string? sourceUrl, |
| | | 51 | | string? clientIp, |
| | | 52 | | string? userAgent) |
| | | 53 | | { |
| | 19 | 54 | | var ids = orderIds?.Distinct().ToList() ?? new List<int>(); |
| | 19 | 55 | | var trackingKey = Guid.NewGuid().ToString("N"); |
| | 19 | 56 | | var now = DateTime.UtcNow; |
| | | 57 | | |
| | 19 | 58 | | var session = new TransactionTimeTrackingSession |
| | 19 | 59 | | { |
| | 19 | 60 | | TrackingKey = trackingKey, |
| | 19 | 61 | | UserName = userName, |
| | 19 | 62 | | AccessProfileId = accessProfileId, |
| | 19 | 63 | | TransactionType = transactionType.ToString(), |
| | 19 | 64 | | OrdersCountInitial = ids.Count, |
| | 19 | 65 | | OrderIdsJson = ids.Count == 0 ? null : string.Join(",", ids), |
| | 19 | 66 | | StartedAtUtc = now, |
| | 19 | 67 | | LastSeenAtUtc = now, |
| | 19 | 68 | | IsCompleted = false, |
| | 19 | 69 | | IsAbandoned = false, |
| | 19 | 70 | | SourceUrl = Truncate(sourceUrl, SourceUrlMaxLength), |
| | 19 | 71 | | ClientIp = Truncate(clientIp, ClientIpMaxLength), |
| | 19 | 72 | | UserAgent = Truncate(userAgent, UserAgentMaxLength), |
| | 19 | 73 | | CreatedAtUtc = now |
| | 19 | 74 | | }; |
| | | 75 | | |
| | 19 | 76 | | await _dataServices.AddSession(session); |
| | 19 | 77 | | return trackingKey; |
| | 19 | 78 | | } |
| | | 79 | | |
| | | 80 | | public async Task<long?> CompleteSessionAsync( |
| | | 81 | | string trackingKey, |
| | | 82 | | long transactionId, |
| | | 83 | | int ordersCountFinal, |
| | | 84 | | string? comment, |
| | | 85 | | string transactionUrl) |
| | | 86 | | { |
| | 14 | 87 | | if (string.IsNullOrWhiteSpace(trackingKey)) |
| | 4 | 88 | | return null; |
| | | 89 | | |
| | 10 | 90 | | var session = await _dataServices.GetSessionByTrackingKey(trackingKey); |
| | 10 | 91 | | if (session == null) |
| | 1 | 92 | | return null; |
| | | 93 | | |
| | 9 | 94 | | var existingResult = await _dataServices.GetResultByTrackingKey(trackingKey); |
| | 9 | 95 | | if (existingResult != null) |
| | 1 | 96 | | return existingResult.Id; |
| | | 97 | | |
| | 8 | 98 | | var completedAt = DateTime.UtcNow; |
| | 8 | 99 | | session.IsCompleted = true; |
| | 8 | 100 | | session.IsAbandoned = false; |
| | 8 | 101 | | session.CompletedAtUtc = completedAt; |
| | 8 | 102 | | session.LastSeenAtUtc = completedAt; |
| | 8 | 103 | | await _dataServices.UpdateSession(session); |
| | | 104 | | |
| | 8 | 105 | | var durationSeconds = (int)Math.Max(0, (completedAt - session.StartedAtUtc).TotalSeconds); |
| | 8 | 106 | | var result = new TransactionTimeTrackingResult |
| | 8 | 107 | | { |
| | 8 | 108 | | TrackingSessionId = session.Id, |
| | 8 | 109 | | TrackingKey = session.TrackingKey, |
| | 8 | 110 | | UserName = session.UserName, |
| | 8 | 111 | | AccessProfileId = session.AccessProfileId, |
| | 8 | 112 | | TransactionType = session.TransactionType, |
| | 8 | 113 | | OrdersCountFinal = ordersCountFinal, |
| | 8 | 114 | | StartedAtUtc = session.StartedAtUtc, |
| | 8 | 115 | | CompletedAtUtc = completedAt, |
| | 8 | 116 | | DurationSeconds = durationSeconds, |
| | 8 | 117 | | TransactionId = transactionId, |
| | 8 | 118 | | TransactionUrl = Truncate(transactionUrl, TransactionUrlMaxLength) ?? string.Empty, |
| | 8 | 119 | | Comment = Truncate(comment, CommentMaxLength), |
| | 8 | 120 | | CreatedAtUtc = completedAt |
| | 8 | 121 | | }; |
| | | 122 | | |
| | 8 | 123 | | await _dataServices.AddResult(result); |
| | 8 | 124 | | return result.Id; |
| | 14 | 125 | | } |
| | | 126 | | |
| | | 127 | | public async Task<int> MarkAbandonedAsync(TimeSpan olderThan) |
| | | 128 | | { |
| | 5 | 129 | | var olderThanUtc = DateTime.UtcNow.Subtract(olderThan); |
| | 5 | 130 | | var candidates = await _dataServices.GetAbandonedCandidates(olderThanUtc); |
| | 5 | 131 | | var marked = 0; |
| | | 132 | | |
| | 20 | 133 | | foreach (var session in candidates) |
| | | 134 | | { |
| | 5 | 135 | | session.IsAbandoned = true; |
| | 5 | 136 | | session.LastSeenAtUtc = DateTime.UtcNow; |
| | 5 | 137 | | await _dataServices.UpdateSession(session); |
| | 5 | 138 | | marked++; |
| | | 139 | | } |
| | | 140 | | |
| | 5 | 141 | | return marked; |
| | 5 | 142 | | } |
| | | 143 | | |
| | | 144 | | private static string? Truncate(string? value, int maxLength) |
| | 73 | 145 | | => value is { Length: > 0 } && value.Length > maxLength ? value[..maxLength] : value; |
| | | 146 | | } |