From d5dad225b5b7f9c36ad3393a7742728936f42bd4 Mon Sep 17 00:00:00 2001 From: "Tizian.Breuch" Date: Tue, 25 Nov 2025 10:55:53 +0100 Subject: [PATCH] bilder --- .../Services/Admin/AdminProductService.cs | 97 +++++++++++++------ 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/Webshop.Application/Services/Admin/AdminProductService.cs b/Webshop.Application/Services/Admin/AdminProductService.cs index 3c1a55f..510ef47 100644 --- a/Webshop.Application/Services/Admin/AdminProductService.cs +++ b/Webshop.Application/Services/Admin/AdminProductService.cs @@ -114,7 +114,9 @@ namespace Webshop.Application.Services.Admin public async Task UpdateAdminProductAsync(UpdateAdminProductDto productDto) { - // 1. Produkt laden (Tracked) + Console.WriteLine($"---- UPDATE START: Produkt-ID {productDto.Id} ----"); + + // 1. Produkt aus der DB laden (inklusive aktueller RowVersion) var existingProduct = await _context.Products .Include(p => p.Images) .Include(p => p.Productcategories) @@ -125,23 +127,32 @@ namespace Webshop.Application.Services.Admin return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{productDto.Id}' nicht gefunden."); } - // 2. Concurrency Check (nur initial) + // 2. ROBUSTER CONCURRENCY CHECK (Manuell) + // Wir vergleichen die Strings direkt. Das ist sicherer als Byte-Manipulation. if (!string.IsNullOrEmpty(productDto.RowVersion)) { - try + // DB-Wert in String wandeln + string dbRowVersion = Convert.ToBase64String(existingProduct.RowVersion ?? new byte[0]); + + // Frontend-Wert bereinigen (Leerzeichen zu Plus, falls URL-Decoded wurde) + string incomingRowVersion = productDto.RowVersion.Trim().Replace(" ", "+"); + + Console.WriteLine($"DB Version: '{dbRowVersion}'"); + Console.WriteLine($"Frontend Version: '{incomingRowVersion}'"); + + if (dbRowVersion != incomingRowVersion) { - string fixedRowVersion = productDto.RowVersion.Replace(" ", "+"); - byte[] incomingRowVersion = Convert.FromBase64String(fixedRowVersion); - _context.Entry(existingProduct).Property(p => p.RowVersion).OriginalValue = incomingRowVersion; - } - catch (FormatException) - { - return ServiceResult.Fail(ServiceResultType.Failure, "RowVersion Format ist ungültig."); + Console.WriteLine("!!! KONFLIKT: Versionen stimmen nicht überein !!!"); + return ServiceResult.Fail(ServiceResultType.Conflict, "Das Produkt wurde in der Zwischenzeit von jemand anderem bearbeitet. Bitte laden Sie die Seite neu."); } } + // Wenn wir hier sind, ist die Version KORREKT. + // Wir müssen OriginalValue NICHT mehr setzen, da existingProduct ja + // die aktuelle Version aus der DB hat. + // ----------------------------------------------------------------------- - // SCHRITT A: BILDER & KATEGORIEN UPDATE + // SCHRITT A: BILDER UPDATE // ----------------------------------------------------------------------- bool imagesChanged = false; @@ -156,7 +167,7 @@ namespace Webshop.Application.Services.Admin } } - // Hauptbild + // Hauptbild ersetzen if (productDto.MainImageFile != null) { var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage); @@ -168,7 +179,7 @@ namespace Webshop.Application.Services.Admin imagesChanged = true; } - // Zusatzbilder + // Zusatzbilder hinzufügen if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) { int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1; @@ -181,42 +192,63 @@ namespace Webshop.Application.Services.Admin imagesChanged = true; } - // Kategorien - existingProduct.Productcategories.Clear(); + // Kategorien aktualisieren if (productDto.CategorieIds != null) { - foreach (var categorieId in productDto.CategorieIds) + // Bestehende IDs + var currentIds = existingProduct.Productcategories.Select(pc => pc.categorieId).ToList(); + // Neue IDs + var newIds = productDto.CategorieIds; + + // Nur ändern, wenn wirklich anders (vermeidet unnötige DB-Writes) + if (!new HashSet(currentIds).SetEquals(newIds)) { - existingProduct.Productcategories.Add(new Productcategorie { categorieId = categorieId }); + existingProduct.Productcategories.Clear(); + foreach (var categorieId in newIds) + { + existingProduct.Productcategories.Add(new Productcategorie { categorieId = categorieId }); + } } } - // WICHTIG: Wenn Bilder geändert wurden, speichern wir HIER schon einmal zwischen. - // Das erlaubt der DB, Trigger auszuführen und die RowVersion zu aktualisieren. + // SCHRITT A SPEICHERN (Nur wenn Bilder geändert wurden) if (imagesChanged) { + // Wir aktualisieren den Zeitstempel manuell, damit der User Feedback hat + existingProduct.LastModifiedDate = DateTimeOffset.UtcNow; + existingProduct.RowVersion = Guid.NewGuid().ToByteArray(); + try { await _context.SaveChangesAsync(); - - // Ganz wichtig: Die RowVersion im Entity neu laden, da sie sich in der DB geändert hat! - // Aber: Da wir das Objekt weiterverwenden wollen, müssen wir sicherstellen, - // dass EF Core weiß, dass die Version jetzt "neu" ist. - // Der einfachste Weg: Wir holen uns das aktuelle Token aus der DB. - await _context.Entry(existingProduct).ReloadAsync(); + // Da wir gespeichert haben, hat das Entity jetzt eine NEUE RowVersion in der DB. + // Aber unser EF-Kontext weiß das bereits, weil das Objekt getracked ist. } - catch (DbUpdateConcurrencyException) + catch (Exception ex) { - return ServiceResult.Fail(ServiceResultType.Conflict, "Konflikt beim Aktualisieren der Bilder."); + Console.WriteLine($"Fehler beim Bild-Speichern: {ex.Message}"); + return ServiceResult.Fail(ServiceResultType.Failure, "Fehler beim Speichern der Bilder."); } } // ----------------------------------------------------------------------- - // SCHRITT B: PRODUKT EIGENSCHAFTEN UPDATE + // SCHRITT B: PRODUKT EIGENSCHAFTEN // ----------------------------------------------------------------------- - // Check auf Duplikate (SKU/Slug) hier wie gehabt... + // Check auf Duplikate (SKU/Slug) - nur wenn geändert + if (existingProduct.SKU != productDto.SKU) + { + bool skuExists = await _context.Products.AnyAsync(p => p.SKU == productDto.SKU && p.Id != productDto.Id); + if (skuExists) return ServiceResult.Fail(ServiceResultType.Conflict, $"SKU '{productDto.SKU}' existiert bereits."); + } + if (existingProduct.Slug != productDto.Slug) + { + bool slugExists = await _context.Products.AnyAsync(p => p.Slug == productDto.Slug && p.Id != productDto.Id); + if (slugExists) return ServiceResult.Fail(ServiceResultType.Conflict, $"Slug '{productDto.Slug}' existiert bereits."); + } + + // Werte übernehmen existingProduct.Name = productDto.Name; existingProduct.Description = productDto.Description; existingProduct.SKU = productDto.SKU; @@ -231,17 +263,20 @@ namespace Webshop.Application.Services.Admin existingProduct.IsFeatured = productDto.IsFeatured; existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder; + // Immer aktualisieren existingProduct.LastModifiedDate = DateTimeOffset.UtcNow; - // Neue Version setzen existingProduct.RowVersion = Guid.NewGuid().ToByteArray(); try { + Console.WriteLine("---- RUFE SaveChangesAsync (Properties) AUF ----"); await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { - return ServiceResult.Fail(ServiceResultType.Conflict, "Das Produkt wurde bearbeitet. Bitte neu laden."); + // Sollte dank des manuellen Checks oben kaum noch passieren, + // es sei denn, millisekundengenaue Race-Condition. + return ServiceResult.Fail(ServiceResultType.Conflict, "Konflikt beim Speichern der Daten."); } return ServiceResult.Ok();