| | | 1 | | using Microsoft.AspNetCore.Mvc; |
| | | 2 | | using OfficeOpenXml; |
| | | 3 | | using System.ComponentModel.DataAnnotations; |
| | | 4 | | using Microsoft.AspNetCore.Authorization; |
| | | 5 | | using OzonOrdersWeb.Areas.OzonCards.ViewModels; |
| | | 6 | | using Services.DataServices.ExcelMapping; |
| | | 7 | | |
| | | 8 | | namespace ExcelProcessor.Controllers |
| | | 9 | | { |
| | | 10 | | [Area("OzonCards")] |
| | | 11 | | [Authorize(Roles = "User,Admin")] |
| | | 12 | | public class ExcelController : Controller |
| | | 13 | | { |
| | | 14 | | private readonly IWebHostEnvironment _environment; |
| | | 15 | | private readonly IExcelMappingService _mappingService; |
| | 0 | 16 | | public ExcelController(IWebHostEnvironment environment, |
| | 0 | 17 | | IExcelMappingService mappingService) |
| | | 18 | | { |
| | 0 | 19 | | _environment = environment; |
| | 0 | 20 | | ExcelPackage.LicenseContext = LicenseContext.NonCommercial; |
| | 0 | 21 | | _mappingService = mappingService; |
| | 0 | 22 | | } |
| | | 23 | | |
| | | 24 | | [HttpGet] |
| | | 25 | | public async Task<IActionResult> Index(int? mappingId) |
| | | 26 | | { |
| | 0 | 27 | | if(mappingId == null) |
| | 0 | 28 | | return View(); |
| | | 29 | | |
| | 0 | 30 | | var mapping = await _mappingService.GetMappingAsync(mappingId.Value); |
| | 0 | 31 | | return View(mapping); |
| | 0 | 32 | | } |
| | | 33 | | |
| | | 34 | | |
| | | 35 | | |
| | | 36 | | [HttpPost] |
| | | 37 | | public async Task<IActionResult> UploadFiles(IFormFile sourceFile, IFormFile destinationFile) |
| | | 38 | | { |
| | | 39 | | try |
| | | 40 | | { |
| | 0 | 41 | | if (sourceFile == null || destinationFile == null) |
| | | 42 | | { |
| | 0 | 43 | | return Json(new { success = false, message = "Оба файла должны быть загружены" }); |
| | | 44 | | } |
| | | 45 | | |
| | | 46 | | // Сохраняем файлы и получаем их имена |
| | 0 | 47 | | var sourceFileName = await SaveFile(sourceFile); |
| | 0 | 48 | | var destFileName = await SaveFile(destinationFile); |
| | | 49 | | |
| | 0 | 50 | | var fileInfo = await GetFileInfo(sourceFile, destinationFile); |
| | | 51 | | |
| | 0 | 52 | | return Json(new |
| | 0 | 53 | | { |
| | 0 | 54 | | success = true, |
| | 0 | 55 | | sourceFileName, |
| | 0 | 56 | | destFileName, |
| | 0 | 57 | | sourceSheets = fileInfo.SourceSheets, |
| | 0 | 58 | | destinationSheets = fileInfo.DestinationSheets, |
| | 0 | 59 | | sourceColumns = fileInfo.SourceColumns, |
| | 0 | 60 | | destinationColumns = fileInfo.DestinationColumns |
| | 0 | 61 | | }); |
| | | 62 | | } |
| | 0 | 63 | | catch (Exception ex) |
| | | 64 | | { |
| | 0 | 65 | | return Json(new { success = false, message = ex.Message }); |
| | | 66 | | } |
| | 0 | 67 | | } |
| | | 68 | | |
| | | 69 | | [HttpPost] |
| | | 70 | | public async Task<IActionResult> ProcessExcel([FromForm] ExcelMappingRequest request, string? MappingName = null |
| | | 71 | | { |
| | | 72 | | try |
| | | 73 | | { |
| | 0 | 74 | | if (string.IsNullOrEmpty(request.SourceFileName) || string.IsNullOrEmpty(request.DestinationFileName)) |
| | | 75 | | { |
| | 0 | 76 | | return BadRequest("Файлы не были загружены"); |
| | | 77 | | } |
| | | 78 | | |
| | 0 | 79 | | if (request.ColumnMappings == null || !request.ColumnMappings.Any()) |
| | | 80 | | { |
| | 0 | 81 | | return BadRequest("Не указаны сопоставления столбцов"); |
| | | 82 | | } |
| | | 83 | | |
| | 0 | 84 | | var result = await ProcessExcelFiles(request); |
| | | 85 | | |
| | | 86 | | |
| | 0 | 87 | | DeleteTempFile(request.SourceFileName); |
| | 0 | 88 | | DeleteTempFile(request.DestinationFileName); |
| | | 89 | | |
| | 0 | 90 | | if (result.Success) |
| | | 91 | | { |
| | 0 | 92 | | if (MappingName != null) |
| | | 93 | | { |
| | 0 | 94 | | var id = await _mappingService.SaveMappingAsync(request, MappingName, User.Identity?.Name ?? str |
| | | 95 | | } |
| | | 96 | | |
| | 0 | 97 | | return File(result.ResultFile, |
| | 0 | 98 | | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
| | 0 | 99 | | result.FileName); |
| | | 100 | | } |
| | | 101 | | else |
| | | 102 | | { |
| | 0 | 103 | | return BadRequest(result.Message); |
| | | 104 | | } |
| | | 105 | | } |
| | 0 | 106 | | catch (Exception ex) |
| | | 107 | | { |
| | 0 | 108 | | DeleteTempFile(request.SourceFileName); |
| | 0 | 109 | | DeleteTempFile(request.DestinationFileName); |
| | 0 | 110 | | return StatusCode(500, $"Ошибка обработки: {ex.Message}"); |
| | | 111 | | } |
| | 0 | 112 | | } |
| | | 113 | | |
| | | 114 | | [HttpPost] |
| | | 115 | | public async Task<IActionResult> GetColumnsInfo( |
| | | 116 | | IFormFile sourceFile, |
| | | 117 | | IFormFile destinationFile, |
| | | 118 | | string sourceSheet, |
| | | 119 | | string destinationSheet, |
| | | 120 | | int sourceHeaderRow = 1, |
| | | 121 | | int destinationHeaderRow = 1) |
| | | 122 | | { |
| | | 123 | | try |
| | | 124 | | { |
| | 0 | 125 | | if (sourceFile == null || destinationFile == null) |
| | | 126 | | { |
| | 0 | 127 | | return Json(new { success = false, message = "Файлы не были загружены" }); |
| | | 128 | | } |
| | | 129 | | |
| | 0 | 130 | | using (var sourceStream = new MemoryStream()) |
| | 0 | 131 | | using (var destStream = new MemoryStream()) |
| | | 132 | | { |
| | 0 | 133 | | await sourceFile.CopyToAsync(sourceStream); |
| | 0 | 134 | | await destinationFile.CopyToAsync(destStream); |
| | | 135 | | |
| | 0 | 136 | | sourceStream.Position = 0; |
| | 0 | 137 | | destStream.Position = 0; |
| | | 138 | | |
| | 0 | 139 | | using (var sourcePackage = new ExcelPackage(sourceStream)) |
| | 0 | 140 | | using (var destPackage = new ExcelPackage(destStream)) |
| | | 141 | | { |
| | 0 | 142 | | var sourceWorksheet = sourcePackage.Workbook.Worksheets |
| | 0 | 143 | | .FirstOrDefault(ws => ws.Name.Equals(sourceSheet, StringComparison.OrdinalIgnoreCase)); |
| | | 144 | | |
| | 0 | 145 | | var destWorksheet = destPackage.Workbook.Worksheets |
| | 0 | 146 | | .FirstOrDefault(ws => ws.Name.Equals(destinationSheet, StringComparison.OrdinalIgnoreCase)); |
| | | 147 | | |
| | 0 | 148 | | if (sourceWorksheet == null || destWorksheet == null) |
| | | 149 | | { |
| | 0 | 150 | | return Json(new { success = false, message = "Указанные листы не найдены" }); |
| | | 151 | | } |
| | | 152 | | |
| | 0 | 153 | | var sourceHeaders = GetHeaders(sourceWorksheet, sourceHeaderRow); |
| | 0 | 154 | | var destHeaders = GetHeaders(destWorksheet, destinationHeaderRow); |
| | | 155 | | |
| | 0 | 156 | | var sourceExamples = GetColumnExamples(sourceWorksheet, sourceHeaderRow + 1, sourceHeaders); |
| | 0 | 157 | | var destExamples = GetColumnExamples(destWorksheet, destinationHeaderRow + 1, destHeaders); |
| | | 158 | | |
| | 0 | 159 | | return Json(new |
| | 0 | 160 | | { |
| | 0 | 161 | | success = true, |
| | 0 | 162 | | sourceColumns = sourceHeaders.Keys.Select(k => new |
| | 0 | 163 | | { |
| | 0 | 164 | | name = k, |
| | 0 | 165 | | example = sourceExamples.ContainsKey(k) ? sourceExamples[k] : "" |
| | 0 | 166 | | }), |
| | 0 | 167 | | destinationColumns = destHeaders.Keys.Select(k => new |
| | 0 | 168 | | { |
| | 0 | 169 | | name = k, |
| | 0 | 170 | | example = destExamples.ContainsKey(k) ? destExamples[k] : "" |
| | 0 | 171 | | }) |
| | 0 | 172 | | }); |
| | | 173 | | } |
| | | 174 | | } |
| | | 175 | | } |
| | 0 | 176 | | catch (Exception ex) |
| | | 177 | | { |
| | 0 | 178 | | return Json(new { success = false, message = ex.Message }); |
| | | 179 | | } |
| | 0 | 180 | | } |
| | | 181 | | |
| | | 182 | | private async Task<string?> SaveFile(IFormFile file) |
| | | 183 | | { |
| | 0 | 184 | | if (file == null || file.Length == 0) |
| | 0 | 185 | | return null; |
| | | 186 | | |
| | 0 | 187 | | var uploadsPath = Path.Combine(_environment.WebRootPath, "uploads"); |
| | 0 | 188 | | if (!Directory.Exists(uploadsPath)) |
| | 0 | 189 | | Directory.CreateDirectory(uploadsPath); |
| | | 190 | | |
| | 0 | 191 | | var fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName); |
| | 0 | 192 | | var filePath = Path.Combine(uploadsPath, fileName); |
| | | 193 | | |
| | 0 | 194 | | using (var stream = new FileStream(filePath, FileMode.Create)) |
| | | 195 | | { |
| | 0 | 196 | | await file.CopyToAsync(stream); |
| | 0 | 197 | | } |
| | | 198 | | |
| | 0 | 199 | | return fileName; |
| | 0 | 200 | | } |
| | | 201 | | |
| | | 202 | | private void DeleteTempFile(string fileName) |
| | | 203 | | { |
| | 0 | 204 | | if (!string.IsNullOrEmpty(fileName)) |
| | | 205 | | { |
| | 0 | 206 | | var filePath = Path.Combine(_environment.WebRootPath, "uploads", fileName); |
| | 0 | 207 | | if (System.IO.File.Exists(filePath)) |
| | | 208 | | { |
| | | 209 | | try |
| | | 210 | | { |
| | 0 | 211 | | System.IO.File.Delete(filePath); |
| | 0 | 212 | | } |
| | 0 | 213 | | catch |
| | | 214 | | { |
| | | 215 | | // Игнорируем ошибки удаления |
| | 0 | 216 | | } |
| | | 217 | | } |
| | | 218 | | } |
| | 0 | 219 | | } |
| | | 220 | | |
| | | 221 | | private Dictionary<string, string> GetColumnExamples(ExcelWorksheet worksheet, int dataRow, Dictionary<string, i |
| | | 222 | | { |
| | 0 | 223 | | var examples = new Dictionary<string, string>(); |
| | | 224 | | |
| | 0 | 225 | | foreach (var header in headers) |
| | | 226 | | { |
| | 0 | 227 | | var value = worksheet.Cells[dataRow, header.Value].Value?.ToString(); |
| | 0 | 228 | | examples[header.Key] = value ?? "нет данных"; |
| | | 229 | | } |
| | | 230 | | |
| | 0 | 231 | | return examples; |
| | | 232 | | } |
| | | 233 | | |
| | | 234 | | private async Task<FileInfoResponse> GetFileInfo(IFormFile sourceFile, IFormFile destinationFile) |
| | | 235 | | { |
| | 0 | 236 | | var response = new FileInfoResponse(); |
| | | 237 | | |
| | 0 | 238 | | using (var sourceStream = new MemoryStream()) |
| | 0 | 239 | | using (var destStream = new MemoryStream()) |
| | | 240 | | { |
| | 0 | 241 | | await sourceFile.CopyToAsync(sourceStream); |
| | 0 | 242 | | await destinationFile.CopyToAsync(destStream); |
| | | 243 | | |
| | 0 | 244 | | sourceStream.Position = 0; |
| | 0 | 245 | | destStream.Position = 0; |
| | | 246 | | |
| | 0 | 247 | | using (var sourcePackage = new ExcelPackage(sourceStream)) |
| | 0 | 248 | | using (var destPackage = new ExcelPackage(destStream)) |
| | | 249 | | { |
| | 0 | 250 | | response.SourceSheets = sourcePackage.Workbook.Worksheets |
| | 0 | 251 | | .Select(ws => ws.Name) |
| | 0 | 252 | | .ToList(); |
| | | 253 | | |
| | 0 | 254 | | response.DestinationSheets = destPackage.Workbook.Worksheets |
| | 0 | 255 | | .Select(ws => ws.Name) |
| | 0 | 256 | | .ToList(); |
| | | 257 | | |
| | 0 | 258 | | if (response.SourceSheets.Any()) |
| | | 259 | | { |
| | 0 | 260 | | var firstSourceSheet = sourcePackage.Workbook.Worksheets[response.SourceSheets[0]]; |
| | 0 | 261 | | response.SourceColumns = GetHeaders(firstSourceSheet, 1).Keys.ToList(); |
| | | 262 | | } |
| | | 263 | | |
| | 0 | 264 | | if (response.DestinationSheets.Any()) |
| | | 265 | | { |
| | 0 | 266 | | var firstDestSheet = destPackage.Workbook.Worksheets[response.DestinationSheets[0]]; |
| | 0 | 267 | | response.DestinationColumns = GetHeaders(firstDestSheet, 1).Keys.ToList(); |
| | | 268 | | } |
| | 0 | 269 | | } |
| | 0 | 270 | | } |
| | | 271 | | |
| | 0 | 272 | | return response; |
| | 0 | 273 | | } |
| | | 274 | | |
| | | 275 | | private async Task<ExcelProcessingResult> ProcessExcelFiles(ExcelMappingRequest request) |
| | | 276 | | { |
| | 0 | 277 | | await Task.CompletedTask; |
| | 0 | 278 | | var sourceFilePath = Path.Combine(_environment.WebRootPath, "uploads", request.SourceFileName); |
| | 0 | 279 | | var destFilePath = Path.Combine(_environment.WebRootPath, "uploads", request.DestinationFileName); |
| | | 280 | | |
| | 0 | 281 | | if (!System.IO.File.Exists(sourceFilePath) || !System.IO.File.Exists(destFilePath)) |
| | | 282 | | { |
| | 0 | 283 | | return new ExcelProcessingResult |
| | 0 | 284 | | { |
| | 0 | 285 | | Success = false, |
| | 0 | 286 | | Message = "Файлы не найдены на сервере" |
| | 0 | 287 | | }; |
| | | 288 | | } |
| | | 289 | | |
| | 0 | 290 | | using (var sourceStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read)) |
| | 0 | 291 | | using (var destStream = new FileStream(destFilePath, FileMode.Open, FileAccess.ReadWrite)) |
| | | 292 | | { |
| | 0 | 293 | | using (var sourcePackage = new ExcelPackage(sourceStream)) |
| | 0 | 294 | | using (var destPackage = new ExcelPackage(destStream)) |
| | | 295 | | { |
| | 0 | 296 | | var sourceWorksheet = sourcePackage.Workbook.Worksheets |
| | 0 | 297 | | .FirstOrDefault(ws => ws.Name.Equals(request.SourceSheet, StringComparison.OrdinalIgnoreCase)); |
| | | 298 | | |
| | 0 | 299 | | var destWorksheet = destPackage.Workbook.Worksheets |
| | 0 | 300 | | .FirstOrDefault(ws => ws.Name.Equals(request.DestinationSheet, StringComparison.OrdinalIgnoreCas |
| | | 301 | | |
| | 0 | 302 | | if (sourceWorksheet == null) |
| | | 303 | | { |
| | 0 | 304 | | return new ExcelProcessingResult |
| | 0 | 305 | | { |
| | 0 | 306 | | Success = false, |
| | 0 | 307 | | Message = $"Лист '{request.SourceSheet}' не найден в исходном файле" |
| | 0 | 308 | | }; |
| | | 309 | | } |
| | | 310 | | |
| | 0 | 311 | | if (destWorksheet == null) |
| | | 312 | | { |
| | 0 | 313 | | return new ExcelProcessingResult |
| | 0 | 314 | | { |
| | 0 | 315 | | Success = false, |
| | 0 | 316 | | Message = $"Лист '{request.DestinationSheet}' не найден в целевом файле" |
| | 0 | 317 | | }; |
| | | 318 | | } |
| | | 319 | | |
| | 0 | 320 | | var columnIndexMap = GetColumnIndexMapping(sourceWorksheet, destWorksheet, request); |
| | | 321 | | |
| | 0 | 322 | | if (columnIndexMap == null) |
| | | 323 | | { |
| | 0 | 324 | | return new ExcelProcessingResult |
| | 0 | 325 | | { |
| | 0 | 326 | | Success = false, |
| | 0 | 327 | | Message = "Ошибка сопоставления столбцов" |
| | 0 | 328 | | }; |
| | | 329 | | } |
| | | 330 | | |
| | 0 | 331 | | int processedRows = CopyData(sourceWorksheet, destWorksheet, request, columnIndexMap); |
| | | 332 | | |
| | 0 | 333 | | using (var resultStream = new MemoryStream()) |
| | | 334 | | { |
| | 0 | 335 | | destPackage.SaveAs(resultStream); |
| | 0 | 336 | | resultStream.Position = 0; |
| | | 337 | | |
| | 0 | 338 | | return new ExcelProcessingResult |
| | 0 | 339 | | { |
| | 0 | 340 | | Success = true, |
| | 0 | 341 | | Message = $"Успешно обработано {processedRows} строк", |
| | 0 | 342 | | ProcessedRows = processedRows, |
| | 0 | 343 | | ResultFile = resultStream.ToArray(), |
| | 0 | 344 | | FileName = $"processed_{Path.GetFileNameWithoutExtension(request.DestinationFileName)}.xlsx" |
| | 0 | 345 | | }; |
| | | 346 | | } |
| | | 347 | | } |
| | | 348 | | } |
| | 0 | 349 | | } |
| | | 350 | | |
| | | 351 | | private Dictionary<int, int> GetColumnIndexMapping( |
| | | 352 | | ExcelWorksheet sourceWorksheet, |
| | | 353 | | ExcelWorksheet destWorksheet, |
| | | 354 | | ExcelMappingRequest request) |
| | | 355 | | { |
| | 0 | 356 | | var mapping = new Dictionary<int, int>(); |
| | | 357 | | |
| | 0 | 358 | | var sourceHeaders = GetHeaders(sourceWorksheet, request.SourceHeaderRow); |
| | 0 | 359 | | var destHeaders = GetHeaders(destWorksheet, request.DestinationHeaderRow); |
| | | 360 | | |
| | 0 | 361 | | foreach (var columnMapping in request.ColumnMappings) |
| | | 362 | | { |
| | 0 | 363 | | if (!sourceHeaders.ContainsKey(columnMapping.SourceColumn)) |
| | | 364 | | { |
| | 0 | 365 | | throw new ValidationException($"Столбец '{columnMapping.SourceColumn}' не найден в исходном файле"); |
| | | 366 | | } |
| | | 367 | | |
| | 0 | 368 | | if (!destHeaders.ContainsKey(columnMapping.DestinationColumn)) |
| | | 369 | | { |
| | 0 | 370 | | throw new ValidationException($"Столбец '{columnMapping.DestinationColumn}' не найден в целевом файл |
| | | 371 | | } |
| | | 372 | | |
| | 0 | 373 | | int sourceColIndex = sourceHeaders[columnMapping.SourceColumn]; |
| | 0 | 374 | | int destColIndex = destHeaders[columnMapping.DestinationColumn]; |
| | | 375 | | |
| | 0 | 376 | | mapping.Add(sourceColIndex, destColIndex); |
| | | 377 | | } |
| | | 378 | | |
| | 0 | 379 | | return mapping; |
| | | 380 | | } |
| | | 381 | | |
| | | 382 | | private Dictionary<string, int> GetHeaders(ExcelWorksheet worksheet, int headerRow) |
| | | 383 | | { |
| | 0 | 384 | | var headers = new Dictionary<string, int>(); |
| | 0 | 385 | | int col = 1; |
| | | 386 | | |
| | 0 | 387 | | while (true) |
| | | 388 | | { |
| | 0 | 389 | | var headerValue = worksheet.Cells[headerRow, col].Value?.ToString(); |
| | 0 | 390 | | if (string.IsNullOrEmpty(headerValue)) |
| | | 391 | | break; |
| | | 392 | | |
| | 0 | 393 | | headers[headerValue.Trim()] = col; |
| | 0 | 394 | | col++; |
| | | 395 | | } |
| | | 396 | | |
| | 0 | 397 | | return headers; |
| | | 398 | | } |
| | | 399 | | |
| | | 400 | | private int CopyData( |
| | | 401 | | ExcelWorksheet sourceWorksheet, |
| | | 402 | | ExcelWorksheet destWorksheet, |
| | | 403 | | ExcelMappingRequest request, |
| | | 404 | | Dictionary<int, int> columnIndexMap) |
| | | 405 | | { |
| | 0 | 406 | | int processedRows = 0; |
| | 0 | 407 | | int sourceRow = request.SourceDataStartRow; |
| | 0 | 408 | | int destRow = request.DestinationDataStartRow; |
| | | 409 | | |
| | 0 | 410 | | while (true) |
| | | 411 | | { |
| | 0 | 412 | | bool hasData = false; |
| | 0 | 413 | | foreach (var mapping in columnIndexMap) |
| | | 414 | | { |
| | 0 | 415 | | var sourceValue = sourceWorksheet.Cells[sourceRow, mapping.Key].Value; |
| | 0 | 416 | | if (sourceValue != null && !string.IsNullOrEmpty(sourceValue.ToString())) |
| | | 417 | | { |
| | 0 | 418 | | hasData = true; |
| | 0 | 419 | | break; |
| | | 420 | | } |
| | | 421 | | } |
| | | 422 | | |
| | 0 | 423 | | if (!hasData) |
| | | 424 | | break; |
| | | 425 | | |
| | 0 | 426 | | foreach (var mapping in columnIndexMap) |
| | | 427 | | { |
| | 0 | 428 | | var sourceValue = sourceWorksheet.Cells[sourceRow, mapping.Key].Value; |
| | 0 | 429 | | destWorksheet.Cells[destRow, mapping.Value].Value = sourceValue; |
| | | 430 | | } |
| | | 431 | | |
| | 0 | 432 | | sourceRow++; |
| | 0 | 433 | | destRow++; |
| | 0 | 434 | | processedRows++; |
| | | 435 | | } |
| | | 436 | | |
| | 0 | 437 | | return processedRows; |
| | | 438 | | } |
| | | 439 | | |
| | | 440 | | // Периодическая очистка старых файлов |
| | | 441 | | private void CleanOldTempFiles() |
| | | 442 | | { |
| | | 443 | | try |
| | | 444 | | { |
| | 0 | 445 | | var uploadsPath = Path.Combine(_environment.WebRootPath, "uploads"); |
| | 0 | 446 | | if (Directory.Exists(uploadsPath)) |
| | | 447 | | { |
| | 0 | 448 | | foreach (var file in Directory.GetFiles(uploadsPath)) |
| | | 449 | | { |
| | 0 | 450 | | var fileInfo = new FileInfo(file); |
| | 0 | 451 | | if (fileInfo.LastWriteTime < DateTime.Now.AddHours(-1)) |
| | | 452 | | { |
| | 0 | 453 | | fileInfo.Delete(); |
| | | 454 | | } |
| | | 455 | | } |
| | | 456 | | } |
| | 0 | 457 | | } |
| | 0 | 458 | | catch |
| | | 459 | | { |
| | | 460 | | // Игнорируем ошибки очистки |
| | 0 | 461 | | } |
| | 0 | 462 | | } |
| | | 463 | | } |
| | | 464 | | } |