| | | 1 | | using System.IO.Compression; |
| | | 2 | | using System.Security.Claims; |
| | | 3 | | using iText.Kernel.Pdf; |
| | | 4 | | using Microsoft.AspNetCore.Mvc; |
| | | 5 | | using Newtonsoft.Json; |
| | | 6 | | using OzonDomains; |
| | | 7 | | using OzonDomains.Models; |
| | | 8 | | using OzonOrdersWeb.Authorization; |
| | | 9 | | using Services.ApiServices.DropBoxApi; |
| | | 10 | | using Services.ApiServices.OzonApi; |
| | | 11 | | using Services.ApiServices.YandexApi; |
| | | 12 | | using Services.LabelServices; |
| | | 13 | | using Services.ParserServices.HelpDictEnum; |
| | | 14 | | using Services.SignalRServices; |
| | | 15 | | |
| | | 16 | | namespace OzonOrdersWeb.Controllers; |
| | | 17 | | |
| | | 18 | | public partial class OrdersController |
| | | 19 | | { |
| | | 20 | | public async Task<IActionResult> PrintOrdersLable(string ids, int page) |
| | | 21 | | |
| | | 22 | | { |
| | | 23 | | List<OzonClient> clients = await _ozonClientServices.GetOzonClients(); |
| | | 24 | | |
| | | 25 | | int[] idArray = ids.Split(',').Select(int.Parse).ToArray(); |
| | | 26 | | |
| | | 27 | | var ordersLables = await _orderServices.GetOrdersNoTracking(idArray); |
| | | 28 | | |
| | | 29 | | Dictionary<string, byte[]> lables = new Dictionary<string, byte[]>(); |
| | | 30 | | |
| | | 31 | | int continueLablesCuont = 0; |
| | | 32 | | |
| | | 33 | | var failedShipments = new List<string>(); |
| | | 34 | | |
| | | 35 | | |
| | | 36 | | // Экран использует устаревший прямой путь без нового JS-пайплайна. Сгруппируем |
| | | 37 | | // заказы по клиенту: экземпляр OzonJsonDataBuilder переключается только между |
| | | 38 | | // группами, а запросы этикеток внутри одной группы независимы и идут параллельно. |
| | | 39 | | const int labelParallelism = 8; |
| | | 40 | | var resultLock = new object(); |
| | | 41 | | |
| | | 42 | | foreach (var clientGroup in ordersLables.GroupBy(order => order.OzonClient?.ClientId)) |
| | | 43 | | { |
| | | 44 | | var client = clients.FirstOrDefault(c => c.ClientId == clientGroup.Key); |
| | | 45 | | if (client == null || string.IsNullOrEmpty(client.DecryptClientId) || |
| | | 46 | | string.IsNullOrEmpty(client.DecryptApiKey)) |
| | | 47 | | { |
| | | 48 | | lock (resultLock) |
| | | 49 | | { |
| | | 50 | | foreach (var order in clientGroup) |
| | | 51 | | { |
| | | 52 | | continueLablesCuont++; |
| | | 53 | | failedShipments.Add($"{order.ShipmentNumber}: Ozon клиент не найден"); |
| | | 54 | | } |
| | | 55 | | } |
| | | 56 | | |
| | | 57 | | continue; |
| | | 58 | | } |
| | | 59 | | |
| | | 60 | | _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 61 | | using var semaphore = new SemaphoreSlim(labelParallelism); |
| | | 62 | | |
| | | 63 | | await Task.WhenAll(clientGroup.Select(async order => |
| | | 64 | | { |
| | | 65 | | await semaphore.WaitAsync(); |
| | | 66 | | try |
| | | 67 | | { |
| | | 68 | | var isRussianPost = order.Delivery?.Provider?.Name?.Contains( |
| | | 69 | | "Почта России", StringComparison.OrdinalIgnoreCase) == true; |
| | | 70 | | var maxAttempts = isRussianPost ? 5 : 3; |
| | | 71 | | var delayMs = isRussianPost ? 1500 : 500; |
| | | 72 | | |
| | | 73 | | byte[]? labelBytes = null; |
| | | 74 | | Exception? lastError = null; |
| | | 75 | | |
| | | 76 | | for (var attempt = 1; attempt <= maxAttempts; attempt++) |
| | | 77 | | { |
| | | 78 | | try |
| | | 79 | | { |
| | | 80 | | labelBytes = await _jsonDataBuilder.GetOrderLabel(order.ShipmentNumber!); |
| | | 81 | | if (labelBytes is { Length: > 0 }) |
| | | 82 | | break; |
| | | 83 | | } |
| | | 84 | | catch (Exception ex) |
| | | 85 | | { |
| | | 86 | | lastError = ex; |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | if (attempt < maxAttempts) |
| | | 90 | | await Task.Delay(delayMs); |
| | | 91 | | } |
| | | 92 | | |
| | | 93 | | lock (resultLock) |
| | | 94 | | { |
| | | 95 | | if (labelBytes is { Length: > 0 }) |
| | | 96 | | { |
| | | 97 | | lables[order.ShipmentNumber!] = labelBytes; |
| | | 98 | | } |
| | | 99 | | else |
| | | 100 | | { |
| | | 101 | | continueLablesCuont++; |
| | | 102 | | var reason = lastError?.Message ?? "этикетка не сформирована (пустой ответ)"; |
| | | 103 | | failedShipments.Add($"{order.ShipmentNumber}: {reason}"); |
| | | 104 | | } |
| | | 105 | | } |
| | | 106 | | } |
| | | 107 | | catch (Exception ex) |
| | | 108 | | { |
| | | 109 | | lock (resultLock) |
| | | 110 | | { |
| | | 111 | | continueLablesCuont++; |
| | | 112 | | failedShipments.Add($"{order.ShipmentNumber}: {ex.Message}"); |
| | | 113 | | } |
| | | 114 | | } |
| | | 115 | | finally |
| | | 116 | | { |
| | | 117 | | semaphore.Release(); |
| | | 118 | | } |
| | | 119 | | })); |
| | | 120 | | } |
| | | 121 | | |
| | | 122 | | |
| | | 123 | | var result = SendFilesToUser(lables); |
| | | 124 | | |
| | | 125 | | if (result == null) |
| | | 126 | | |
| | | 127 | | { |
| | | 128 | | if (Request.Cookies["selectedIds"] != null) |
| | | 129 | | |
| | | 130 | | { |
| | | 131 | | Response.Cookies.Delete("selectedIds"); |
| | | 132 | | } |
| | | 133 | | |
| | | 134 | | |
| | | 135 | | TempData["ErorrResult"] = |
| | | 136 | | $"Не удалось распечатать этикетку для заказов: {string.Join(", ", ordersLables.Select(o => o.Shipmen |
| | | 137 | | $"Печать этикетки возможна только для заказов со статусом <b>'Ожидает отгрузки'</b>"; |
| | | 138 | | |
| | | 139 | | |
| | | 140 | | var errorMsg = |
| | | 141 | | $"Не удалось распечатать этикетки. Печать возможна только для заказов со статусом 'Ожидает отгрузки' |
| | | 142 | | |
| | | 143 | | if (failedShipments.Any()) |
| | | 144 | | |
| | | 145 | | { |
| | | 146 | | errorMsg += $" Ошибки по заказам: {string.Join("; ", failedShipments.Take(5))}" + |
| | | 147 | | (failedShipments.Count > 5 ? " ..." : ""); |
| | | 148 | | } |
| | | 149 | | |
| | | 150 | | |
| | | 151 | | await NotificationService.NotifyAllAsync(errorMsg); |
| | | 152 | | |
| | | 153 | | return RedirectToAction(nameof(System.Index), new { sortOrder = GetSortStateCookie(), page = page }); |
| | | 154 | | } |
| | | 155 | | |
| | | 156 | | |
| | | 157 | | var successCount = lables.Count; |
| | | 158 | | |
| | | 159 | | if (successCount > 0) |
| | | 160 | | |
| | | 161 | | { |
| | | 162 | | var msg = $"<div class='alert alert-success'>" + |
| | | 163 | | $"<strong>Этикетки сформированы</strong><br/>" + |
| | | 164 | | $"Успешно: <b>{successCount}</b>, с ошибками: <b>{continueLablesCuont}</b>." + |
| | | 165 | | $"</div>"; |
| | | 166 | | |
| | | 167 | | await NotificationService.NotifyAllAsync(msg); |
| | | 168 | | |
| | | 169 | | |
| | | 170 | | if (continueLablesCuont > 0 && failedShipments.Any()) |
| | | 171 | | |
| | | 172 | | { |
| | | 173 | | var warn = $"<div class='alert alert-warning'>" + |
| | | 174 | | $"<strong>Часть этикеток не сформирована</strong><br/>" + |
| | | 175 | | $"Ошибки по заказам: {string.Join("; ", failedShipments.Take(5))}" + |
| | | 176 | | (failedShipments.Count > 5 ? " ..." : "") + |
| | | 177 | | $"</div>"; |
| | | 178 | | |
| | | 179 | | await NotificationService.NotifyAllAsync(warn); |
| | | 180 | | } |
| | | 181 | | } |
| | | 182 | | |
| | | 183 | | |
| | | 184 | | ClearSelectedIdsSession(); |
| | | 185 | | |
| | | 186 | | return result; |
| | | 187 | | } |
| | | 188 | | |
| | | 189 | | |
| | | 190 | | [ServiceEndpoint] |
| | | 191 | | [HttpGet] |
| | | 192 | | public async Task<IActionResult> GetOzonLabelEligibleOrders(string ids) |
| | | 193 | | { |
| | | 194 | | if (string.IsNullOrWhiteSpace(ids)) |
| | | 195 | | { |
| | | 196 | | return Json(new { eligibleOrderIds = Array.Empty<int>(), skippedOrders = Array.Empty<object>() }); |
| | | 197 | | } |
| | | 198 | | |
| | | 199 | | var orderIds = ids.Split(',', StringSplitOptions.RemoveEmptyEntries) |
| | | 200 | | .Select(value => int.TryParse(value.Trim(), out var id) ? id : (int?)null) |
| | | 201 | | .OfType<int>() |
| | | 202 | | .Distinct() |
| | | 203 | | .ToArray(); |
| | | 204 | | |
| | | 205 | | if (orderIds.Length == 0) |
| | | 206 | | { |
| | | 207 | | return Json(new { eligibleOrderIds = Array.Empty<int>(), skippedOrders = Array.Empty<object>() }); |
| | | 208 | | } |
| | | 209 | | |
| | | 210 | | var orders = await _orderServices.GetOrdersNoTracking(orderIds); |
| | | 211 | | var eligibleOrderIds = orders |
| | | 212 | | .Where(order => OzonLabelOrderEligibility.IsOzonOrder( |
| | | 213 | | order.OzonClient?.ClientType, |
| | | 214 | | order.ShipmentNumber, |
| | | 215 | | order.OzonClient?.Id)) |
| | | 216 | | .Select(order => order.Id) |
| | | 217 | | .ToArray(); |
| | | 218 | | var skippedOrders = orders |
| | | 219 | | .Where(order => !eligibleOrderIds.Contains(order.Id)) |
| | | 220 | | .Select(order => new |
| | | 221 | | { |
| | | 222 | | orderId = order.Id, |
| | | 223 | | shipmentNumber = order.ShipmentNumber, |
| | | 224 | | clientType = order.OzonClient?.ClientType.ToString(), |
| | | 225 | | clientName = order.OzonClient?.Name |
| | | 226 | | }) |
| | | 227 | | .ToArray(); |
| | | 228 | | |
| | | 229 | | return Json(new { eligibleOrderIds, skippedOrders }); |
| | | 230 | | } |
| | | 231 | | |
| | | 232 | | [ServiceEndpoint] |
| | | 233 | | [HttpGet] |
| | | 234 | | public async Task<IActionResult> GetOrderProducts(string ids) |
| | | 235 | | |
| | | 236 | | { |
| | | 237 | | try |
| | | 238 | | |
| | | 239 | | { |
| | | 240 | | if (string.IsNullOrWhiteSpace(ids)) |
| | | 241 | | |
| | | 242 | | return Json(new { orderProducts = Array.Empty<object>() }); |
| | | 243 | | |
| | | 244 | | |
| | | 245 | | var idArray = ids.Split(',', StringSplitOptions.RemoveEmptyEntries) |
| | | 246 | | .Select(s => int.TryParse(s.Trim(), out var id) ? id : (int?)null) |
| | | 247 | | .Where(id => id.HasValue) |
| | | 248 | | .Select(id => id!.Value) |
| | | 249 | | .ToArray(); |
| | | 250 | | |
| | | 251 | | |
| | | 252 | | if (idArray.Length == 0) |
| | | 253 | | |
| | | 254 | | return Json(new { orderProducts = Array.Empty<object>() }); |
| | | 255 | | |
| | | 256 | | |
| | | 257 | | var clients = await _ozonClientServices.GetOzonClients(); |
| | | 258 | | |
| | | 259 | | var result = new List<object>(); |
| | | 260 | | |
| | | 261 | | // Строки одного отправления (несколько товаров в posting) — один запрос к Ozon. |
| | | 262 | | var loadedPostings = new HashSet<string>(StringComparer.Ordinal); |
| | | 263 | | |
| | | 264 | | foreach (var orderId in idArray) |
| | | 265 | | { |
| | | 266 | | var order = await _orderServices.GetOrder(orderId); |
| | | 267 | | if (order?.OzonClient == null || string.IsNullOrEmpty(order.ShipmentNumber)) |
| | | 268 | | continue; |
| | | 269 | | |
| | | 270 | | var apiPosting = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber); |
| | | 271 | | if (!loadedPostings.Add(apiPosting)) |
| | | 272 | | continue; |
| | | 273 | | |
| | | 274 | | try |
| | | 275 | | { |
| | | 276 | | var client = clients.FirstOrDefault(c => c.ClientId == order.OzonClient.ClientId); |
| | | 277 | | if (client == null || string.IsNullOrEmpty(client.DecryptClientId) || |
| | | 278 | | string.IsNullOrEmpty(client.DecryptApiKey)) |
| | | 279 | | continue; |
| | | 280 | | |
| | | 281 | | _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 282 | | var posting = await _jsonDataBuilder.GetPostingProductsWithDeliveryAsync(apiPosting); |
| | | 283 | | var skus = posting.Products |
| | | 284 | | .Select(p => p["sku"]?.ToObject<long?>() ?? 0) |
| | | 285 | | .Where(sku => sku > 0) |
| | | 286 | | .Distinct() |
| | | 287 | | .ToArray(); |
| | | 288 | | var images = await _jsonDataBuilder.GetPrimaryImagesBySkuAsync(skus); |
| | | 289 | | |
| | | 290 | | result.Add(new |
| | | 291 | | { |
| | | 292 | | orderId = order.Id, |
| | | 293 | | shipmentNumber = apiPosting, |
| | | 294 | | deliveryMethod = posting.DeliveryMethodName, |
| | | 295 | | isRussianPost = OzonDeliveryMethodHelper.IsRussianPost( |
| | | 296 | | posting.DeliveryMethodName, posting.TplProvider), |
| | | 297 | | products = posting.Products.Select(p => new |
| | | 298 | | { |
| | | 299 | | offer_id = p["offer_id"]?.ToString(), |
| | | 300 | | name = p["name"]?.ToString(), |
| | | 301 | | sku = p["sku"]?.ToObject<long?>(), |
| | | 302 | | imageUrl = ResolvePostingProductImage(p, images, order), |
| | | 303 | | quantity = p["quantity"]?.ToObject<int?>(), |
| | | 304 | | price = p["price"]?.ToString(), |
| | | 305 | | currency_code = p["currency_code"]?.ToString() |
| | | 306 | | }).ToList() |
| | | 307 | | }); |
| | | 308 | | } |
| | | 309 | | catch |
| | | 310 | | { |
| | | 311 | | // Пропускаем заказ при ошибке |
| | | 312 | | } |
| | | 313 | | } |
| | | 314 | | |
| | | 315 | | return Json(new { orderProducts = result }); |
| | | 316 | | } |
| | | 317 | | |
| | | 318 | | catch |
| | | 319 | | |
| | | 320 | | { |
| | | 321 | | return Json(new { orderProducts = Array.Empty<object>() }); |
| | | 322 | | } |
| | | 323 | | } |
| | | 324 | | |
| | | 325 | | /// <summary> |
| | | 326 | | /// Картинка товара отправления: главная картинка Ozon по SKU/offer_id, иначе — картинка |
| | | 327 | | /// карточки товара заказа Studio (как в основной таблице), если это тот же SKU. |
| | | 328 | | /// </summary> |
| | | 329 | | private static string? ResolvePostingProductImage( |
| | | 330 | | Newtonsoft.Json.Linq.JObject product, |
| | | 331 | | IReadOnlyDictionary<string, string> images, |
| | | 332 | | Order order) |
| | | 333 | | { |
| | | 334 | | var sku = product["sku"]?.ToString(); |
| | | 335 | | if (!string.IsNullOrEmpty(sku) && images.TryGetValue(sku, out var bySku)) |
| | | 336 | | return bySku; |
| | | 337 | | |
| | | 338 | | var offerId = product["offer_id"]?.ToString(); |
| | | 339 | | if (!string.IsNullOrEmpty(offerId) && images.TryGetValue(offerId, out var byOffer)) |
| | | 340 | | return byOffer; |
| | | 341 | | |
| | | 342 | | return !string.IsNullOrEmpty(sku) && order.ProductInfo?.FbsOzonSkuId == sku |
| | | 343 | | ? order.ProductInfo.ImageUrl |
| | | 344 | | : null; |
| | | 345 | | } |
| | | 346 | | |
| | | 347 | | |
| | | 348 | | [ServiceEndpoint] |
| | | 349 | | [HttpPost] |
| | | 350 | | public async Task<IActionResult> GetOzonCountryRequirements(string ids) |
| | | 351 | | { |
| | | 352 | | var orderIds = ParseOrderIds(ids); |
| | | 353 | | if (orderIds.Length == 0) |
| | | 354 | | return Json(new { success = true, requirements = Array.Empty<object>() }); |
| | | 355 | | |
| | | 356 | | var clients = await _ozonClientServices.GetOzonClients(); |
| | | 357 | | var orders = await _orderServices.GetOrdersNoTracking(orderIds); |
| | | 358 | | var requirements = new List<object>(); |
| | | 359 | | |
| | | 360 | | foreach (var order in orders.Where(order => |
| | | 361 | | OzonLabelOrderEligibility.IsOzonOrder(order.OzonClient?.ClientType, order.ShipmentNumber, |
| | | 362 | | order.OzonClient?.Id) && !string.IsNullOrWhiteSpace(order.ShipmentNumber))) |
| | | 363 | | { |
| | | 364 | | var client = clients.FirstOrDefault(item => item.ClientId == order.OzonClient?.ClientId); |
| | | 365 | | if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) || |
| | | 366 | | string.IsNullOrWhiteSpace(client.DecryptApiKey)) |
| | | 367 | | { |
| | | 368 | | continue; |
| | | 369 | | } |
| | | 370 | | |
| | | 371 | | try |
| | | 372 | | { |
| | | 373 | | _ozonApiDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 374 | | var postingNumber = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber!); |
| | | 375 | | var products = await _ozonApiDataManager.GetFbsProductsRequiringCountryAsync(postingNumber); |
| | | 376 | | requirements.AddRange(products.Select(product => new |
| | | 377 | | { |
| | | 378 | | orderId = order.Id, |
| | | 379 | | shipmentNumber = order.ShipmentNumber, |
| | | 380 | | productId = product["sku"]?.ToObject<long>() ?? 0, |
| | | 381 | | productName = product["name"]?.ToObject<string>() ?? product["offer_id"]?.ToObject<string>() ?? |
| | | 382 | | offerId = product["offer_id"]?.ToObject<string>() ?? string.Empty |
| | | 383 | | }).Where(item => item.productId > 0)); |
| | | 384 | | } |
| | | 385 | | catch |
| | | 386 | | { |
| | | 387 | | // Ошибка Ozon по одному отправлению не должна ронять проверку остальных заказов |
| | | 388 | | // и всю печать — недостающую страну пользователь увидит уже как ошибку отгрузки. |
| | | 389 | | } |
| | | 390 | | } |
| | | 391 | | |
| | | 392 | | return Json(new { success = true, requirements }); |
| | | 393 | | } |
| | | 394 | | |
| | | 395 | | /// <summary> |
| | | 396 | | /// Требования Ozon к данным экземпляров (Честный ЗНАК, ГТД, РНПТ) перед сборкой отправления. |
| | | 397 | | /// Без них /v4/posting/fbs/ship отвечает EXEMPLAR_INFO_NOT_FILLED_COMPLETELY. |
| | | 398 | | /// </summary> |
| | | 399 | | [ServiceEndpoint] |
| | | 400 | | [HttpPost] |
| | | 401 | | public async Task<IActionResult> GetOzonExemplarRequirements(string ids) |
| | | 402 | | { |
| | | 403 | | var orderIds = ParseOrderIds(ids); |
| | | 404 | | if (orderIds.Length == 0) |
| | | 405 | | return Json(new { success = true, requirements = Array.Empty<object>() }); |
| | | 406 | | |
| | | 407 | | var clients = await _ozonClientServices.GetOzonClients(); |
| | | 408 | | var orders = await _orderServices.GetOrdersNoTracking(orderIds); |
| | | 409 | | var requirements = new List<object>(); |
| | | 410 | | |
| | | 411 | | foreach (var ordersByClient in orders.GroupBy(order => order.OzonClient?.ClientId)) |
| | | 412 | | { |
| | | 413 | | var client = clients.FirstOrDefault(item => item.ClientId == ordersByClient.Key); |
| | | 414 | | if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) || |
| | | 415 | | string.IsNullOrWhiteSpace(client.DecryptApiKey)) |
| | | 416 | | { |
| | | 417 | | continue; |
| | | 418 | | } |
| | | 419 | | |
| | | 420 | | _ozonExemplarService.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 421 | | var clientRequirements = await _ozonExemplarService.GetRequirementsAsync(ordersByClient); |
| | | 422 | | |
| | | 423 | | requirements.AddRange(clientRequirements.Select(item => new |
| | | 424 | | { |
| | | 425 | | orderId = item.OrderId, |
| | | 426 | | shipmentNumber = item.ShipmentNumber, |
| | | 427 | | productId = item.Requirement.ProductId, |
| | | 428 | | productName = item.ProductName, |
| | | 429 | | offerId = item.OfferId, |
| | | 430 | | exemplarId = item.Requirement.ExemplarId, |
| | | 431 | | index = item.Requirement.Index, |
| | | 432 | | quantity = item.Requirement.Quantity, |
| | | 433 | | needsMandatoryMark = item.Requirement.NeedsMandatoryMark, |
| | | 434 | | needsGtd = item.Requirement.NeedsGtd, |
| | | 435 | | needsRnpt = item.Requirement.NeedsRnpt, |
| | | 436 | | unsupported = item.Requirement.Unsupported |
| | | 437 | | })); |
| | | 438 | | } |
| | | 439 | | |
| | | 440 | | return Json(new { success = true, requirements }); |
| | | 441 | | } |
| | | 442 | | |
| | | 443 | | /// <summary> |
| | | 444 | | /// Сохраняет данные экземпляров в Ozon и ждёт вердикта о готовности отправления к сборке. |
| | | 445 | | /// Печать продолжается только при ship_available. |
| | | 446 | | /// </summary> |
| | | 447 | | [ServiceEndpoint] |
| | | 448 | | [HttpPost] |
| | | 449 | | public async Task<IActionResult> SetOzonExemplars([FromBody] List<OzonExemplarSubmission>? submissions) |
| | | 450 | | { |
| | | 451 | | if (submissions == null || submissions.Count == 0) |
| | | 452 | | { |
| | | 453 | | return BadRequest(new { success = false, message = "Не переданы данные экземпляров" }); |
| | | 454 | | } |
| | | 455 | | |
| | | 456 | | var orderIds = submissions.Select(item => item.OrderId).Distinct().ToArray(); |
| | | 457 | | var orders = await _orderServices.GetOrdersNoTracking(orderIds); |
| | | 458 | | var clients = await _ozonClientServices.GetOzonClients(); |
| | | 459 | | var ordersById = orders.ToDictionary(order => order.Id); |
| | | 460 | | |
| | | 461 | | var reasons = new List<string>(); |
| | | 462 | | |
| | | 463 | | foreach (var submission in submissions) |
| | | 464 | | { |
| | | 465 | | if (!ordersById.TryGetValue(submission.OrderId, out var order) || |
| | | 466 | | string.IsNullOrWhiteSpace(order.ShipmentNumber)) |
| | | 467 | | { |
| | | 468 | | return BadRequest(new { success = false, message = "Заказ для сохранения экземпляров не найден" }); |
| | | 469 | | } |
| | | 470 | | |
| | | 471 | | var client = clients.FirstOrDefault(item => item.ClientId == order.OzonClient?.ClientId); |
| | | 472 | | if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) || |
| | | 473 | | string.IsNullOrWhiteSpace(client.DecryptApiKey)) |
| | | 474 | | { |
| | | 475 | | return BadRequest(new { success = false, message = $"{order.ShipmentNumber}: не найден клиент Ozon" |
| | | 476 | | } |
| | | 477 | | |
| | | 478 | | _ozonExemplarService.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 479 | | |
| | | 480 | | OzonExemplarSaveResult result; |
| | | 481 | | try |
| | | 482 | | { |
| | | 483 | | result = await _ozonExemplarService.SaveAsync(order.ShipmentNumber, submission.Exemplars); |
| | | 484 | | } |
| | | 485 | | catch (Exception ex) |
| | | 486 | | { |
| | | 487 | | return BadRequest(new |
| | | 488 | | { |
| | | 489 | | success = false, |
| | | 490 | | message = $"{order.ShipmentNumber}: Ozon отклонил данные экземпляров. {ex.Message}" |
| | | 491 | | }); |
| | | 492 | | } |
| | | 493 | | |
| | | 494 | | if (!result.ShipAvailable) |
| | | 495 | | { |
| | | 496 | | reasons.AddRange(result.Reasons.Select(reason => $"{order.ShipmentNumber}: {reason}")); |
| | | 497 | | } |
| | | 498 | | } |
| | | 499 | | |
| | | 500 | | if (reasons.Count > 0) |
| | | 501 | | { |
| | | 502 | | return Json(new |
| | | 503 | | { |
| | | 504 | | success = false, |
| | | 505 | | message = "Ozon не подтвердил готовность отправления к сборке.", |
| | | 506 | | reasons |
| | | 507 | | }); |
| | | 508 | | } |
| | | 509 | | |
| | | 510 | | return Json(new { success = true }); |
| | | 511 | | } |
| | | 512 | | |
| | | 513 | | [ServiceEndpoint] |
| | | 514 | | [HttpGet] |
| | | 515 | | public async Task<IActionResult> GetOzonProductCountries(int orderId, string? search = null) |
| | | 516 | | { |
| | | 517 | | var order = await _orderServices.GetOrder(orderId); |
| | | 518 | | var client = (await _ozonClientServices.GetOzonClients()) |
| | | 519 | | .FirstOrDefault(item => item.ClientId == order?.OzonClient?.ClientId); |
| | | 520 | | if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) || |
| | | 521 | | string.IsNullOrWhiteSpace(client.DecryptApiKey)) |
| | | 522 | | { |
| | | 523 | | return BadRequest(new { success = false, message = "Не найден клиент Ozon для заказа" }); |
| | | 524 | | } |
| | | 525 | | |
| | | 526 | | _ozonApiDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 527 | | var countries = await _ozonApiDataManager.GetFbsProductCountriesAsync(search); |
| | | 528 | | return Json(new |
| | | 529 | | { |
| | | 530 | | success = true, |
| | | 531 | | countries = countries.Select(country => new |
| | | 532 | | { |
| | | 533 | | name = country["name"]?.ToObject<string>() ?? string.Empty, |
| | | 534 | | countryIsoCode = country["country_iso_code"]?.ToObject<string>() ?? string.Empty |
| | | 535 | | }).Where(country => !string.IsNullOrWhiteSpace(country.countryIsoCode)) |
| | | 536 | | .OrderBy(country => country.name, StringComparer.Create( |
| | | 537 | | System.Globalization.CultureInfo.GetCultureInfo("ru-RU"), ignoreCase: true)) |
| | | 538 | | }); |
| | | 539 | | } |
| | | 540 | | |
| | | 541 | | [ServiceEndpoint] |
| | | 542 | | [HttpPost] |
| | | 543 | | public async Task<IActionResult> SetOzonProductCountries([FromBody] List<OzonProductCountrySelection>? selection |
| | | 544 | | { |
| | | 545 | | if (selections == null || selections.Count == 0 || selections.Any(selection => |
| | | 546 | | selection.OrderId <= 0 || selection.ProductId <= 0 || string.IsNullOrWhiteSpace(selection.CountryIso |
| | | 547 | | { |
| | | 548 | | return BadRequest(new { success = false, message = "Не заполнены страны-изготовители" }); |
| | | 549 | | } |
| | | 550 | | |
| | | 551 | | var orders = await _orderServices.GetOrdersNoTracking(selections.Select(selection => selection.OrderId) |
| | | 552 | | .Distinct().ToArray()); |
| | | 553 | | var clients = await _ozonClientServices.GetOzonClients(); |
| | | 554 | | var ordersById = orders.ToDictionary(order => order.Id); |
| | | 555 | | |
| | | 556 | | foreach (var selection in selections) |
| | | 557 | | { |
| | | 558 | | if (!ordersById.TryGetValue(selection.OrderId, out var order) || |
| | | 559 | | string.IsNullOrWhiteSpace(order.ShipmentNumber)) |
| | | 560 | | { |
| | | 561 | | return BadRequest(new { success = false, message = "Заказ для сохранения страны не найден" }); |
| | | 562 | | } |
| | | 563 | | |
| | | 564 | | var client = clients.FirstOrDefault(item => item.ClientId == order.OzonClient?.ClientId); |
| | | 565 | | if (client == null || string.IsNullOrWhiteSpace(client.DecryptClientId) || |
| | | 566 | | string.IsNullOrWhiteSpace(client.DecryptApiKey)) |
| | | 567 | | { |
| | | 568 | | return BadRequest(new { success = false, message = $"{order.ShipmentNumber}: не найден клиент Ozon" |
| | | 569 | | } |
| | | 570 | | |
| | | 571 | | _ozonApiDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 572 | | await _ozonApiDataManager.SetFbsProductCountryAsync( |
| | | 573 | | OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber), selection.ProductId, |
| | | 574 | | selection.CountryIsoCode); |
| | | 575 | | } |
| | | 576 | | |
| | | 577 | | return Json(new { success = true }); |
| | | 578 | | } |
| | | 579 | | |
| | | 580 | | private static int[] ParseOrderIds(string? ids) |
| | | 581 | | { |
| | | 582 | | return (ids ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries) |
| | | 583 | | .Select(value => int.TryParse(value.Trim(), out var id) ? id : (int?)null) |
| | | 584 | | .OfType<int>() |
| | | 585 | | .Distinct() |
| | | 586 | | .ToArray(); |
| | | 587 | | } |
| | | 588 | | |
| | | 589 | | [ServiceEndpoint] |
| | | 590 | | [HttpPost] |
| | | 591 | | public async Task<IActionResult> CheckLabelsInDropbox(string ids, string? sessionKey = null) |
| | | 592 | | |
| | | 593 | | { |
| | | 594 | | try |
| | | 595 | | |
| | | 596 | | { |
| | | 597 | | int[] idArray = ids.Split(',').Select(int.Parse).ToArray(); |
| | | 598 | | |
| | | 599 | | var ordersLables = await _orderServices.GetOrdersNoTracking(idArray); |
| | | 600 | | |
| | | 601 | | Dictionary<string, byte[]> labels = new Dictionary<string, byte[]>(); |
| | | 602 | | |
| | | 603 | | var failedShipments = new List<string>(); |
| | | 604 | | |
| | | 605 | | |
| | | 606 | | // Проверяем наличие этикеток в хранилище |
| | | 607 | | |
| | | 608 | | bool allLabelsExist = true; |
| | | 609 | | |
| | | 610 | | foreach (var order in ordersLables) |
| | | 611 | | |
| | | 612 | | { |
| | | 613 | | try |
| | | 614 | | |
| | | 615 | | { |
| | | 616 | | var matchedFileName = await _labelFileStorage.FindExistingFileNameAsync( |
| | | 617 | | OzonPostingNumberHelper.GetLabelFileNameCandidates(order.ShipmentNumber)); |
| | | 618 | | |
| | | 619 | | if (matchedFileName != null) |
| | | 620 | | |
| | | 621 | | { |
| | | 622 | | // Загружаем файл из хранилища |
| | | 623 | | |
| | | 624 | | try |
| | | 625 | | |
| | | 626 | | { |
| | | 627 | | var labelBytes = await _labelFileStorage.TryDownloadAsync(matchedFileName); |
| | | 628 | | |
| | | 629 | | if (labelBytes != null && labelBytes.Length > 0) |
| | | 630 | | |
| | | 631 | | { |
| | | 632 | | labels.Add(order.ShipmentNumber!, labelBytes); |
| | | 633 | | } |
| | | 634 | | else |
| | | 635 | | { |
| | | 636 | | allLabelsExist = false; |
| | | 637 | | } |
| | | 638 | | } |
| | | 639 | | |
| | | 640 | | catch (Exception storageEx) |
| | | 641 | | |
| | | 642 | | { |
| | | 643 | | allLabelsExist = false; |
| | | 644 | | |
| | | 645 | | failedShipments.Add( |
| | | 646 | | $"{order.ShipmentNumber}: Не удалось загрузить этикетку - {storageEx.Message}"); |
| | | 647 | | } |
| | | 648 | | } |
| | | 649 | | |
| | | 650 | | else |
| | | 651 | | |
| | | 652 | | { |
| | | 653 | | allLabelsExist = false; |
| | | 654 | | } |
| | | 655 | | } |
| | | 656 | | |
| | | 657 | | catch (Exception ex) |
| | | 658 | | { |
| | | 659 | | allLabelsExist = false; |
| | | 660 | | |
| | | 661 | | System.Diagnostics.Debug.WriteLine( |
| | | 662 | | $"Ошибка при проверке этикетки {order.ShipmentNumber} в хранилище: {ex.Message}"); |
| | | 663 | | } |
| | | 664 | | } |
| | | 665 | | |
| | | 666 | | |
| | | 667 | | // Если все этикетки существуют в хранилище, сохраняем их в сессию и возвращаем sessionKey |
| | | 668 | | |
| | | 669 | | if (allLabelsExist && labels.Count > 0) |
| | | 670 | | |
| | | 671 | | { |
| | | 672 | | var finalSessionKey = _ozonLabelSessionStore.SaveLabels(HttpContext.Session, sessionKey, labels, app |
| | | 673 | | |
| | | 674 | | |
| | | 675 | | // Обновляем кэш для всех найденных этикеток |
| | | 676 | | |
| | | 677 | | foreach (var shipmentNumber in labels.Keys) |
| | | 678 | | |
| | | 679 | | { |
| | | 680 | | UpdateLabelCache(shipmentNumber, true); |
| | | 681 | | } |
| | | 682 | | |
| | | 683 | | |
| | | 684 | | return Json(new |
| | | 685 | | |
| | | 686 | | { |
| | | 687 | | success = true, |
| | | 688 | | |
| | | 689 | | message = $"Этикетки для всех заказов найдены в системе ({labels.Count} шт.)", |
| | | 690 | | |
| | | 691 | | sessionKey = finalSessionKey, |
| | | 692 | | |
| | | 693 | | generatedLabels = labels.Keys.ToList(), |
| | | 694 | | |
| | | 695 | | failedShipments = failedShipments, |
| | | 696 | | |
| | | 697 | | totalCount = labels.Count, |
| | | 698 | | |
| | | 699 | | labelsAlreadyExist = true, |
| | | 700 | | |
| | | 701 | | skipFurtherSteps = true // Флаг для фронтенда, чтобы пропустить дальнейшие шаги |
| | | 702 | | }); |
| | | 703 | | } |
| | | 704 | | |
| | | 705 | | |
| | | 706 | | string? accumulatedSessionKey = null; |
| | | 707 | | if (labels.Count > 0 && !string.IsNullOrEmpty(sessionKey)) |
| | | 708 | | { |
| | | 709 | | accumulatedSessionKey = _ozonLabelSessionStore.SaveLabels(HttpContext.Session, sessionKey, labels, a |
| | | 710 | | } |
| | | 711 | | |
| | | 712 | | // Если этикетки не найдены или не все найдены, возвращаем информацию для продолжения обычного процесса |
| | | 713 | | |
| | | 714 | | return Json(new |
| | | 715 | | |
| | | 716 | | { |
| | | 717 | | success = true, |
| | | 718 | | |
| | | 719 | | message = labels.Count > 0 |
| | | 720 | | ? $"Найдено {labels.Count} из {ordersLables.Count} этикеток в системе. Продолжаем генерацию оста |
| | | 721 | | : "Этикетки не найдены в системе. Продолжаем генерацию.", |
| | | 722 | | |
| | | 723 | | labelsFound = labels.Count, |
| | | 724 | | |
| | | 725 | | totalOrders = ordersLables.Count, |
| | | 726 | | |
| | | 727 | | skipFurtherSteps = false, |
| | | 728 | | |
| | | 729 | | foundLabels = labels.Keys.ToList(), |
| | | 730 | | |
| | | 731 | | sessionKey = accumulatedSessionKey |
| | | 732 | | }); |
| | | 733 | | } |
| | | 734 | | |
| | | 735 | | catch (Exception ex) |
| | | 736 | | { |
| | | 737 | | return Json(new |
| | | 738 | | |
| | | 739 | | { |
| | | 740 | | success = false, |
| | | 741 | | |
| | | 742 | | message = $"Ошибка при проверке этикеток в системе: {ex.Message}", |
| | | 743 | | |
| | | 744 | | skipFurtherSteps = false |
| | | 745 | | }); |
| | | 746 | | } |
| | | 747 | | } |
| | | 748 | | |
| | | 749 | | |
| | | 750 | | /// <summary> |
| | | 751 | | /// Батч-запрос статусов Ozon для всех отправлений пачки: группируем по клиенту Ozon |
| | | 752 | | /// (у каждого свои креды) и опрашиваем отправления группы параллельно. Ошибку запроса |
| | | 753 | | /// не подменяем пустым статусом — такое отправление просто не попадает в словарь и |
| | | 754 | | /// вызывающий код запрашивает статус для него обычным путём. |
| | | 755 | | /// </summary> |
| | | 756 | | private async Task<Dictionary<string, string>> LoadOzonStatusesAsync( |
| | | 757 | | IEnumerable<Order> orders, |
| | | 758 | | List<OzonClient> clients) |
| | | 759 | | { |
| | | 760 | | var result = new Dictionary<string, string>(StringComparer.Ordinal); |
| | | 761 | | |
| | | 762 | | foreach (var clientGroup in orders.GroupBy(o => o.OzonClient?.ClientId)) |
| | | 763 | | { |
| | | 764 | | var client = clients.FirstOrDefault(c => c.ClientId == clientGroup.Key); |
| | | 765 | | if (client == null || string.IsNullOrEmpty(client.DecryptClientId) || |
| | | 766 | | string.IsNullOrEmpty(client.DecryptApiKey)) |
| | | 767 | | { |
| | | 768 | | continue; |
| | | 769 | | } |
| | | 770 | | |
| | | 771 | | var postings = clientGroup |
| | | 772 | | .Where(o => !string.IsNullOrWhiteSpace(o.ShipmentNumber)) |
| | | 773 | | .Select(o => OzonPostingNumberHelper.GetOzonApiPostingNumber(o.ShipmentNumber)) |
| | | 774 | | .Distinct(StringComparer.Ordinal) |
| | | 775 | | .ToList(); |
| | | 776 | | |
| | | 777 | | if (postings.Count == 0) |
| | | 778 | | { |
| | | 779 | | continue; |
| | | 780 | | } |
| | | 781 | | |
| | | 782 | | _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 783 | | |
| | | 784 | | try |
| | | 785 | | { |
| | | 786 | | var statuses = await _jsonDataBuilder.GetProductStatusBatch(postings); |
| | | 787 | | foreach (var (posting, value) in statuses) |
| | | 788 | | { |
| | | 789 | | if (value.Success && !string.IsNullOrEmpty(value.Status)) |
| | | 790 | | { |
| | | 791 | | result[posting] = value.Status; |
| | | 792 | | } |
| | | 793 | | } |
| | | 794 | | } |
| | | 795 | | catch |
| | | 796 | | { |
| | | 797 | | // Статусы — оптимизация, не обязательное условие: при сбое каждый заказ |
| | | 798 | | // запросит свой статус сам. |
| | | 799 | | } |
| | | 800 | | } |
| | | 801 | | |
| | | 802 | | return result; |
| | | 803 | | } |
| | | 804 | | |
| | | 805 | | [ServiceEndpoint] |
| | | 806 | | public async Task<IActionResult> UpdateOrdersStatus(string ids, string? splitOption = null) |
| | | 807 | | |
| | | 808 | | { |
| | | 809 | | try |
| | | 810 | | |
| | | 811 | | { |
| | | 812 | | List<OzonClient> clients = await _ozonClientServices.GetOzonClients(); |
| | | 813 | | |
| | | 814 | | int[] idArray = ids.Split(',').Select(int.Parse).ToArray(); |
| | | 815 | | |
| | | 816 | | var ordersLables = await _orderServices.GetOrdersNoTracking(idArray); |
| | | 817 | | |
| | | 818 | | var failedShipments = new List<string>(); |
| | | 819 | | |
| | | 820 | | string updatedOrdersResult = ""; |
| | | 821 | | |
| | | 822 | | var studioParentGroups = OzonPostingNumberHelper.GroupStudioLocalSplitSiblings(ordersLables); |
| | | 823 | | |
| | | 824 | | var shippedStudioParents = new HashSet<string>(StringComparer.Ordinal); |
| | | 825 | | |
| | | 826 | | // Отгрузка собирает отправление целиком (все товары), поэтому остальные строки того же |
| | | 827 | | // отправления (несколько разных товаров в одном posting) повторно не отгружаем — иначе они |
| | | 828 | | // пошли бы со статусом из батча ниже, уже устаревшим после первой отгрузки. |
| | | 829 | | var processedPostings = new HashSet<string>(StringComparer.Ordinal); |
| | | 830 | | |
| | | 831 | | // Статусы всех отправлений — заранее и параллельно, по одному батчу на клиента Ozon. |
| | | 832 | | // Иначе каждый заказ в цикле ниже начинался бы с собственного последовательного |
| | | 833 | | // запроса статуса (~0.4 c на заказ впустую, пока Ozon уже всё отдал). |
| | | 834 | | var knownStatuses = await LoadOzonStatusesAsync(ordersLables, clients); |
| | | 835 | | |
| | | 836 | | foreach (var order in ordersLables) |
| | | 837 | | |
| | | 838 | | { |
| | | 839 | | try |
| | | 840 | | |
| | | 841 | | { |
| | | 842 | | var client = clients.FirstOrDefault(c => c.ClientId == order.OzonClient?.ClientId); |
| | | 843 | | if (client == null || string.IsNullOrEmpty(client.DecryptClientId) || |
| | | 844 | | string.IsNullOrEmpty(client.DecryptApiKey)) |
| | | 845 | | { |
| | | 846 | | failedShipments.Add($"{order.ShipmentNumber}: Ozon клиент не найден"); |
| | | 847 | | continue; |
| | | 848 | | } |
| | | 849 | | |
| | | 850 | | _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 851 | | // _ozonShipService держит собственный (DI, transient) OzonJsonDataBuilder — |
| | | 852 | | // SetClient выше на него не влияет, клиента нужно выставлять отдельно здесь же. |
| | | 853 | | _ozonShipService.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 854 | | |
| | | 855 | | var apiPosting = OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber); |
| | | 856 | | if (studioParentGroups.TryGetValue(apiPosting, out var siblings)) |
| | | 857 | | { |
| | | 858 | | if (shippedStudioParents.Contains(apiPosting)) |
| | | 859 | | { |
| | | 860 | | updatedOrdersResult = |
| | | 861 | | $"Отгрузка для {order.ShipmentNumber} выполнена в составе группы {apiPosting}."; |
| | | 862 | | continue; |
| | | 863 | | } |
| | | 864 | | |
| | | 865 | | updatedOrdersResult = await _ozonLabelPrintService.ProcessStudioLocalSplitGroupShipAsync( |
| | | 866 | | siblings, |
| | | 867 | | apiPosting, |
| | | 868 | | _ozonShipService.ProcessOrderStatusAsync); |
| | | 869 | | shippedStudioParents.Add(apiPosting); |
| | | 870 | | continue; |
| | | 871 | | } |
| | | 872 | | |
| | | 873 | | if (!processedPostings.Add(apiPosting)) |
| | | 874 | | { |
| | | 875 | | updatedOrdersResult = |
| | | 876 | | $"Отгрузка для {order.ShipmentNumber} выполнена в составе отправления {apiPosting}."; |
| | | 877 | | continue; |
| | | 878 | | } |
| | | 879 | | |
| | | 880 | | updatedOrdersResult = knownStatuses.TryGetValue(apiPosting, out var knownStatus) |
| | | 881 | | ? await _ozonShipService.ProcessOrderStatusWithKnownStatusAsync( |
| | | 882 | | order, splitOption, knownStatus) |
| | | 883 | | : await _ozonShipService.ProcessOrderStatusAsync(order, splitOption); |
| | | 884 | | } |
| | | 885 | | |
| | | 886 | | catch (Exception ex) |
| | | 887 | | { |
| | | 888 | | await _ozonShipService.RecordFailedShipmentAsync(order, ex, failedShipments); |
| | | 889 | | } |
| | | 890 | | } |
| | | 891 | | |
| | | 892 | | |
| | | 893 | | await _clientStatusSyncService.SyncOrderStatusesAsync(idArray); |
| | | 894 | | |
| | | 895 | | var labelBlockReasons = new List<string>(); |
| | | 896 | | string? primaryOzonStatus = null; |
| | | 897 | | foreach (var order in ordersLables) |
| | | 898 | | { |
| | | 899 | | if (failedShipments.Exists(f => f.StartsWith(order.ShipmentNumber + ":", StringComparison.Ordinal))) |
| | | 900 | | { |
| | | 901 | | continue; |
| | | 902 | | } |
| | | 903 | | |
| | | 904 | | if (order.OzonClient == null) |
| | | 905 | | { |
| | | 906 | | continue; |
| | | 907 | | } |
| | | 908 | | |
| | | 909 | | var client = clients.FirstOrDefault(c => c.ClientId == order.OzonClient.ClientId); |
| | | 910 | | if (client == null || string.IsNullOrEmpty(client.DecryptClientId) || |
| | | 911 | | string.IsNullOrEmpty(client.DecryptApiKey)) |
| | | 912 | | { |
| | | 913 | | continue; |
| | | 914 | | } |
| | | 915 | | |
| | | 916 | | _jsonDataBuilder.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 917 | | var ozonStatus = await _jsonDataBuilder.GetProductSatatus( |
| | | 918 | | OzonPostingNumberHelper.GetOzonApiPostingNumber(order.ShipmentNumber!)); |
| | | 919 | | primaryOzonStatus ??= ozonStatus; |
| | | 920 | | |
| | | 921 | | if (!OzonLabelStatusHelper.IsOzonStatusLabelGenerationBlocked(ozonStatus)) |
| | | 922 | | { |
| | | 923 | | continue; |
| | | 924 | | } |
| | | 925 | | |
| | | 926 | | var statusName = OzonLabelStatusHelper.GetOzonStatusDisplayName(ozonStatus); |
| | | 927 | | labelBlockReasons.Add( |
| | | 928 | | $"{order.ShipmentNumber}: статус «{statusName}» — Ozon API не выдаёт этикетки для завершённых от |
| | | 929 | | } |
| | | 930 | | |
| | | 931 | | var allFailed = ordersLables.Count > 0 && failedShipments.Count >= ordersLables.Count; |
| | | 932 | | var anyFailed = failedShipments.Any(); |
| | | 933 | | var labelGenerationBlocked = labelBlockReasons.Count > 0; |
| | | 934 | | |
| | | 935 | | if (anyFailed) |
| | | 936 | | { |
| | | 937 | | } |
| | | 938 | | |
| | | 939 | | var resultMessage = anyFailed |
| | | 940 | | ? string.Join("; ", failedShipments) |
| | | 941 | | : updatedOrdersResult; |
| | | 942 | | |
| | | 943 | | if (!anyFailed && !string.IsNullOrWhiteSpace(updatedOrdersResult)) |
| | | 944 | | { |
| | | 945 | | resultMessage = updatedOrdersResult; |
| | | 946 | | } |
| | | 947 | | |
| | | 948 | | if (labelGenerationBlocked) |
| | | 949 | | { |
| | | 950 | | resultMessage = string.Join("; ", labelBlockReasons); |
| | | 951 | | } |
| | | 952 | | |
| | | 953 | | var result = new |
| | | 954 | | |
| | | 955 | | { |
| | | 956 | | success = !allFailed && !labelGenerationBlocked, |
| | | 957 | | |
| | | 958 | | message = resultMessage, |
| | | 959 | | |
| | | 960 | | updatedOrders = ordersLables.Count > 0 ? ordersLables[0].ShipmentNumber : "", |
| | | 961 | | |
| | | 962 | | failedShipments = failedShipments, |
| | | 963 | | |
| | | 964 | | totalOrders = ordersLables.Count, |
| | | 965 | | |
| | | 966 | | ozonStatus = primaryOzonStatus, |
| | | 967 | | |
| | | 968 | | labelGenerationBlocked, |
| | | 969 | | |
| | | 970 | | labelBlockReasons, |
| | | 971 | | |
| | | 972 | | step = allFailed || labelGenerationBlocked ? "UpdateOrdersStatus" : null |
| | | 973 | | }; |
| | | 974 | | |
| | | 975 | | |
| | | 976 | | return Json(result); |
| | | 977 | | } |
| | | 978 | | |
| | | 979 | | catch (Exception ex) |
| | | 980 | | |
| | | 981 | | { |
| | | 982 | | return Json(new |
| | | 983 | | |
| | | 984 | | { |
| | | 985 | | success = false, |
| | | 986 | | |
| | | 987 | | message = $"Ошибка при обновлении статусов: {ex.Message}", |
| | | 988 | | |
| | | 989 | | step = "UpdateOrdersStatus" |
| | | 990 | | }); |
| | | 991 | | } |
| | | 992 | | } |
| | | 993 | | |
| | | 994 | | |
| | | 995 | | [ServiceEndpoint] |
| | | 996 | | [HttpPost] |
| | | 997 | | public async Task<IActionResult> GenerateLabels(string ids, string? sessionKey = null) |
| | | 998 | | |
| | | 999 | | { |
| | | 1000 | | try |
| | | 1001 | | |
| | | 1002 | | { |
| | | 1003 | | List<OzonClient> clients = await _ozonClientServices.GetOzonClients(); |
| | | 1004 | | |
| | | 1005 | | int[] idArray = ids.Split(',').Select(int.Parse).ToArray(); |
| | | 1006 | | |
| | | 1007 | | var ordersLables = await _orderServices.GetOrdersNoTracking(idArray); |
| | | 1008 | | |
| | | 1009 | | Dictionary<string, byte[]> labels = new Dictionary<string, byte[]>(); |
| | | 1010 | | |
| | | 1011 | | var failedShipments = new List<string>(); |
| | | 1012 | | |
| | | 1013 | | var labelsFromDropbox = new List<string>(); |
| | | 1014 | | |
| | | 1015 | | var labelsGenerated = new List<string>(); |
| | | 1016 | | |
| | | 1017 | | var ordersNeedingGeneration = new List<Order>(); |
| | | 1018 | | |
| | | 1019 | | var autoShippedShipments = new List<string>(); |
| | | 1020 | | |
| | | 1021 | | var labelsStored = new List<string>(); |
| | | 1022 | | |
| | | 1023 | | foreach (var order in ordersLables) |
| | | 1024 | | |
| | | 1025 | | { |
| | | 1026 | | try |
| | | 1027 | | |
| | | 1028 | | { |
| | | 1029 | | var matchedFileName = await _labelFileStorage.FindExistingFileNameAsync( |
| | | 1030 | | OzonPostingNumberHelper.GetLabelFileNameCandidates(order.ShipmentNumber)); |
| | | 1031 | | |
| | | 1032 | | |
| | | 1033 | | if (matchedFileName != null) |
| | | 1034 | | |
| | | 1035 | | { |
| | | 1036 | | try |
| | | 1037 | | |
| | | 1038 | | { |
| | | 1039 | | var labelBytes = await _labelFileStorage.TryDownloadAsync(matchedFileName); |
| | | 1040 | | |
| | | 1041 | | if (labelBytes != null && labelBytes.Length > 0) |
| | | 1042 | | |
| | | 1043 | | { |
| | | 1044 | | labels.Add(order.ShipmentNumber!, labelBytes); |
| | | 1045 | | |
| | | 1046 | | labelsFromDropbox.Add(order.ShipmentNumber!); |
| | | 1047 | | } |
| | | 1048 | | else |
| | | 1049 | | { |
| | | 1050 | | ordersNeedingGeneration.Add(order); |
| | | 1051 | | } |
| | | 1052 | | } |
| | | 1053 | | |
| | | 1054 | | catch (Exception storageEx) |
| | | 1055 | | |
| | | 1056 | | { |
| | | 1057 | | ordersNeedingGeneration.Add(order); |
| | | 1058 | | failedShipments.Add( |
| | | 1059 | | $"{order.ShipmentNumber}: Не удалось загрузить этикетку - {storageEx.Message}"); |
| | | 1060 | | } |
| | | 1061 | | } |
| | | 1062 | | |
| | | 1063 | | else |
| | | 1064 | | |
| | | 1065 | | { |
| | | 1066 | | ordersNeedingGeneration.Add(order); |
| | | 1067 | | } |
| | | 1068 | | } |
| | | 1069 | | |
| | | 1070 | | catch (Exception ex) |
| | | 1071 | | { |
| | | 1072 | | failedShipments.Add($"{order.ShipmentNumber}: {ex.Message}"); |
| | | 1073 | | } |
| | | 1074 | | } |
| | | 1075 | | |
| | | 1076 | | await _ozonLabelPrintService.GenerateOzonLabelsForOrdersAsync( |
| | | 1077 | | ordersNeedingGeneration, |
| | | 1078 | | clients, |
| | | 1079 | | labels, |
| | | 1080 | | labelsGenerated, |
| | | 1081 | | failedShipments, |
| | | 1082 | | autoShippedShipments, |
| | | 1083 | | labelsStored); |
| | | 1084 | | |
| | | 1085 | | if (labelsStored.Count > 0) |
| | | 1086 | | { |
| | | 1087 | | var stored = labelsStored.ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | | 1088 | | await _labelPrintLogService.RecordAsync(ordersLables |
| | | 1089 | | .Where(order => order.ShipmentNumber is { } shipment && stored.Contains(shipment)) |
| | | 1090 | | .Select(order => CreateStudioLabelLog($"{order.ShipmentNumber}.pdf", order.Id))); |
| | | 1091 | | } |
| | | 1092 | | |
| | | 1093 | | // ScanIt связывает этикетку с заказом: если его ещё нет в базе, берём у Ozon при печати. |
| | | 1094 | | try |
| | | 1095 | | { |
| | | 1096 | | await _ozonLabelPrintService.EnsureScanitAsync(ordersLables.ToList(), clients); |
| | | 1097 | | } |
| | | 1098 | | catch |
| | | 1099 | | { |
| | | 1100 | | // Не мешаем печати: ScanIt подтянется при следующей печати или загрузке заказа. |
| | | 1101 | | } |
| | | 1102 | | |
| | | 1103 | | if (labels.Count == 0) |
| | | 1104 | | |
| | | 1105 | | { |
| | | 1106 | | |
| | | 1107 | | return Json(new |
| | | 1108 | | |
| | | 1109 | | { |
| | | 1110 | | success = false, |
| | | 1111 | | |
| | | 1112 | | message = "Не удалось получить или сгенерировать ни одной этикетки", |
| | | 1113 | | |
| | | 1114 | | failedShipments = failedShipments, |
| | | 1115 | | |
| | | 1116 | | autoShippedShipments = autoShippedShipments, |
| | | 1117 | | |
| | | 1118 | | step = "GenerateLabels" |
| | | 1119 | | }); |
| | | 1120 | | } |
| | | 1121 | | |
| | | 1122 | | |
| | | 1123 | | var finalSessionKey = _ozonLabelSessionStore.SaveLabels(HttpContext.Session, sessionKey, labels, append: |
| | | 1124 | | |
| | | 1125 | | |
| | | 1126 | | var message = $"Обработано {labels.Count} этикеток"; |
| | | 1127 | | |
| | | 1128 | | if (labelsFromDropbox.Count > 0 && labelsGenerated.Count > 0) |
| | | 1129 | | |
| | | 1130 | | { |
| | | 1131 | | message += |
| | | 1132 | | $" ({labelsFromDropbox.Count} загружено из системы, {labelsGenerated.Count} сгенерировано и сохр |
| | | 1133 | | } |
| | | 1134 | | |
| | | 1135 | | else if (labelsFromDropbox.Count > 0) |
| | | 1136 | | |
| | | 1137 | | { |
| | | 1138 | | message += $" (все загружены из системы)"; |
| | | 1139 | | } |
| | | 1140 | | |
| | | 1141 | | else if (labelsGenerated.Count > 0) |
| | | 1142 | | |
| | | 1143 | | { |
| | | 1144 | | message += $" (все сгенерированы и успешно сохранены в системе)"; |
| | | 1145 | | } |
| | | 1146 | | |
| | | 1147 | | if (autoShippedShipments.Count > 0) |
| | | 1148 | | |
| | | 1149 | | { |
| | | 1150 | | message += $". Статус {autoShippedShipments.Count} заказ(ов) автоматически обновлён (отгружен) перед |
| | | 1151 | | } |
| | | 1152 | | |
| | | 1153 | | // Обновляем кэш для всех успешно обработанных этикеток |
| | | 1154 | | |
| | | 1155 | | foreach (var shipmentNumber in labels.Keys) |
| | | 1156 | | |
| | | 1157 | | { |
| | | 1158 | | UpdateLabelCache(shipmentNumber, true); |
| | | 1159 | | } |
| | | 1160 | | |
| | | 1161 | | |
| | | 1162 | | return Json(new |
| | | 1163 | | |
| | | 1164 | | { |
| | | 1165 | | success = true, |
| | | 1166 | | |
| | | 1167 | | message = message, |
| | | 1168 | | |
| | | 1169 | | sessionKey = finalSessionKey, |
| | | 1170 | | |
| | | 1171 | | generatedLabels = labels.Keys.ToList(), |
| | | 1172 | | |
| | | 1173 | | failedShipments = failedShipments, |
| | | 1174 | | |
| | | 1175 | | totalCount = labels.Count, |
| | | 1176 | | |
| | | 1177 | | fromDropbox = labelsFromDropbox, |
| | | 1178 | | |
| | | 1179 | | autoShippedShipments = autoShippedShipments, |
| | | 1180 | | |
| | | 1181 | | newlyGenerated = labelsGenerated, |
| | | 1182 | | |
| | | 1183 | | labelsFromDropbox = labelsFromDropbox |
| | | 1184 | | }); |
| | | 1185 | | } |
| | | 1186 | | |
| | | 1187 | | catch (Exception ex) |
| | | 1188 | | |
| | | 1189 | | { |
| | | 1190 | | return Json(new |
| | | 1191 | | |
| | | 1192 | | { |
| | | 1193 | | success = false, |
| | | 1194 | | |
| | | 1195 | | message = $"Ошибка при генерации этикеток: {ex.Message}", |
| | | 1196 | | |
| | | 1197 | | step = "GenerateLabels" |
| | | 1198 | | }); |
| | | 1199 | | } |
| | | 1200 | | } |
| | | 1201 | | |
| | | 1202 | | |
| | | 1203 | | [HttpPost] |
| | | 1204 | | /// <summary> |
| | | 1205 | | /// Этикетки Яндекс.Маркета для выбранных заказов. |
| | | 1206 | | /// <paramref name="force"/> = перепечать: игнорирует сохранённый PDF и запрашивает у API |
| | | 1207 | | /// новый в формате <paramref name="format"/>, перезаписывая файл в хранилище. |
| | | 1208 | | /// </summary> |
| | | 1209 | | public async Task<IActionResult> GenerateYandexLabels(string ids, string format = "A7", bool force = false) |
| | | 1210 | | |
| | | 1211 | | { |
| | | 1212 | | try |
| | | 1213 | | |
| | | 1214 | | { |
| | | 1215 | | var labels = new Dictionary<string, byte[]>(); |
| | | 1216 | | |
| | | 1217 | | var failedShipments = new List<string>(); |
| | | 1218 | | |
| | | 1219 | | var labelsFromDropbox = new List<string>(); |
| | | 1220 | | |
| | | 1221 | | var labelsGenerated = new List<string>(); |
| | | 1222 | | var storedLabelLogs = new List<LabelPrintLogEntry>(); |
| | | 1223 | | |
| | | 1224 | | |
| | | 1225 | | var f = (format ?? "A7").Trim().ToUpperInvariant(); |
| | | 1226 | | |
| | | 1227 | | if (f != "A9_HORIZONTALLY" && f != "A9" && f != "A7" && f != "A4") |
| | | 1228 | | |
| | | 1229 | | f = "A7"; |
| | | 1230 | | |
| | | 1231 | | |
| | | 1232 | | var clients = await _ozonClientServices.GetOzonClients(); |
| | | 1233 | | |
| | | 1234 | | var idArray = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) |
| | | 1235 | | .Select(s => int.TryParse(s.Trim(), out var n) ? n : 0).Where(i => i > 0).ToArray(); |
| | | 1236 | | |
| | | 1237 | | var ordersToProcess = new List<Order>(); |
| | | 1238 | | |
| | | 1239 | | |
| | | 1240 | | foreach (var orderId in idArray) |
| | | 1241 | | |
| | | 1242 | | { |
| | | 1243 | | var order = await _orderServices.GetOrder(orderId); |
| | | 1244 | | |
| | | 1245 | | if (order == null) continue; |
| | | 1246 | | |
| | | 1247 | | if (order.OzonClient?.ClientType != ClientType.YANDEX) |
| | | 1248 | | |
| | | 1249 | | { |
| | | 1250 | | failedShipments.Add($"{order.ShipmentNumber}: не заказ Яндекс.Маркета"); |
| | | 1251 | | |
| | | 1252 | | continue; |
| | | 1253 | | } |
| | | 1254 | | |
| | | 1255 | | |
| | | 1256 | | ordersToProcess.Add(order); |
| | | 1257 | | } |
| | | 1258 | | |
| | | 1259 | | |
| | | 1260 | | // YandexDataManager хранит текущего клиента. Поэтому клиентов обрабатываем |
| | | 1261 | | // последовательно, а независимые заказы одного клиента — параллельно. |
| | | 1262 | | // Внутренний rate limiter YandexDataManager по-прежнему ограничивает API до 100 rpm. |
| | | 1263 | | // Partner API допускает не более 4 параллельных запросов на кампанию/бизнес (иначе HTTP 420). |
| | | 1264 | | const int labelParallelism = 4; |
| | | 1265 | | var resultLock = new object(); |
| | | 1266 | | |
| | | 1267 | | foreach (var clientGroup in ordersToProcess.GroupBy(order => order.OzonClient?.ClientId)) |
| | | 1268 | | { |
| | | 1269 | | var client = clients.FirstOrDefault(c => c.ClientId == clientGroup.Key); |
| | | 1270 | | if (client == null || string.IsNullOrEmpty(client.DecryptClientId) || |
| | | 1271 | | string.IsNullOrEmpty(client.DecryptApiKey)) |
| | | 1272 | | { |
| | | 1273 | | lock (resultLock) |
| | | 1274 | | { |
| | | 1275 | | failedShipments.AddRange(clientGroup.Select(order => |
| | | 1276 | | $"{order.ShipmentNumber}: нет данных клиента Яндекс")); |
| | | 1277 | | } |
| | | 1278 | | |
| | | 1279 | | continue; |
| | | 1280 | | } |
| | | 1281 | | |
| | | 1282 | | _yandexDataManager.SetClient(client.DecryptClientId, client.DecryptApiKey); |
| | | 1283 | | using var semaphore = new SemaphoreSlim(labelParallelism); |
| | | 1284 | | |
| | | 1285 | | await Task.WhenAll(clientGroup.Select(async order => |
| | | 1286 | | { |
| | | 1287 | | await semaphore.WaitAsync(); |
| | | 1288 | | try |
| | | 1289 | | { |
| | | 1290 | | var shipmentNumber = order.ShipmentNumber; |
| | | 1291 | | if (string.IsNullOrWhiteSpace(shipmentNumber)) |
| | | 1292 | | { |
| | | 1293 | | lock (resultLock) failedShipments.Add("Заказ без номера отправления"); |
| | | 1294 | | return; |
| | | 1295 | | } |
| | | 1296 | | |
| | | 1297 | | var fileName = $"{shipmentNumber}.pdf"; |
| | | 1298 | | if (!force && await _labelFileStorage.ExistsAsync(fileName)) |
| | | 1299 | | { |
| | | 1300 | | var storedBytes = await _labelFileStorage.TryDownloadAsync(fileName); |
| | | 1301 | | if (storedBytes is not { Length: > 0 }) |
| | | 1302 | | throw new Exception("Пустой файл этикетки в хранилище"); |
| | | 1303 | | |
| | | 1304 | | lock (resultLock) |
| | | 1305 | | { |
| | | 1306 | | labels[shipmentNumber] = storedBytes; |
| | | 1307 | | labelsFromDropbox.Add(shipmentNumber); |
| | | 1308 | | } |
| | | 1309 | | |
| | | 1310 | | return; |
| | | 1311 | | } |
| | | 1312 | | |
| | | 1313 | | if (!long.TryParse(shipmentNumber, out var yandexOrderId)) |
| | | 1314 | | { |
| | | 1315 | | lock (resultLock) failedShipments.Add($"{shipmentNumber}: некорректный номер заказа"); |
| | | 1316 | | return; |
| | | 1317 | | } |
| | | 1318 | | |
| | | 1319 | | var campaignId = await _yandexDataManager.GetCampaignIdForOrderAsync( |
| | | 1320 | | client.DecryptClientId, yandexOrderId); |
| | | 1321 | | if (!campaignId.HasValue) |
| | | 1322 | | { |
| | | 1323 | | lock (resultLock) failedShipments.Add($"{shipmentNumber}: не удалось получить campaignId |
| | | 1324 | | return; |
| | | 1325 | | } |
| | | 1326 | | |
| | | 1327 | | var labelBytes = await _yandexDataManager.GetOrderLabelPdfAsync( |
| | | 1328 | | campaignId.Value, yandexOrderId, f); |
| | | 1329 | | if (labelBytes is not { Length: > 0 }) |
| | | 1330 | | { |
| | | 1331 | | lock (resultLock) failedShipments.Add($"{shipmentNumber}: пустой ответ API этикеток"); |
| | | 1332 | | return; |
| | | 1333 | | } |
| | | 1334 | | |
| | | 1335 | | await _labelFileStorage.UploadAsync(labelBytes, fileName, overwrite: true); |
| | | 1336 | | lock (resultLock) |
| | | 1337 | | { |
| | | 1338 | | labels[shipmentNumber] = labelBytes; |
| | | 1339 | | labelsGenerated.Add(shipmentNumber); |
| | | 1340 | | storedLabelLogs.Add(CreateStudioLabelLog(fileName, order.Id)); |
| | | 1341 | | } |
| | | 1342 | | } |
| | | 1343 | | catch (Exception ex) |
| | | 1344 | | { |
| | | 1345 | | lock (resultLock) failedShipments.Add($"{order.ShipmentNumber}: {ex.Message}"); |
| | | 1346 | | } |
| | | 1347 | | finally |
| | | 1348 | | { |
| | | 1349 | | semaphore.Release(); |
| | | 1350 | | } |
| | | 1351 | | })); |
| | | 1352 | | } |
| | | 1353 | | |
| | | 1354 | | |
| | | 1355 | | if (labels.Count == 0) |
| | | 1356 | | |
| | | 1357 | | { |
| | | 1358 | | return Json(new |
| | | 1359 | | |
| | | 1360 | | { |
| | | 1361 | | success = false, |
| | | 1362 | | |
| | | 1363 | | message = "Не удалось получить или сгенерировать ни одной этикетки Яндекс", |
| | | 1364 | | |
| | | 1365 | | failedShipments = failedShipments |
| | | 1366 | | }); |
| | | 1367 | | } |
| | | 1368 | | |
| | | 1369 | | if (storedLabelLogs.Count > 0) |
| | | 1370 | | { |
| | | 1371 | | await _labelPrintLogService.RecordAsync(storedLabelLogs); |
| | | 1372 | | } |
| | | 1373 | | |
| | | 1374 | | |
| | | 1375 | | var sessionKey = $"labels_{Guid.NewGuid()}"; |
| | | 1376 | | |
| | | 1377 | | HttpContext.Session.SetString(sessionKey, JsonConvert.SerializeObject(labels.Keys.ToList())); |
| | | 1378 | | |
| | | 1379 | | foreach (var label in labels) |
| | | 1380 | | |
| | | 1381 | | { |
| | | 1382 | | var fileKey = $"{sessionKey}_{label.Key}"; |
| | | 1383 | | |
| | | 1384 | | HttpContext.Session.Set(fileKey, label.Value); |
| | | 1385 | | } |
| | | 1386 | | |
| | | 1387 | | |
| | | 1388 | | var message = $"Обработано {labels.Count} этикеток Яндекс"; |
| | | 1389 | | |
| | | 1390 | | if (labelsFromDropbox.Count > 0 && labelsGenerated.Count > 0) |
| | | 1391 | | |
| | | 1392 | | message += $" ({labelsFromDropbox.Count} из системы, {labelsGenerated.Count} сгенерировано)"; |
| | | 1393 | | |
| | | 1394 | | else if (labelsFromDropbox.Count > 0) |
| | | 1395 | | |
| | | 1396 | | message += " (все из системы)"; |
| | | 1397 | | |
| | | 1398 | | else if (labelsGenerated.Count > 0) |
| | | 1399 | | |
| | | 1400 | | message += " (все сгенерированы)"; |
| | | 1401 | | |
| | | 1402 | | |
| | | 1403 | | foreach (var shipmentNumber in labels.Keys) |
| | | 1404 | | |
| | | 1405 | | UpdateLabelCache(shipmentNumber, true); |
| | | 1406 | | |
| | | 1407 | | |
| | | 1408 | | return Json(new |
| | | 1409 | | |
| | | 1410 | | { |
| | | 1411 | | success = true, |
| | | 1412 | | |
| | | 1413 | | message = message, |
| | | 1414 | | |
| | | 1415 | | sessionKey = sessionKey, |
| | | 1416 | | |
| | | 1417 | | generatedLabels = labels.Keys.ToList(), |
| | | 1418 | | |
| | | 1419 | | failedShipments = failedShipments, |
| | | 1420 | | |
| | | 1421 | | totalCount = labels.Count, |
| | | 1422 | | |
| | | 1423 | | fromDropbox = labelsFromDropbox, |
| | | 1424 | | |
| | | 1425 | | newlyGenerated = labelsGenerated |
| | | 1426 | | }); |
| | | 1427 | | } |
| | | 1428 | | |
| | | 1429 | | catch (Exception ex) |
| | | 1430 | | { |
| | | 1431 | | return Json(new { success = false, message = $"Ошибка при генерации этикеток Яндекс: {ex.Message}" }); |
| | | 1432 | | } |
| | | 1433 | | } |
| | | 1434 | | |
| | | 1435 | | private LabelPrintLogEntry CreateStudioLabelLog(string fileName, int orderId) => |
| | | 1436 | | new( |
| | | 1437 | | fileName, |
| | | 1438 | | orderId, |
| | | 1439 | | LabelPrintSource.Studio2, |
| | | 1440 | | LabelPrintActorKind.StudioUser, |
| | | 1441 | | User.FindFirstValue(ClaimTypes.NameIdentifier), |
| | | 1442 | | User.Identity?.Name ?? "Неизвестный пользователь"); |
| | | 1443 | | |
| | | 1444 | | |
| | | 1445 | | [ServiceEndpoint] |
| | | 1446 | | [HttpGet] |
| | | 1447 | | public async Task<IActionResult> DownloadLabels(string sessionKey) |
| | | 1448 | | |
| | | 1449 | | { |
| | | 1450 | | try |
| | | 1451 | | |
| | | 1452 | | { |
| | | 1453 | | var labelKeysJson = HttpContext.Session.GetString(sessionKey); |
| | | 1454 | | |
| | | 1455 | | if (string.IsNullOrEmpty(labelKeysJson)) |
| | | 1456 | | |
| | | 1457 | | { |
| | | 1458 | | return NotFound("Файлы этикеток не найдены или срок их хранения истек"); |
| | | 1459 | | } |
| | | 1460 | | |
| | | 1461 | | |
| | | 1462 | | var labelKeys = JsonConvert.DeserializeObject<List<string>>(labelKeysJson); |
| | | 1463 | | if (labelKeys == null || labelKeys.Count == 0) |
| | | 1464 | | { |
| | | 1465 | | return NotFound("Файлы этикеток не найдены или срок их хранения истек"); |
| | | 1466 | | } |
| | | 1467 | | |
| | | 1468 | | Dictionary<string, byte[]> labels = new Dictionary<string, byte[]>(); |
| | | 1469 | | |
| | | 1470 | | |
| | | 1471 | | foreach (var key in labelKeys) |
| | | 1472 | | |
| | | 1473 | | { |
| | | 1474 | | await Task.CompletedTask; |
| | | 1475 | | var fileKey = $"{sessionKey}_{key}"; |
| | | 1476 | | |
| | | 1477 | | var bytes = HttpContext.Session.Get(fileKey); |
| | | 1478 | | |
| | | 1479 | | if (bytes != null) |
| | | 1480 | | |
| | | 1481 | | { |
| | | 1482 | | labels.Add(key, bytes); |
| | | 1483 | | } |
| | | 1484 | | } |
| | | 1485 | | |
| | | 1486 | | |
| | | 1487 | | if (labels.Count == 0) |
| | | 1488 | | |
| | | 1489 | | { |
| | | 1490 | | return NotFound("Файлы этикеток не найдены"); |
| | | 1491 | | } |
| | | 1492 | | |
| | | 1493 | | |
| | | 1494 | | // Очищаем сессию после использования |
| | | 1495 | | |
| | | 1496 | | HttpContext.Session.Remove(sessionKey); |
| | | 1497 | | |
| | | 1498 | | foreach (var key in labelKeys) |
| | | 1499 | | |
| | | 1500 | | { |
| | | 1501 | | var fileKey = $"{sessionKey}_{key}"; |
| | | 1502 | | |
| | | 1503 | | HttpContext.Session.Remove(fileKey); |
| | | 1504 | | } |
| | | 1505 | | |
| | | 1506 | | |
| | | 1507 | | // Если только один файл - возвращаем его напрямую |
| | | 1508 | | |
| | | 1509 | | if (labels.Count == 1) |
| | | 1510 | | |
| | | 1511 | | { |
| | | 1512 | | var label = labels.First(); |
| | | 1513 | | |
| | | 1514 | | Response.Headers.Append("Content-Disposition", $"inline; filename=\"{label.Key}.pdf\""); |
| | | 1515 | | return File(label.Value, "application/pdf"); |
| | | 1516 | | } |
| | | 1517 | | |
| | | 1518 | | |
| | | 1519 | | // Несколько этикеток возвращаем единым многостраничным PDF. |
| | | 1520 | | await using var mergedStream = new MemoryStream(); |
| | | 1521 | | using var mergedPdf = new PdfDocument(new PdfWriter(mergedStream)); |
| | | 1522 | | foreach (var label in labels) |
| | | 1523 | | { |
| | | 1524 | | await using var sourceStream = new MemoryStream(label.Value); |
| | | 1525 | | using var sourcePdf = new PdfDocument(new PdfReader(sourceStream)); |
| | | 1526 | | sourcePdf.CopyPagesTo(1, sourcePdf.GetNumberOfPages(), mergedPdf); |
| | | 1527 | | } |
| | | 1528 | | |
| | | 1529 | | mergedPdf.Close(); |
| | | 1530 | | Response.Headers.Append("Content-Disposition", |
| | | 1531 | | $"inline; filename=\"labels_{DateTime.Now:yyyyMMdd_HHmmss}.pdf\""); |
| | | 1532 | | return File(mergedStream.ToArray(), "application/pdf"); |
| | | 1533 | | } |
| | | 1534 | | |
| | | 1535 | | catch (Exception ex) |
| | | 1536 | | { |
| | | 1537 | | return StatusCode(500, $"Ошибка при скачивании этикеток: {ex.Message}"); |
| | | 1538 | | } |
| | | 1539 | | } |
| | | 1540 | | |
| | | 1541 | | |
| | | 1542 | | [ServiceEndpoint] |
| | | 1543 | | [HttpPost] |
| | | 1544 | | public IActionResult ClearLabelsSession(string sessionKey) |
| | | 1545 | | { |
| | | 1546 | | _ozonLabelSessionStore.ClearSession(HttpContext.Session, sessionKey); |
| | | 1547 | | return Ok(new { success = true }); |
| | | 1548 | | } |
| | | 1549 | | |
| | | 1550 | | |
| | | 1551 | | private IActionResult? SendFilesToUser(Dictionary<string, byte[]> files) |
| | | 1552 | | |
| | | 1553 | | { |
| | | 1554 | | List<FileContentResult> fileResults = new List<FileContentResult>(); |
| | | 1555 | | |
| | | 1556 | | |
| | | 1557 | | foreach (var file in files) |
| | | 1558 | | |
| | | 1559 | | { |
| | | 1560 | | var fileResult = new FileContentResult(file.Value, "application/pdf") |
| | | 1561 | | |
| | | 1562 | | { |
| | | 1563 | | FileDownloadName = $"{file.Key}.pdf" |
| | | 1564 | | }; |
| | | 1565 | | |
| | | 1566 | | |
| | | 1567 | | fileResults.Add(fileResult); |
| | | 1568 | | } |
| | | 1569 | | |
| | | 1570 | | |
| | | 1571 | | if (fileResults.Count == 0) |
| | | 1572 | | |
| | | 1573 | | { |
| | | 1574 | | return null; |
| | | 1575 | | } |
| | | 1576 | | |
| | | 1577 | | |
| | | 1578 | | // Если только один файл — отдаем его напрямую, без ZIP |
| | | 1579 | | |
| | | 1580 | | if (fileResults.Count == 1) |
| | | 1581 | | |
| | | 1582 | | { |
| | | 1583 | | var single = fileResults[0]; |
| | | 1584 | | |
| | | 1585 | | return File(single.FileContents, single.ContentType, single.FileDownloadName); |
| | | 1586 | | } |
| | | 1587 | | |
| | | 1588 | | |
| | | 1589 | | string tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); |
| | | 1590 | | |
| | | 1591 | | Directory.CreateDirectory(tempDirectory); |
| | | 1592 | | |
| | | 1593 | | |
| | | 1594 | | List<string> tempFiles = new List<string>(); |
| | | 1595 | | |
| | | 1596 | | foreach (var fileResult in fileResults) |
| | | 1597 | | |
| | | 1598 | | { |
| | | 1599 | | string tempFilePath = Path.Combine(tempDirectory, fileResult.FileDownloadName); |
| | | 1600 | | |
| | | 1601 | | System.IO.File.WriteAllBytes(tempFilePath, fileResult.FileContents); |
| | | 1602 | | |
| | | 1603 | | tempFiles.Add(tempFilePath); |
| | | 1604 | | } |
| | | 1605 | | |
| | | 1606 | | |
| | | 1607 | | string zipFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip"); |
| | | 1608 | | |
| | | 1609 | | ZipFile.CreateFromDirectory(tempDirectory, zipFilePath); |
| | | 1610 | | |
| | | 1611 | | |
| | | 1612 | | Directory.Delete(tempDirectory, true); |
| | | 1613 | | |
| | | 1614 | | |
| | | 1615 | | return PhysicalFile(zipFilePath, "application/zip", $"labels_{DateTime.Now:dd.MM.yyyy}.zip"); |
| | | 1616 | | } |
| | | 1617 | | } |
| | | 1618 | | |
| | | 1619 | | public sealed class OzonProductCountrySelection |
| | | 1620 | | { |
| | 0 | 1621 | | public int OrderId { get; set; } |
| | 0 | 1622 | | public long ProductId { get; set; } |
| | 0 | 1623 | | public string CountryIsoCode { get; set; } = string.Empty; |
| | | 1624 | | } |
| | | 1625 | | |
| | | 1626 | | /// <summary>Данные экземпляров одного заказа, введённые в форме перед печатью этикетки.</summary> |
| | | 1627 | | public sealed class OzonExemplarSubmission |
| | | 1628 | | { |
| | | 1629 | | public int OrderId { get; set; } |
| | | 1630 | | |
| | | 1631 | | public List<OzonExemplarInput> Exemplars { get; set; } = new(); |
| | | 1632 | | } |