| | | 1 | | using Hangfire; |
| | | 2 | | using Microsoft.AspNetCore.Authorization; |
| | | 3 | | using Microsoft.AspNetCore.Mvc; |
| | | 4 | | using Microsoft.EntityFrameworkCore; |
| | | 5 | | using Newtonsoft.Json; |
| | | 6 | | using OServices.FiltersServices.FilterModels; |
| | | 7 | | using OzonDomains; |
| | | 8 | | using OzonDomains.Models; |
| | | 9 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.EmailRouting; |
| | | 10 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.FileUploadRecordViewModels; |
| | | 11 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.JobLogs; |
| | | 12 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.OrderHistory; |
| | | 13 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.OzonWebhookViewModels; |
| | | 14 | | using Services.OzonWebhookServices.Monitoring; |
| | | 15 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.TransactionJobs; |
| | | 16 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.TransactionTimeTracking; |
| | | 17 | | using OzonOrdersWeb.Areas.Studio2.ViewModels.WarehouseStock; |
| | | 18 | | using OzonOrdersWeb.WebServices.JobLogs; |
| | | 19 | | using OzonOrdersWeb.WebServices.SystemAlerts; |
| | | 20 | | using OzonOrdersWeb.WebServices.TransactionTimeTracking; |
| | | 21 | | using OzonRepositories.Context; |
| | | 22 | | using Services.DataServices; |
| | | 23 | | using Services.ApiServices._1CApi.Models; |
| | | 24 | | using Services.SearchServices.OrderSearch; |
| | | 25 | | |
| | | 26 | | namespace OzonOrdersWeb.Areas.Studio2.Controllers; |
| | | 27 | | |
| | | 28 | | [Authorize(Roles = "User,Admin,SuperAdmin")] |
| | | 29 | | [Area("Studio2")] |
| | | 30 | | [Route("Studio2/JobLogs")] |
| | | 31 | | public partial class JobLogsController : Controller |
| | | 32 | | { |
| | | 33 | | private readonly ApplicationDbContext _context; |
| | | 34 | | private readonly IJobLogsBadgeService _badgeService; |
| | | 35 | | private readonly OzonPushNotificationDataService _webhookDataService; |
| | | 36 | | private readonly OzonClientServices _ozonClientServices; |
| | | 37 | | private readonly EmailAttachmentProcessingResultDataServices _emailResultDataServices; |
| | | 38 | | private readonly EmailRoutingRuleDataServices _emailRulesDataServices; |
| | | 39 | | private readonly ITransactionTimeTrackingReportQueryService _timeTrackingReportQueryService; |
| | | 40 | | private readonly IOrderSearchDiagnosticsService _orderSearchDiagnosticsService; |
| | | 41 | | private readonly IServerDiagnosticsService _serverDiagnosticsService; |
| | | 42 | | private readonly IBackgroundJobClient _backgroundJobClient; |
| | | 43 | | private readonly IOzonWebhookAvailabilityService _webhookAvailability; |
| | | 44 | | private readonly OzonWebhookAvailabilityStore _webhookAvailabilityStore; |
| | | 45 | | |
| | 0 | 46 | | public JobLogsController( |
| | 0 | 47 | | ApplicationDbContext context, |
| | 0 | 48 | | IJobLogsBadgeService badgeService, |
| | 0 | 49 | | OzonPushNotificationDataService webhookDataService, |
| | 0 | 50 | | OzonClientServices ozonClientServices, |
| | 0 | 51 | | EmailAttachmentProcessingResultDataServices emailResultDataServices, |
| | 0 | 52 | | EmailRoutingRuleDataServices emailRulesDataServices, |
| | 0 | 53 | | ITransactionTimeTrackingReportQueryService timeTrackingReportQueryService, |
| | 0 | 54 | | IOrderSearchDiagnosticsService orderSearchDiagnosticsService, |
| | 0 | 55 | | IServerDiagnosticsService serverDiagnosticsService, |
| | 0 | 56 | | IBackgroundJobClient backgroundJobClient, |
| | 0 | 57 | | IOzonWebhookAvailabilityService webhookAvailability, |
| | 0 | 58 | | OzonWebhookAvailabilityStore webhookAvailabilityStore, |
| | 0 | 59 | | SystemAlertIncidentStore alertIncidentStore, |
| | 0 | 60 | | SystemAlertEvaluationService alertEvaluation) |
| | | 61 | | { |
| | 0 | 62 | | _alertIncidentStore = alertIncidentStore; |
| | 0 | 63 | | _alertEvaluation = alertEvaluation; |
| | 0 | 64 | | _webhookAvailability = webhookAvailability; |
| | 0 | 65 | | _webhookAvailabilityStore = webhookAvailabilityStore; |
| | 0 | 66 | | _context = context; |
| | 0 | 67 | | _badgeService = badgeService; |
| | 0 | 68 | | _webhookDataService = webhookDataService; |
| | 0 | 69 | | _ozonClientServices = ozonClientServices; |
| | 0 | 70 | | _emailResultDataServices = emailResultDataServices; |
| | 0 | 71 | | _emailRulesDataServices = emailRulesDataServices; |
| | 0 | 72 | | _timeTrackingReportQueryService = timeTrackingReportQueryService; |
| | 0 | 73 | | _orderSearchDiagnosticsService = orderSearchDiagnosticsService; |
| | 0 | 74 | | _serverDiagnosticsService = serverDiagnosticsService; |
| | 0 | 75 | | _backgroundJobClient = backgroundJobClient; |
| | 0 | 76 | | } |
| | | 77 | | |
| | | 78 | | [HttpGet("badge-summary")] |
| | | 79 | | public async Task<IActionResult> GetBadgeSummary(bool fresh, CancellationToken cancellationToken) |
| | | 80 | | { |
| | 0 | 81 | | var isAdmin = User.IsInRole("Admin") || User.IsInRole("SuperAdmin"); |
| | | 82 | | |
| | | 83 | | // При загрузке/перезагрузке страницы (fresh) админ сразу видит актуальное состояние пуш-адресов, |
| | | 84 | | // а не снимок джоба (раз в 30 минут). Не чаще раза в 20 с и не дольше 6 с ожидания. |
| | 0 | 85 | | if (fresh && isAdmin) |
| | 0 | 86 | | await _webhookAvailability.RefreshIfStaleAsync(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(6)); |
| | | 87 | | |
| | 0 | 88 | | var summary = await _badgeService.GetSummaryAsync(isAdmin, cancellationToken); |
| | 0 | 89 | | return Json(summary); |
| | 0 | 90 | | } |
| | | 91 | | |
| | | 92 | | /// <summary>Старые ссылки /JobLogs без префикса Studio2.</summary> |
| | | 93 | | [HttpGet("/JobLogs")] |
| | | 94 | | [HttpGet("/JobLogs/Index")] |
| | | 95 | | public IActionResult LegacyIndexRedirect() |
| | | 96 | | { |
| | 0 | 97 | | var query = Request.Query.ToDictionary(q => q.Key, q => q.Value.ToString()); |
| | 0 | 98 | | if (query.TryGetValue("txView", out var txView) && |
| | 0 | 99 | | string.Equals(txView, "onec", StringComparison.OrdinalIgnoreCase)) |
| | | 100 | | { |
| | 0 | 101 | | query["tab"] = "onec"; |
| | 0 | 102 | | query.Remove("txView"); |
| | | 103 | | } |
| | | 104 | | |
| | 0 | 105 | | var path = "/Studio2/JobLogs?" + string.Join("&", |
| | 0 | 106 | | query.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}")); |
| | 0 | 107 | | return Redirect(path); |
| | | 108 | | } |
| | | 109 | | |
| | | 110 | | [HttpGet("")] |
| | | 111 | | public async Task<IActionResult> Index( |
| | | 112 | | string tab = "transactions", |
| | | 113 | | long? executionId = null, |
| | | 114 | | int? scheduleId = null, |
| | | 115 | | string? status = null, |
| | | 116 | | string? triggerType = null, |
| | | 117 | | string? orderNumber = null, |
| | | 118 | | DateTime? from = null, |
| | | 119 | | DateTime? to = null, |
| | | 120 | | int page = 1, |
| | | 121 | | int pageSize = 50, |
| | | 122 | | string? messageType = null, |
| | | 123 | | int? clientId = null, |
| | | 124 | | string? processingStatus = null, |
| | | 125 | | string? postingNumber = null, |
| | | 126 | | int webhookTake = 200, |
| | | 127 | | int emailPage = 1, |
| | | 128 | | int emailPageSize = 25, |
| | | 129 | | int? ruleId = null, |
| | | 130 | | string? emailStatus = null, |
| | | 131 | | string? sender = null, |
| | | 132 | | string? fileName = null, |
| | | 133 | | DateTime? dateFrom = null, |
| | | 134 | | DateTime? dateTo = null, |
| | | 135 | | int filePage = 1, |
| | | 136 | | int filePageSize = 15, |
| | | 137 | | int stockPage = 1, |
| | | 138 | | int stockPageSize = 50, |
| | | 139 | | string? stockSourceType = null, |
| | | 140 | | string? stockSyncStatus = null, |
| | | 141 | | DateTime? stockFrom = null, |
| | | 142 | | DateTime? stockTo = null, |
| | | 143 | | string? nameFilter = null, |
| | | 144 | | DateTime? fileDateFrom = null, |
| | | 145 | | DateTime? fileDateTo = null, |
| | | 146 | | int onecPage = 1, |
| | | 147 | | int onecPageSize = 50, |
| | | 148 | | DateTime? onecFrom = null, |
| | | 149 | | DateTime? onecTo = null, |
| | | 150 | | bool? onecSuccess = null, |
| | | 151 | | string? onecChangedBy = null, |
| | | 152 | | long? onecExecutionId = null, |
| | | 153 | | DateTime? dateFromUtc = null, |
| | | 154 | | DateTime? dateToUtc = null, |
| | | 155 | | string? userName = null, |
| | | 156 | | string? transactionType = null, |
| | | 157 | | bool? alertActive = null, |
| | | 158 | | string? alertDomain = null, |
| | | 159 | | string? alertSeverity = null, |
| | | 160 | | CancellationToken cancellationToken = default) |
| | | 161 | | { |
| | | 162 | | // Хвосты статусов переехали на /Studio2/OrderControl — NormalizeTab это значение не знает, |
| | | 163 | | // поэтому проверяем сырой параметр до нормализации. |
| | 0 | 164 | | if (string.Equals(tab, "missingstatus", StringComparison.OrdinalIgnoreCase)) |
| | 0 | 165 | | return RedirectToAction("Index", "OrderControl", new { area = "Studio2", missingStatus = true }); |
| | | 166 | | |
| | 0 | 167 | | var activeTab = NormalizeTab(tab); |
| | | 168 | | |
| | | 169 | | // Обнуление и накат остатков живут на странице, откуда их запускают. |
| | 0 | 170 | | if (activeTab is "clear" or "push") |
| | 0 | 171 | | return RedirectToAction("Index", "BitrixStock", new |
| | 0 | 172 | | { |
| | 0 | 173 | | area = "Studio2", |
| | 0 | 174 | | tab = activeTab == "clear" ? "clearHistory" : "pushHistory", |
| | 0 | 175 | | executionId |
| | 0 | 176 | | }); |
| | | 177 | | |
| | 0 | 178 | | var isAdmin = User.IsInRole("Admin") || User.IsInRole("SuperAdmin"); |
| | 0 | 179 | | if ((activeTab is "transactions" or "onec") && !isAdmin) |
| | 0 | 180 | | activeTab = "timetracking"; |
| | | 181 | | |
| | 0 | 182 | | var vm = new JobLogsPageViewModel |
| | 0 | 183 | | { |
| | 0 | 184 | | ActiveTab = activeTab, |
| | 0 | 185 | | HighlightExecutionId = executionId, |
| | 0 | 186 | | ShowTransactionsTab = isAdmin, |
| | 0 | 187 | | AlertActive = alertActive, |
| | 0 | 188 | | AlertDomain = alertDomain ?? string.Empty, |
| | 0 | 189 | | AlertSeverity = alertSeverity ?? string.Empty |
| | 0 | 190 | | }; |
| | | 191 | | |
| | 0 | 192 | | switch (activeTab) |
| | | 193 | | { |
| | 0 | 194 | | case "transactions" when isAdmin: |
| | 0 | 195 | | vm.Transactions = await LoadTransactionsTabAsync( |
| | 0 | 196 | | scheduleId, status, triggerType, orderNumber, from, to, page, pageSize, cancellationToken); |
| | 0 | 197 | | vm.Transactions.HighlightExecutionId = executionId; |
| | 0 | 198 | | break; |
| | 0 | 199 | | case "onec" when isAdmin: |
| | 0 | 200 | | vm.OneC = await LoadOneCTabAsync( |
| | 0 | 201 | | onecPage, onecPageSize, onecFrom, onecTo, onecSuccess, onecChangedBy, |
| | 0 | 202 | | onecExecutionId ?? executionId, |
| | 0 | 203 | | cancellationToken); |
| | 0 | 204 | | break; |
| | | 205 | | case "timetracking": |
| | 0 | 206 | | vm.TimeTracking = await _timeTrackingReportQueryService.GetAsync( |
| | 0 | 207 | | dateFromUtc, dateToUtc, userName, transactionType, cancellationToken); |
| | 0 | 208 | | break; |
| | | 209 | | case "webhook": |
| | 0 | 210 | | vm.Webhook = await LoadWebhookTabAsync( |
| | 0 | 211 | | messageType, clientId, processingStatus, postingNumber, from, to, webhookTake, cancellationToken); |
| | 0 | 212 | | break; |
| | | 213 | | case "email": |
| | 0 | 214 | | vm.EmailRouting = await LoadEmailRoutingTabAsync(); |
| | 0 | 215 | | break; |
| | | 216 | | case "fileupload": |
| | 0 | 217 | | vm.FileUpload = await LoadFileUploadTabAsync( |
| | 0 | 218 | | filePage, filePageSize, nameFilter, fileDateFrom, fileDateTo, cancellationToken); |
| | 0 | 219 | | break; |
| | | 220 | | case "stocksync": |
| | 0 | 221 | | vm.StockSync = await LoadStockSyncTabAsync( |
| | 0 | 222 | | stockPage, stockPageSize, stockSourceType, stockSyncStatus, stockFrom, stockTo, cancellationToken); |
| | 0 | 223 | | break; |
| | | 224 | | case "ordersearch": |
| | 0 | 225 | | vm.OrderSearch = await LoadOrderSearchTabAsync(cancellationToken); |
| | 0 | 226 | | break; |
| | | 227 | | case "server": |
| | 0 | 228 | | vm.ServerDiagnostics = new JobLogsServerDiagnosticsTabViewModel |
| | 0 | 229 | | { |
| | 0 | 230 | | Snapshot = await _serverDiagnosticsService.GetSnapshotAsync(cancellationToken) |
| | 0 | 231 | | }; |
| | 0 | 232 | | break; |
| | 0 | 233 | | case "alerts" when isAdmin: |
| | | 234 | | // Строки системных инцидентов подгружаются серверным AG Grid блоками. |
| | | 235 | | break; |
| | | 236 | | } |
| | 0 | 237 | | if (isAdmin) |
| | 0 | 238 | | vm.CurrentSystemAlerts = await _badgeService.GetSystemAlertsAsync(cancellationToken); |
| | | 239 | | |
| | 0 | 240 | | return View(vm); |
| | 0 | 241 | | } |
| | | 242 | | |
| | | 243 | | [HttpGet("ordersearch/snapshot")] |
| | | 244 | | public async Task<IActionResult> OrderSearchSnapshot(CancellationToken cancellationToken) |
| | | 245 | | { |
| | 0 | 246 | | var snapshot = await _orderSearchDiagnosticsService.GetSnapshotAsync(cancellationToken); |
| | 0 | 247 | | return Json(snapshot); |
| | 0 | 248 | | } |
| | | 249 | | |
| | | 250 | | [HttpGet("ordersearch/lookup")] |
| | | 251 | | public async Task<IActionResult> OrderSearchLookup( |
| | | 252 | | int? orderId, |
| | | 253 | | string? shipmentNumber, |
| | | 254 | | CancellationToken cancellationToken) |
| | | 255 | | { |
| | 0 | 256 | | var hasId = orderId is > 0; |
| | 0 | 257 | | var hasShipment = !string.IsNullOrWhiteSpace(shipmentNumber); |
| | | 258 | | |
| | 0 | 259 | | if (!hasId && !hasShipment) |
| | | 260 | | { |
| | 0 | 261 | | return BadRequest(new { error = "Укажите ID заказа или номер отправления" }); |
| | | 262 | | } |
| | | 263 | | |
| | 0 | 264 | | if (hasId && hasShipment) |
| | | 265 | | { |
| | 0 | 266 | | return BadRequest(new { error = "Укажите только ID или только номер отправления" }); |
| | | 267 | | } |
| | | 268 | | |
| | 0 | 269 | | var result = await _orderSearchDiagnosticsService.LookupAsync(orderId, shipmentNumber, cancellationToken); |
| | 0 | 270 | | if (result.Items.Count == 0) |
| | | 271 | | { |
| | 0 | 272 | | return NotFound(new { error = "Заказы не найдены", queryLabel = result.QueryLabel }); |
| | | 273 | | } |
| | | 274 | | |
| | 0 | 275 | | return Json(result); |
| | 0 | 276 | | } |
| | | 277 | | |
| | | 278 | | [Authorize(Roles = "Admin")] |
| | | 279 | | [HttpPost("ordersearch/repair-drift")] |
| | | 280 | | public IActionResult EnqueueOrderSearchDriftRepair() |
| | | 281 | | { |
| | 0 | 282 | | var userName = User.Identity?.Name ?? "Admin"; |
| | 0 | 283 | | var hangfireJobId = _backgroundJobClient.Enqueue<OrderSearchDriftRepairJob>( |
| | 0 | 284 | | job => job.ExecuteAsync(userName)); |
| | | 285 | | |
| | 0 | 286 | | return Json(new |
| | 0 | 287 | | { |
| | 0 | 288 | | message = "Задача исправления расхождения поставлена в очередь Hangfire. Обновите страницу через 1–2 минуты. |
| | 0 | 289 | | hangfireJobId, |
| | 0 | 290 | | jobId = OrderSearchDriftRepairJob.JobId |
| | 0 | 291 | | }); |
| | | 292 | | } |
| | | 293 | | |
| | | 294 | | [Authorize(Roles = "Admin")] |
| | | 295 | | [HttpPost("ordersearch/full-reindex")] |
| | | 296 | | public IActionResult EnqueueOrderSearchFullReindex() |
| | | 297 | | { |
| | 0 | 298 | | var hangfireJobId = _backgroundJobClient.Enqueue<OrderSearchFullReindexJob>( |
| | 0 | 299 | | job => job.ExecuteAsync()); |
| | | 300 | | |
| | 0 | 301 | | return Json(new |
| | 0 | 302 | | { |
| | 0 | 303 | | message = "Полная переиндексация поставлена в очередь Hangfire. Это может занять несколько минут.", |
| | 0 | 304 | | hangfireJobId, |
| | 0 | 305 | | jobId = OrderSearchFullReindexJob.JobId |
| | 0 | 306 | | }); |
| | | 307 | | } |
| | | 308 | | |
| | | 309 | | /// <summary> |
| | | 310 | | /// Полный ответ 1С по проведению: то, что <c>ErrorSummary</c> (≤2000 символов) не вмещает. Берётся из |
| | | 311 | | /// <c>DetailsJson.responses[].errorDetail</c>, отдельных колонок в БД не заводилось. |
| | | 312 | | /// </summary> |
| | | 313 | | [Authorize(Roles = "Admin")] |
| | | 314 | | [HttpGet("onec/postings/{postingId:long}/error-detail")] |
| | | 315 | | public async Task<IActionResult> PostingErrorDetail(long postingId, CancellationToken cancellationToken) |
| | | 316 | | { |
| | 0 | 317 | | var posting = await _context.OrderHistoryOneCPostings |
| | 0 | 318 | | .AsNoTracking() |
| | 0 | 319 | | .Where(p => p.Id == postingId) |
| | 0 | 320 | | .Select(p => new { p.Id, p.ErrorSummary, p.DetailsJson }) |
| | 0 | 321 | | .FirstOrDefaultAsync(cancellationToken); |
| | | 322 | | |
| | 0 | 323 | | if (posting == null) |
| | 0 | 324 | | return NotFound(); |
| | | 325 | | |
| | 0 | 326 | | return Json(new |
| | 0 | 327 | | { |
| | 0 | 328 | | success = true, |
| | 0 | 329 | | id = posting.Id, |
| | 0 | 330 | | errorSummary = posting.ErrorSummary, |
| | 0 | 331 | | items = GetErrorDetails(posting.DetailsJson) |
| | 0 | 332 | | }); |
| | 0 | 333 | | } |
| | | 334 | | |
| | | 335 | | [Authorize(Roles = "Admin")] |
| | | 336 | | [HttpGet("onec/postings/{postingId:long}/orders")] |
| | | 337 | | [HttpGet("transactions/postings/{postingId:long}/orders")] |
| | | 338 | | public async Task<IActionResult> PostingOrders(long postingId, CancellationToken cancellationToken) |
| | | 339 | | { |
| | 0 | 340 | | var posting = await _context.OrderHistoryOneCPostings |
| | 0 | 341 | | .AsNoTracking() |
| | 0 | 342 | | .FirstOrDefaultAsync(p => p.Id == postingId, cancellationToken); |
| | | 343 | | |
| | 0 | 344 | | if (posting == null) |
| | 0 | 345 | | return NotFound(); |
| | | 346 | | |
| | 0 | 347 | | if (posting.TransactionId is > 0) |
| | | 348 | | { |
| | 0 | 349 | | var executionId = await _context.BackgroundTransactionResults |
| | 0 | 350 | | .AsNoTracking() |
| | 0 | 351 | | .Where(r => r.TransactionId == posting.TransactionId) |
| | 0 | 352 | | .OrderByDescending(r => r.CreatedAt) |
| | 0 | 353 | | .Select(r => (long?)r.ExecutionId) |
| | 0 | 354 | | .FirstOrDefaultAsync(cancellationToken); |
| | | 355 | | |
| | 0 | 356 | | if (executionId is > 0) |
| | | 357 | | { |
| | 0 | 358 | | return RedirectToAction("ExecutionOrders", "TransactionJobs", new { area = "Studio2", id = executionId.V |
| | | 359 | | } |
| | | 360 | | } |
| | | 361 | | |
| | 0 | 362 | | var shipmentNumber = await _context.Orders |
| | 0 | 363 | | .AsNoTracking() |
| | 0 | 364 | | .Where(o => o.Id == posting.OrderId) |
| | 0 | 365 | | .Select(o => o.ShipmentNumber) |
| | 0 | 366 | | .FirstOrDefaultAsync(cancellationToken); |
| | | 367 | | |
| | 0 | 368 | | var filterData = new OrderFilterModel |
| | 0 | 369 | | { |
| | 0 | 370 | | ShipmentNumber = string.IsNullOrWhiteSpace(shipmentNumber) ? null : shipmentNumber.Trim(), |
| | 0 | 371 | | GroupByShipment = true, |
| | 0 | 372 | | IncludeArchived = false, |
| | 0 | 373 | | SyncClientStatusesOnLoad = false, |
| | 0 | 374 | | OnlyWithZasyl = false |
| | 0 | 375 | | }; |
| | | 376 | | |
| | 0 | 377 | | Response.Cookies.Append("FilterData", JsonConvert.SerializeObject(filterData)); |
| | 0 | 378 | | return RedirectToAction("Index", "Orders", new { area = "Studio2" }); |
| | 0 | 379 | | } |
| | | 380 | | |
| | | 381 | | private async Task<BackgroundJobsExecutionsPageViewModel> LoadTransactionsTabAsync( |
| | | 382 | | int? scheduleId, |
| | | 383 | | string? status, |
| | | 384 | | string? triggerType, |
| | | 385 | | string? orderNumber, |
| | | 386 | | DateTime? from, |
| | | 387 | | DateTime? to, |
| | | 388 | | int page, |
| | | 389 | | int pageSize, |
| | | 390 | | CancellationToken cancellationToken) |
| | | 391 | | { |
| | 0 | 392 | | page = page < 1 ? 1 : page; |
| | 0 | 393 | | pageSize = pageSize <= 0 ? 50 : Math.Min(pageSize, 200); |
| | | 394 | | |
| | 0 | 395 | | var schedules = await _context.BackgroundJobSchedules |
| | 0 | 396 | | .AsNoTracking() |
| | 0 | 397 | | .Include(s => s.OzonClient) |
| | 0 | 398 | | .OrderBy(s => s.Name) |
| | 0 | 399 | | .ToListAsync(cancellationToken); |
| | | 400 | | |
| | 0 | 401 | | var query = _context.BackgroundJobExecutions |
| | 0 | 402 | | .AsNoTracking() |
| | 0 | 403 | | .Include(e => e.Schedule) |
| | 0 | 404 | | .AsQueryable(); |
| | | 405 | | |
| | 0 | 406 | | if (scheduleId.HasValue) |
| | 0 | 407 | | query = query.Where(e => e.ScheduleId == scheduleId.Value); |
| | | 408 | | |
| | 0 | 409 | | if (!string.IsNullOrWhiteSpace(status)) |
| | 0 | 410 | | query = query.Where(e => e.Status == status); |
| | | 411 | | |
| | 0 | 412 | | if (!string.IsNullOrWhiteSpace(triggerType)) |
| | 0 | 413 | | query = query.Where(e => e.TriggerType == triggerType); |
| | | 414 | | |
| | 0 | 415 | | OrderExecutionLookupViewModel? orderLookup = null; |
| | 0 | 416 | | if (!string.IsNullOrWhiteSpace(orderNumber)) |
| | | 417 | | { |
| | 0 | 418 | | orderLookup = await BuildOrderExecutionLookupAsync(orderNumber.Trim(), cancellationToken); |
| | 0 | 419 | | var executionIds = orderLookup.ExecutionIds; |
| | 0 | 420 | | query = executionIds.Count > 0 |
| | 0 | 421 | | ? query.Where(e => executionIds.Contains(e.Id)) |
| | 0 | 422 | | : query.Where(e => false); |
| | 0 | 423 | | } |
| | | 424 | | |
| | 0 | 425 | | if (from.HasValue) |
| | 0 | 426 | | query = query.Where(e => e.StartedAt >= from.Value); |
| | | 427 | | |
| | 0 | 428 | | if (to.HasValue) |
| | | 429 | | { |
| | 0 | 430 | | var toExclusive = to.Value.Date.AddDays(1); |
| | 0 | 431 | | query = query.Where(e => e.StartedAt < toExclusive); |
| | | 432 | | } |
| | | 433 | | |
| | 0 | 434 | | var totalCount = await query.CountAsync(cancellationToken); |
| | 0 | 435 | | var executions = await query |
| | 0 | 436 | | .OrderByDescending(e => e.StartedAt) |
| | 0 | 437 | | .Skip((page - 1) * pageSize) |
| | 0 | 438 | | .Take(pageSize) |
| | 0 | 439 | | .ToListAsync(cancellationToken); |
| | | 440 | | |
| | 0 | 441 | | return new BackgroundJobsExecutionsPageViewModel |
| | 0 | 442 | | { |
| | 0 | 443 | | Schedules = schedules, |
| | 0 | 444 | | Executions = executions, |
| | 0 | 445 | | ScheduleId = scheduleId, |
| | 0 | 446 | | Status = status, |
| | 0 | 447 | | TriggerType = triggerType, |
| | 0 | 448 | | OrderNumber = orderNumber, |
| | 0 | 449 | | OrderLookup = orderLookup, |
| | 0 | 450 | | From = from, |
| | 0 | 451 | | To = to, |
| | 0 | 452 | | Page = page, |
| | 0 | 453 | | PageSize = pageSize, |
| | 0 | 454 | | TotalCount = totalCount |
| | 0 | 455 | | }; |
| | 0 | 456 | | } |
| | | 457 | | |
| | | 458 | | private const int OrderLookupOrdersLimit = 50; |
| | | 459 | | private const int OrderLookupMatchesLimit = 200; |
| | | 460 | | |
| | | 461 | | /// <summary> |
| | | 462 | | /// Находит заказ по номеру отправления или Id и собирает, в каких транзакциях он был проведён |
| | | 463 | | /// (фоновой задачей и вручную) и в какие фоновые запуски попадал при отборе. |
| | | 464 | | /// </summary> |
| | | 465 | | private async Task<OrderExecutionLookupViewModel> BuildOrderExecutionLookupAsync( |
| | | 466 | | string query, |
| | | 467 | | CancellationToken cancellationToken) |
| | | 468 | | { |
| | 0 | 469 | | var lookup = new OrderExecutionLookupViewModel { Query = query }; |
| | 0 | 470 | | var queryAsOrderId = int.TryParse(query, out var parsedId) ? parsedId : (int?)null; |
| | | 471 | | |
| | 0 | 472 | | var orders = await _context.Orders |
| | 0 | 473 | | .AsNoTracking() |
| | 0 | 474 | | .Include(o => o.AppStatus) |
| | 0 | 475 | | .Include(o => o.ClientStatus) |
| | 0 | 476 | | .Include(o => o.OzonClient) |
| | 0 | 477 | | .Include(o => o.ShipmentWarehouse) |
| | 0 | 478 | | .Where(o => (o.ShipmentNumber != null && o.ShipmentNumber.Contains(query)) || |
| | 0 | 479 | | (queryAsOrderId != null && o.Id == queryAsOrderId)) |
| | 0 | 480 | | .OrderByDescending(o => o.Id) |
| | 0 | 481 | | .Take(OrderLookupOrdersLimit) |
| | 0 | 482 | | .ToListAsync(cancellationToken); |
| | | 483 | | |
| | | 484 | | var orderIds = orders.Select(o => o.Id).ToList(); |
| | | 485 | | var shipmentByOrderId = orders.ToDictionary(o => o.Id, o => o.ShipmentNumber); |
| | | 486 | | |
| | 0 | 487 | | var results = orderIds.Count == 0 |
| | 0 | 488 | | ? new List<BackgroundTransactionResult>() |
| | 0 | 489 | | : await _context.BackgroundTransactionResults |
| | 0 | 490 | | .AsNoTracking() |
| | 0 | 491 | | .Include(r => r.Execution) |
| | 0 | 492 | | .ThenInclude(e => e.Schedule) |
| | 0 | 493 | | .Where(r => orderIds.Contains(r.OrderId)) |
| | 0 | 494 | | .OrderByDescending(r => r.CreatedAt) |
| | 0 | 495 | | .ToListAsync(cancellationToken); |
| | | 496 | | |
| | 0 | 497 | | var postings = orderIds.Count == 0 |
| | 0 | 498 | | ? new List<OrderHistoryOneCPosting>() |
| | 0 | 499 | | : await _context.OrderHistoryOneCPostings |
| | 0 | 500 | | .AsNoTracking() |
| | 0 | 501 | | .Where(p => orderIds.Contains(p.OrderId)) |
| | 0 | 502 | | .OrderByDescending(p => p.PostedAt) |
| | 0 | 503 | | .ToListAsync(cancellationToken); |
| | | 504 | | |
| | 0 | 505 | | var transactionIds = results |
| | | 506 | | .Where(r => r.TransactionId is > 0).Select(r => r.TransactionId!.Value) |
| | | 507 | | .Concat(postings.Where(p => p.TransactionId is > 0).Select(p => p.TransactionId!.Value)) |
| | | 508 | | .Concat(orders.Where(o => o.TransactionId is > 0).Select(o => o.TransactionId!.Value)) |
| | 0 | 509 | | .Distinct() |
| | 0 | 510 | | .ToList(); |
| | | 511 | | |
| | 0 | 512 | | var transactions = transactionIds.Count == 0 |
| | 0 | 513 | | ? new Dictionary<int, Transaction>() |
| | 0 | 514 | | : await _context.Transactions |
| | 0 | 515 | | .AsNoTracking() |
| | 0 | 516 | | .Where(t => transactionIds.Contains(t.Id)) |
| | | 517 | | .ToDictionaryAsync(t => t.Id, cancellationToken); |
| | | 518 | | |
| | | 519 | | Transaction? FindTransaction(int? transactionId) => |
| | 0 | 520 | | transactionId is > 0 && transactions.TryGetValue(transactionId.Value, out var tx) ? tx : null; |
| | | 521 | | |
| | 0 | 522 | | lookup.Orders = orders.Select(o => |
| | 0 | 523 | | { |
| | 0 | 524 | | var tx = FindTransaction(o.TransactionId); |
| | 0 | 525 | | return new OrderExecutionLookupOrderViewModel |
| | 0 | 526 | | { |
| | 0 | 527 | | OrderId = o.Id, |
| | 0 | 528 | | ShipmentNumber = o.ShipmentNumber, |
| | 0 | 529 | | Article = o.Article, |
| | 0 | 530 | | AppStatusName = o.AppStatus?.Name, |
| | 0 | 531 | | ClientStatusName = o.ClientStatus?.Name, |
| | 0 | 532 | | OzonClientName = o.OzonClient?.Name, |
| | 0 | 533 | | ShipmentWarehouseName = o.ShipmentWarehouse?.Name, |
| | 0 | 534 | | TransactionId = o.TransactionId, |
| | 0 | 535 | | TransactionTypeName = tx?.Type?.GetDisplayName(), |
| | 0 | 536 | | TransactionCreatedAt = tx?.CreatedDateTime, |
| | 0 | 537 | | TransactionCreateBy = tx?.CreateBy |
| | 0 | 538 | | }; |
| | 0 | 539 | | }).ToList(); |
| | | 540 | | |
| | | 541 | | // Проведение 1С по паре «заказ + транзакция» дополняет строку фонового результата складами и документами. |
| | 0 | 542 | | var postingByOrderAndTransaction = postings |
| | | 543 | | .Where(p => p.TransactionId is > 0) |
| | | 544 | | .GroupBy(p => (p.OrderId, TransactionId: p.TransactionId!.Value)) |
| | | 545 | | .ToDictionary(g => g.Key, g => g.OrderByDescending(p => p.PostedAt).First()); |
| | | 546 | | |
| | 0 | 547 | | foreach (var result in results) |
| | | 548 | | { |
| | 0 | 549 | | var posting = result.TransactionId is > 0 && |
| | 0 | 550 | | postingByOrderAndTransaction.TryGetValue((result.OrderId, result.TransactionId.Value), out var |
| | 0 | 551 | | ? p |
| | 0 | 552 | | : null; |
| | 0 | 553 | | var tx = FindTransaction(result.TransactionId); |
| | | 554 | | |
| | 0 | 555 | | lookup.Postings.Add(new OrderExecutionLookupPostingViewModel |
| | 0 | 556 | | { |
| | 0 | 557 | | OrderId = result.OrderId, |
| | 0 | 558 | | ShipmentNumber = shipmentByOrderId.GetValueOrDefault(result.OrderId), |
| | 0 | 559 | | ExecutionId = result.ExecutionId, |
| | 0 | 560 | | JobType = result.Execution?.JobType, |
| | 0 | 561 | | ScheduleName = result.Execution?.Schedule?.Name, |
| | 0 | 562 | | TransactionId = result.TransactionId, |
| | 0 | 563 | | TransactionTypeName = tx?.Type?.GetDisplayName(), |
| | 0 | 564 | | PerformedBy = !string.IsNullOrWhiteSpace(tx?.CreateBy) ? tx!.CreateBy : posting?.ChangedBy, |
| | 0 | 565 | | PerformedAt = result.CreatedAt, |
| | 0 | 566 | | WarehouseName = result.WarehouseName, |
| | 0 | 567 | | WarehouseSender = posting?.WarehouseSender, |
| | 0 | 568 | | WarehouseRecipient = posting?.WarehouseRecipient, |
| | 0 | 569 | | DocumentNumbers = !string.IsNullOrWhiteSpace(result.OneCDocumentNumber) |
| | 0 | 570 | | ? result.OneCDocumentNumber |
| | 0 | 571 | | : posting?.DocumentNumbers, |
| | 0 | 572 | | IsPosted = result.IsPosted || (posting?.IsPosted ?? false), |
| | 0 | 573 | | Status = result.Status, |
| | 0 | 574 | | Comment = result.Comment, |
| | 0 | 575 | | ErrorMessage = !string.IsNullOrWhiteSpace(result.ErrorMessage) |
| | 0 | 576 | | ? result.ErrorMessage |
| | 0 | 577 | | : posting?.ErrorSummary |
| | 0 | 578 | | }); |
| | | 579 | | } |
| | | 580 | | |
| | | 581 | | // Ручные проведения — те, что не попали ни в один результат фонового запуска. |
| | 0 | 582 | | var automatedKeys = results |
| | | 583 | | .Where(r => r.TransactionId is > 0) |
| | | 584 | | .Select(r => (r.OrderId, TransactionId: r.TransactionId!.Value)) |
| | 0 | 585 | | .ToHashSet(); |
| | | 586 | | |
| | 0 | 587 | | foreach (var posting in postings) |
| | | 588 | | { |
| | 0 | 589 | | if (posting.TransactionId is > 0 && |
| | 0 | 590 | | automatedKeys.Contains((posting.OrderId, posting.TransactionId.Value))) |
| | | 591 | | continue; |
| | | 592 | | |
| | 0 | 593 | | var tx = FindTransaction(posting.TransactionId); |
| | 0 | 594 | | lookup.Postings.Add(new OrderExecutionLookupPostingViewModel |
| | 0 | 595 | | { |
| | 0 | 596 | | OrderId = posting.OrderId, |
| | 0 | 597 | | ShipmentNumber = shipmentByOrderId.GetValueOrDefault(posting.OrderId), |
| | 0 | 598 | | ExecutionId = null, |
| | 0 | 599 | | TransactionId = posting.TransactionId, |
| | 0 | 600 | | TransactionTypeName = tx?.Type?.GetDisplayName(), |
| | 0 | 601 | | PerformedBy = !string.IsNullOrWhiteSpace(posting.ChangedBy) ? posting.ChangedBy : tx?.CreateBy, |
| | 0 | 602 | | PerformedAt = posting.PostedAt, |
| | 0 | 603 | | WarehouseSender = posting.WarehouseSender, |
| | 0 | 604 | | WarehouseRecipient = posting.WarehouseRecipient, |
| | 0 | 605 | | DocumentNumbers = posting.DocumentNumbers, |
| | 0 | 606 | | IsPosted = posting.IsPosted, |
| | 0 | 607 | | Status = posting.Success ? "PostedTo1C" : "Failed", |
| | 0 | 608 | | ErrorMessage = posting.ErrorSummary |
| | 0 | 609 | | }); |
| | | 610 | | } |
| | | 611 | | |
| | 0 | 612 | | lookup.Postings = lookup.Postings |
| | | 613 | | .OrderByDescending(p => p.PerformedAt ?? DateTime.MinValue) |
| | 0 | 614 | | .ToList(); |
| | | 615 | | |
| | 0 | 616 | | var matches = await _context.BackgroundOrderMatches |
| | 0 | 617 | | .AsNoTracking() |
| | 0 | 618 | | .Include(m => m.Execution) |
| | 0 | 619 | | .ThenInclude(e => e.Schedule) |
| | 0 | 620 | | .Where(m => (m.OrderId != null && orderIds.Contains(m.OrderId.Value)) || m.OrderNumber.Contains(query)) |
| | 0 | 621 | | .OrderByDescending(m => m.CreatedAt) |
| | 0 | 622 | | .Take(OrderLookupMatchesLimit) |
| | 0 | 623 | | .ToListAsync(cancellationToken); |
| | | 624 | | |
| | | 625 | | lookup.Matches = matches.Select(m => new OrderExecutionLookupMatchViewModel |
| | | 626 | | { |
| | | 627 | | ExecutionId = m.ExecutionId, |
| | | 628 | | JobType = m.Execution?.JobType, |
| | | 629 | | ScheduleName = m.Execution?.Schedule?.Name, |
| | | 630 | | StartedAt = m.Execution?.StartedAt, |
| | | 631 | | OrderId = m.OrderId, |
| | | 632 | | OrderNumber = m.OrderNumber, |
| | | 633 | | Article = m.Article, |
| | | 634 | | OrderStatus = m.OrderStatus, |
| | | 635 | | IsMatchedInDb = m.IsMatchedInDb, |
| | | 636 | | IsEligibleForProcessing = m.IsEligibleForProcessing, |
| | | 637 | | RejectionReason = m.RejectionReason |
| | | 638 | | }).ToList(); |
| | | 639 | | |
| | | 640 | | lookup.ExecutionIds = results.Select(r => r.ExecutionId) |
| | | 641 | | .Concat(matches.Select(m => m.ExecutionId)) |
| | 0 | 642 | | .Distinct() |
| | 0 | 643 | | .ToList(); |
| | | 644 | | |
| | 0 | 645 | | return lookup; |
| | 0 | 646 | | } |
| | | 647 | | |
| | | 648 | | private async Task<JobLogsOneCPostingsTabViewModel> LoadOneCTabAsync( |
| | | 649 | | int page, |
| | | 650 | | int pageSize, |
| | | 651 | | DateTime? from, |
| | | 652 | | DateTime? to, |
| | | 653 | | bool? success, |
| | | 654 | | string? changedBy, |
| | | 655 | | long? executionId, |
| | | 656 | | CancellationToken cancellationToken) |
| | | 657 | | { |
| | 0 | 658 | | page = page < 1 ? 1 : page; |
| | 0 | 659 | | pageSize = pageSize <= 0 ? 50 : Math.Min(pageSize, 200); |
| | | 660 | | |
| | 0 | 661 | | var vm = new JobLogsOneCPostingsTabViewModel |
| | 0 | 662 | | { |
| | 0 | 663 | | Page = page, |
| | 0 | 664 | | PageSize = pageSize, |
| | 0 | 665 | | From = from, |
| | 0 | 666 | | To = to, |
| | 0 | 667 | | Success = success, |
| | 0 | 668 | | ChangedBy = changedBy, |
| | 0 | 669 | | ExecutionId = executionId |
| | 0 | 670 | | }; |
| | | 671 | | |
| | 0 | 672 | | var query = _context.OrderHistoryOneCPostings.AsNoTracking(); |
| | | 673 | | |
| | 0 | 674 | | if (executionId is > 0) |
| | | 675 | | { |
| | 0 | 676 | | var txIds = await _context.BackgroundTransactionResults |
| | 0 | 677 | | .AsNoTracking() |
| | 0 | 678 | | .Where(r => r.ExecutionId == executionId && r.TransactionId != null) |
| | 0 | 679 | | .Select(r => r.TransactionId!.Value) |
| | 0 | 680 | | .Distinct() |
| | 0 | 681 | | .ToListAsync(cancellationToken); |
| | | 682 | | |
| | 0 | 683 | | if (txIds.Count == 0) |
| | 0 | 684 | | return vm; |
| | | 685 | | |
| | 0 | 686 | | query = query.Where(p => p.TransactionId != null && txIds.Contains(p.TransactionId.Value)); |
| | 0 | 687 | | } |
| | | 688 | | |
| | 0 | 689 | | if (from.HasValue) |
| | 0 | 690 | | query = query.Where(p => p.PostedAt >= from.Value); |
| | | 691 | | |
| | 0 | 692 | | if (to.HasValue) |
| | | 693 | | { |
| | 0 | 694 | | var toExclusive = to.Value.Date.AddDays(1); |
| | 0 | 695 | | query = query.Where(p => p.PostedAt < toExclusive); |
| | | 696 | | } |
| | | 697 | | |
| | 0 | 698 | | if (success.HasValue) |
| | 0 | 699 | | query = query.Where(p => p.Success == success.Value); |
| | | 700 | | |
| | 0 | 701 | | if (!string.IsNullOrWhiteSpace(changedBy)) |
| | | 702 | | { |
| | 0 | 703 | | var by = changedBy.Trim(); |
| | 0 | 704 | | query = query.Where(p => |
| | 0 | 705 | | (p.ChangedBy != null && p.ChangedBy.Contains(by)) || |
| | 0 | 706 | | (p.TransactionId != null && _context.Transactions |
| | 0 | 707 | | .Any(t => t.Id == p.TransactionId && t.CreateBy != null && t.CreateBy.Contains(by)))); |
| | | 708 | | } |
| | | 709 | | |
| | 0 | 710 | | vm.TotalCount = await query.CountAsync(cancellationToken); |
| | | 711 | | |
| | 0 | 712 | | var postings = await query |
| | 0 | 713 | | .OrderByDescending(p => p.PostedAt) |
| | 0 | 714 | | .Skip((page - 1) * pageSize) |
| | 0 | 715 | | .Take(pageSize) |
| | 0 | 716 | | .ToListAsync(cancellationToken); |
| | | 717 | | |
| | 0 | 718 | | var txIdsOnPage = postings |
| | | 719 | | .Where(p => p.TransactionId is > 0) |
| | | 720 | | .Select(p => p.TransactionId!.Value) |
| | 0 | 721 | | .Distinct() |
| | 0 | 722 | | .ToList(); |
| | | 723 | | |
| | 0 | 724 | | var executionByTx = txIdsOnPage.Count == 0 |
| | 0 | 725 | | ? new Dictionary<int, long>() |
| | 0 | 726 | | : await _context.BackgroundTransactionResults |
| | 0 | 727 | | .AsNoTracking() |
| | 0 | 728 | | .Where(r => r.TransactionId != null && txIdsOnPage.Contains(r.TransactionId.Value)) |
| | 0 | 729 | | .GroupBy(r => r.TransactionId!.Value) |
| | 0 | 730 | | .Select(g => new { TransactionId = g.Key, ExecutionId = g.Min(x => x.ExecutionId) }) |
| | | 731 | | .ToDictionaryAsync(x => x.TransactionId, x => x.ExecutionId, cancellationToken); |
| | | 732 | | |
| | 0 | 733 | | var createByByTx = txIdsOnPage.Count == 0 |
| | 0 | 734 | | ? new Dictionary<int, string?>() |
| | 0 | 735 | | : await _context.Transactions |
| | 0 | 736 | | .AsNoTracking() |
| | 0 | 737 | | .Where(t => txIdsOnPage.Contains(t.Id)) |
| | 0 | 738 | | .Select(t => new { t.Id, t.CreateBy }) |
| | | 739 | | .ToDictionaryAsync(t => t.Id, t => t.CreateBy, cancellationToken); |
| | | 740 | | |
| | | 741 | | var orderIds = postings.Select(p => p.OrderId).Distinct().ToList(); |
| | 0 | 742 | | var shipmentsByOrderId = orderIds.Count == 0 |
| | 0 | 743 | | ? new Dictionary<int, string?>() |
| | 0 | 744 | | : await _context.Orders |
| | 0 | 745 | | .AsNoTracking() |
| | 0 | 746 | | .Where(o => orderIds.Contains(o.Id)) |
| | 0 | 747 | | .Select(o => new { o.Id, o.ShipmentNumber }) |
| | | 748 | | .ToDictionaryAsync(o => o.Id, o => o.ShipmentNumber, cancellationToken); |
| | | 749 | | |
| | 0 | 750 | | vm.Postings = postings.Select(p => |
| | 0 | 751 | | { |
| | 0 | 752 | | string? performedBy = !string.IsNullOrWhiteSpace(p.ChangedBy) ? p.ChangedBy.Trim() : null; |
| | 0 | 753 | | if (performedBy == null && p.TransactionId is > 0 && |
| | 0 | 754 | | createByByTx.TryGetValue(p.TransactionId.Value, out var createBy) && |
| | 0 | 755 | | !string.IsNullOrWhiteSpace(createBy)) |
| | 0 | 756 | | { |
| | 0 | 757 | | performedBy = createBy.Trim(); |
| | 0 | 758 | | } |
| | 0 | 759 | | |
| | 0 | 760 | | return new JobLogsOneCPostingRowViewModel |
| | 0 | 761 | | { |
| | 0 | 762 | | Posting = MapOneCPosting(p), |
| | 0 | 763 | | PerformedBy = performedBy, |
| | 0 | 764 | | ExecutionId = p.TransactionId is > 0 && executionByTx.TryGetValue(p.TransactionId.Value, out var execId) |
| | 0 | 765 | | ? execId |
| | 0 | 766 | | : null, |
| | 0 | 767 | | ShipmentNumber = shipmentsByOrderId.GetValueOrDefault(p.OrderId) |
| | 0 | 768 | | }; |
| | 0 | 769 | | }).ToList(); |
| | | 770 | | |
| | 0 | 771 | | return vm; |
| | 0 | 772 | | } |
| | | 773 | | |
| | 0 | 774 | | private static OrderHistoryOneCPostingViewModel MapOneCPosting(OrderHistoryOneCPosting p) => new() |
| | 0 | 775 | | { |
| | 0 | 776 | | Id = p.Id, |
| | 0 | 777 | | OrderId = p.OrderId, |
| | 0 | 778 | | TransactionId = p.TransactionId, |
| | 0 | 779 | | OperationKind = p.OperationKind, |
| | 0 | 780 | | Success = p.Success, |
| | 0 | 781 | | PostedAt = p.PostedAt, |
| | 0 | 782 | | ChangedBy = p.ChangedBy, |
| | 0 | 783 | | HistoryBatchKey = p.HistoryBatchKey, |
| | 0 | 784 | | OrganizationName = p.OrganizationName, |
| | 0 | 785 | | OrganizationInn = p.OrganizationInn, |
| | 0 | 786 | | SenderName = p.SenderName, |
| | 0 | 787 | | SenderInn = p.SenderInn, |
| | 0 | 788 | | WarehouseSender = p.WarehouseSender, |
| | 0 | 789 | | WarehouseRecipient = p.WarehouseRecipient, |
| | 0 | 790 | | OrdersCount = p.OrdersCount, |
| | 0 | 791 | | ProductsCount = p.ProductsCount, |
| | 0 | 792 | | ArticlesSummary = p.ArticlesSummary, |
| | 0 | 793 | | DocumentsCount = p.DocumentsCount, |
| | 0 | 794 | | DocumentNumbers = p.DocumentNumbers, |
| | 0 | 795 | | DocumentGuids = p.DocumentGuids, |
| | 0 | 796 | | IsPosted = p.IsPosted, |
| | 0 | 797 | | ErrorSummary = p.ErrorSummary, |
| | 0 | 798 | | CreatedNomenclature = GetCreatedNomenclature(p.DetailsJson) |
| | 0 | 799 | | }; |
| | | 800 | | |
| | | 801 | | private static List<object> GetErrorDetails(string? detailsJson) |
| | | 802 | | { |
| | 0 | 803 | | if (string.IsNullOrWhiteSpace(detailsJson)) return []; |
| | | 804 | | try |
| | | 805 | | { |
| | 0 | 806 | | var details = JsonConvert.DeserializeAnonymousType(detailsJson, |
| | 0 | 807 | | new { responses = Array.Empty<OneCResponse>() }); |
| | 0 | 808 | | return (details?.responses ?? []) |
| | 0 | 809 | | .Where(r => !string.IsNullOrWhiteSpace(r.ErrorDetail) || !string.IsNullOrWhiteSpace(r.PostingError)) |
| | 0 | 810 | | .Select(r => (object)new |
| | 0 | 811 | | { |
| | 0 | 812 | | documentNumber = r.DocumentNumber, |
| | 0 | 813 | | isPosted = r.IsPosted, |
| | 0 | 814 | | postingError = r.PostingError, |
| | 0 | 815 | | errorDetail = r.ErrorDetail |
| | 0 | 816 | | }) |
| | 0 | 817 | | .ToList(); |
| | | 818 | | } |
| | 0 | 819 | | catch (JsonException) |
| | | 820 | | { |
| | 0 | 821 | | return []; |
| | | 822 | | } |
| | 0 | 823 | | } |
| | | 824 | | |
| | | 825 | | private static string? GetCreatedNomenclature(string? detailsJson) |
| | | 826 | | { |
| | 0 | 827 | | if (string.IsNullOrWhiteSpace(detailsJson)) return null; |
| | | 828 | | try |
| | | 829 | | { |
| | 0 | 830 | | var details = JsonConvert.DeserializeAnonymousType(detailsJson, |
| | 0 | 831 | | new { responses = Array.Empty<OneCResponse>() }); |
| | 0 | 832 | | return string.Join("; ", details?.responses |
| | 0 | 833 | | .Select(r => r.CreatedNomenclature) |
| | 0 | 834 | | .Where(x => !string.IsNullOrWhiteSpace(x)) |
| | 0 | 835 | | .Distinct() ?? []); |
| | | 836 | | } |
| | 0 | 837 | | catch (JsonException) |
| | | 838 | | { |
| | 0 | 839 | | return null; |
| | | 840 | | } |
| | 0 | 841 | | } |
| | | 842 | | |
| | | 843 | | private async Task<OzonWebhookIndexViewModel> LoadWebhookTabAsync( |
| | | 844 | | string? messageType, |
| | | 845 | | int? clientId, |
| | | 846 | | string? processingStatus, |
| | | 847 | | string? postingNumber, |
| | | 848 | | DateTime? from, |
| | | 849 | | DateTime? to, |
| | | 850 | | int take, |
| | | 851 | | CancellationToken cancellationToken) |
| | | 852 | | { |
| | 0 | 853 | | var clients = await _ozonClientServices.GetOzonClients(); |
| | 0 | 854 | | long? sellerId = null; |
| | 0 | 855 | | if (clientId is > 0) |
| | | 856 | | { |
| | 0 | 857 | | var client = clients.FirstOrDefault(c => c.Id == clientId.Value); |
| | 0 | 858 | | if (client?.DecryptClientId != null && long.TryParse(client.DecryptClientId, out var sid)) |
| | 0 | 859 | | sellerId = sid; |
| | | 860 | | } |
| | | 861 | | |
| | 0 | 862 | | var notifications = await _webhookDataService.GetFilteredAsync( |
| | 0 | 863 | | messageType, |
| | 0 | 864 | | sellerId, |
| | 0 | 865 | | processingStatus, |
| | 0 | 866 | | string.IsNullOrWhiteSpace(postingNumber) ? null : postingNumber.Trim(), |
| | 0 | 867 | | from, |
| | 0 | 868 | | to, |
| | 0 | 869 | | Math.Clamp(take, 10, 500), |
| | 0 | 870 | | cancellationToken); |
| | | 871 | | |
| | 0 | 872 | | var messageTypes = await _webhookDataService.GetDistinctMessageTypesAsync(cancellationToken); |
| | | 873 | | |
| | 0 | 874 | | return new OzonWebhookIndexViewModel |
| | 0 | 875 | | { |
| | 0 | 876 | | Notifications = notifications, |
| | 0 | 877 | | AvailableMessageTypes = messageTypes, |
| | | 878 | | Clients = clients.Select(c => new OzonClientFilterItem |
| | | 879 | | { |
| | | 880 | | Id = c.Id, |
| | | 881 | | Name = c.Name, |
| | | 882 | | DecryptClientId = c.DecryptClientId |
| | | 883 | | }).ToList(), |
| | 0 | 884 | | FilterMessageType = messageType, |
| | 0 | 885 | | FilterClientId = clientId, |
| | 0 | 886 | | FilterProcessingStatus = processingStatus, |
| | 0 | 887 | | FilterPostingNumber = postingNumber, |
| | 0 | 888 | | FilterFrom = from, |
| | 0 | 889 | | FilterTo = to, |
| | 0 | 890 | | Take = take, |
| | 0 | 891 | | Availability = _webhookAvailabilityStore.Current |
| | 0 | 892 | | }; |
| | 0 | 893 | | } |
| | | 894 | | |
| | | 895 | | /// <summary>Проверить доступность пуш-адресов Ozon сейчас (иначе снимок обновляется джобом раз в 30 минут).</summar |
| | | 896 | | [Authorize(Roles = "Admin,SuperAdmin")] |
| | | 897 | | [HttpPost("webhook-health/refresh")] |
| | | 898 | | public async Task<IActionResult> RefreshWebhookHealth(CancellationToken cancellationToken) |
| | | 899 | | { |
| | 0 | 900 | | var snapshot = await _webhookAvailability.RefreshAsync(cancellationToken); |
| | 0 | 901 | | return Json(new { success = true, hasProblem = snapshot.HasProblem }); |
| | 0 | 902 | | } |
| | | 903 | | |
| | | 904 | | /// <summary>Включить отправку пушей на адрес, который Ozon отключил (после исправления причины).</summary> |
| | | 905 | | [Authorize(Roles = "Admin,SuperAdmin")] |
| | | 906 | | [HttpPost("webhook-health/enable")] |
| | | 907 | | public async Task<IActionResult> EnableWebhookEndpoint(int clientId, long urlId, CancellationToken cancellationToken |
| | | 908 | | { |
| | 0 | 909 | | if (clientId <= 0 || urlId <= 0) |
| | 0 | 910 | | return BadRequest(new { success = false, message = "Не указан кабинет или адрес." }); |
| | | 911 | | |
| | 0 | 912 | | var error = await _webhookAvailability.EnableAsync(clientId, urlId, cancellationToken); |
| | 0 | 913 | | return error == null |
| | 0 | 914 | | ? Json(new { success = true }) |
| | 0 | 915 | | : Json(new { success = false, message = error }); |
| | 0 | 916 | | } |
| | | 917 | | |
| | | 918 | | /// <summary>Вкладка Email → Dropbox: только счётчики по статусам — строки грузит серверный грид (grid/email).</summ |
| | | 919 | | private async Task<EmailRoutingResultsViewModel> LoadEmailRoutingTabAsync() |
| | | 920 | | { |
| | 0 | 921 | | var statusCounts = await _emailResultDataServices.GetStatusCounts(); |
| | 0 | 922 | | return new EmailRoutingResultsViewModel |
| | 0 | 923 | | { |
| | 0 | 924 | | CountUploaded = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Uploaded), |
| | 0 | 925 | | CountOrdersImported = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.OrdersImported), |
| | 0 | 926 | | CountRejected = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.RejectedByRule), |
| | 0 | 927 | | CountFailed = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Failed), |
| | 0 | 928 | | CountDuplicate = statusCounts.GetValueOrDefault(EmailAttachmentProcessingStatus.Duplicate) |
| | 0 | 929 | | }; |
| | 0 | 930 | | } |
| | | 931 | | |
| | | 932 | | private async Task<FileUploadRecordViewModel> LoadFileUploadTabAsync( |
| | | 933 | | int pageNumber, |
| | | 934 | | int pageSize, |
| | | 935 | | string? nameFilter, |
| | | 936 | | DateTime? dateFrom, |
| | | 937 | | DateTime? dateTo, |
| | | 938 | | CancellationToken cancellationToken) |
| | | 939 | | { |
| | 0 | 940 | | pageNumber = pageNumber < 1 ? 1 : pageNumber; |
| | 0 | 941 | | pageSize = pageSize <= 0 ? 15 : Math.Min(pageSize, 100); |
| | | 942 | | |
| | 0 | 943 | | var query = _context.FileUploadRecords.AsNoTracking(); |
| | | 944 | | |
| | 0 | 945 | | if (!string.IsNullOrWhiteSpace(nameFilter)) |
| | 0 | 946 | | query = query.Where(x => x.FileName.Contains(nameFilter)); |
| | | 947 | | |
| | 0 | 948 | | if (dateFrom.HasValue) |
| | 0 | 949 | | query = query.Where(x => x.Date >= dateFrom.Value); |
| | | 950 | | |
| | 0 | 951 | | if (dateTo.HasValue) |
| | 0 | 952 | | query = query.Where(x => x.Date <= dateTo.Value); |
| | | 953 | | |
| | 0 | 954 | | var totalCount = await query.CountAsync(cancellationToken); |
| | 0 | 955 | | var totalPages = totalCount == 0 ? 1 : (int)Math.Ceiling(totalCount / (double)pageSize); |
| | 0 | 956 | | var items = await query |
| | 0 | 957 | | .OrderByDescending(x => x.Date) |
| | 0 | 958 | | .Skip((pageNumber - 1) * pageSize) |
| | 0 | 959 | | .Take(pageSize) |
| | 0 | 960 | | .ToListAsync(cancellationToken); |
| | | 961 | | |
| | 0 | 962 | | return new FileUploadRecordViewModel |
| | 0 | 963 | | { |
| | 0 | 964 | | Items = items, |
| | 0 | 965 | | PageNumber = pageNumber, |
| | 0 | 966 | | PageSize = pageSize, |
| | 0 | 967 | | TotalPages = totalPages, |
| | 0 | 968 | | TotalCount = totalCount, |
| | 0 | 969 | | NameFilter = nameFilter ?? string.Empty, |
| | 0 | 970 | | DateFromFilter = dateFrom, |
| | 0 | 971 | | DateToFilter = dateTo |
| | 0 | 972 | | }; |
| | 0 | 973 | | } |
| | | 974 | | |
| | | 975 | | private async Task<WarehouseStockSyncRunsPageViewModel> LoadStockSyncTabAsync( |
| | | 976 | | int page, |
| | | 977 | | int pageSize, |
| | | 978 | | string? sourceType, |
| | | 979 | | string? status, |
| | | 980 | | DateTime? from, |
| | | 981 | | DateTime? to, |
| | | 982 | | CancellationToken cancellationToken) |
| | | 983 | | { |
| | 0 | 984 | | page = page < 1 ? 1 : page; |
| | 0 | 985 | | pageSize = pageSize <= 0 ? 50 : Math.Min(pageSize, 200); |
| | | 986 | | |
| | 0 | 987 | | var query = _context.WarehouseStockSyncRuns |
| | 0 | 988 | | .AsNoTracking() |
| | 0 | 989 | | .AsQueryable(); |
| | | 990 | | |
| | 0 | 991 | | if (!string.IsNullOrWhiteSpace(sourceType)) |
| | 0 | 992 | | query = query.Where(x => x.SourceType == sourceType); |
| | 0 | 993 | | if (!string.IsNullOrWhiteSpace(status)) |
| | 0 | 994 | | query = query.Where(x => x.Status == status); |
| | 0 | 995 | | if (from.HasValue) |
| | 0 | 996 | | query = query.Where(x => x.StartedAtUtc >= from.Value); |
| | 0 | 997 | | if (to.HasValue) |
| | | 998 | | { |
| | 0 | 999 | | var toExclusive = to.Value.Date.AddDays(1); |
| | 0 | 1000 | | query = query.Where(x => x.StartedAtUtc < toExclusive); |
| | | 1001 | | } |
| | | 1002 | | |
| | 0 | 1003 | | var totalCount = await query.CountAsync(cancellationToken); |
| | 0 | 1004 | | var runs = await query |
| | 0 | 1005 | | .OrderByDescending(x => x.StartedAtUtc) |
| | 0 | 1006 | | .Skip((page - 1) * pageSize) |
| | 0 | 1007 | | .Take(pageSize) |
| | 0 | 1008 | | .ToListAsync(cancellationToken); |
| | | 1009 | | |
| | 0 | 1010 | | return new WarehouseStockSyncRunsPageViewModel |
| | 0 | 1011 | | { |
| | 0 | 1012 | | Runs = runs, |
| | 0 | 1013 | | SourceType = sourceType, |
| | 0 | 1014 | | Status = status, |
| | 0 | 1015 | | From = from, |
| | 0 | 1016 | | To = to, |
| | 0 | 1017 | | Page = page, |
| | 0 | 1018 | | PageSize = pageSize, |
| | 0 | 1019 | | TotalCount = totalCount |
| | 0 | 1020 | | }; |
| | 0 | 1021 | | } |
| | | 1022 | | |
| | | 1023 | | private async Task<JobLogsOrderSearchTabViewModel> LoadOrderSearchTabAsync(CancellationToken cancellationToken) |
| | | 1024 | | { |
| | 0 | 1025 | | var snapshot = await _orderSearchDiagnosticsService.GetSnapshotAsync(cancellationToken); |
| | 0 | 1026 | | return new JobLogsOrderSearchTabViewModel { Snapshot = snapshot }; |
| | 0 | 1027 | | } |
| | | 1028 | | |
| | 0 | 1029 | | private static string NormalizeTab(string? tab) => tab?.Trim().ToLowerInvariant() switch |
| | 0 | 1030 | | { |
| | 0 | 1031 | | // clear/push оставлены только ради редиректа старых закладок на /Studio2/BitrixStock |
| | 0 | 1032 | | "clear" or "clearhistory" => "clear", |
| | 0 | 1033 | | "push" or "pushhistory" => "push", |
| | 0 | 1034 | | "transactions" or "transaction" => "transactions", |
| | 0 | 1035 | | "onec" or "onecpostings" => "onec", |
| | 0 | 1036 | | "timetracking" or "transactiontimetracking" or "transactiontimetrackingreport" => "timetracking", |
| | 0 | 1037 | | "webhook" or "ozonwebhooklog" => "webhook", |
| | 0 | 1038 | | "email" or "emailrouting" or "emailroutingresults" => "email", |
| | 0 | 1039 | | "fileupload" or "fileuploadrecords" => "fileupload", |
| | 0 | 1040 | | "stocksync" or "warehousestocksync" => "stocksync", |
| | 0 | 1041 | | "ordersearch" or "elasticsearch" or "orderssearch" => "ordersearch", |
| | 0 | 1042 | | "server" or "serverdiagnostics" or "database" => "server", |
| | 0 | 1043 | | "alerts" or "systemalerts" => "alerts", |
| | 0 | 1044 | | _ => "transactions" |
| | 0 | 1045 | | }; |
| | | 1046 | | } |