bilder
All checks were successful
Branch - test - Build and Push Backend API Docker Image / build-and-push (push) Successful in 26s

This commit is contained in:
Tizian.Breuch
2025-11-25 10:55:53 +01:00
parent 6dda8cd106
commit d5dad225b5

View File

@@ -114,7 +114,9 @@ namespace Webshop.Application.Services.Admin
public async Task<ServiceResult> UpdateAdminProductAsync(UpdateAdminProductDto productDto) public async Task<ServiceResult> 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 var existingProduct = await _context.Products
.Include(p => p.Images) .Include(p => p.Images)
.Include(p => p.Productcategories) .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."); 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)) 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(" ", "+"); Console.WriteLine("!!! KONFLIKT: Versionen stimmen nicht <20>berein !!!");
byte[] incomingRowVersion = Convert.FromBase64String(fixedRowVersion); return ServiceResult.Fail(ServiceResultType.Conflict, "Das Produkt wurde in der Zwischenzeit von jemand anderem bearbeitet. Bitte laden Sie die Seite neu.");
_context.Entry(existingProduct).Property(p => p.RowVersion).OriginalValue = incomingRowVersion;
}
catch (FormatException)
{
return ServiceResult.Fail(ServiceResultType.Failure, "RowVersion Format ist ung<6E>ltig.");
} }
} }
// 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; bool imagesChanged = false;
@@ -156,7 +167,7 @@ namespace Webshop.Application.Services.Admin
} }
} }
// Hauptbild // Hauptbild ersetzen
if (productDto.MainImageFile != null) if (productDto.MainImageFile != null)
{ {
var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage); var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage);
@@ -168,7 +179,7 @@ namespace Webshop.Application.Services.Admin
imagesChanged = true; imagesChanged = true;
} }
// Zusatzbilder // Zusatzbilder hinzuf<75>gen
if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any())
{ {
int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1; int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1;
@@ -181,42 +192,63 @@ namespace Webshop.Application.Services.Admin
imagesChanged = true; imagesChanged = true;
} }
// Kategorien // Kategorien aktualisieren
existingProduct.Productcategories.Clear();
if (productDto.CategorieIds != null) 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 <20>ndern, wenn wirklich anders (vermeidet unn<6E>tige DB-Writes)
if (!new HashSet<Guid>(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<67>ndert wurden, speichern wir HIER schon einmal zwischen. // SCHRITT A SPEICHERN (Nur wenn Bilder ge<67>ndert wurden)
// Das erlaubt der DB, Trigger auszuf<75>hren und die RowVersion zu aktualisieren.
if (imagesChanged) if (imagesChanged)
{ {
// Wir aktualisieren den Zeitstempel manuell, damit der User Feedback hat
existingProduct.LastModifiedDate = DateTimeOffset.UtcNow;
existingProduct.RowVersion = Guid.NewGuid().ToByteArray();
try try
{ {
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
// Da wir gespeichert haben, hat das Entity jetzt eine NEUE RowVersion in der DB.
// Ganz wichtig: Die RowVersion im Entity neu laden, da sie sich in der DB ge<67>ndert hat! // Aber unser EF-Kontext wei<65> das bereits, weil das Objekt getracked ist.
// Aber: Da wir das Objekt weiterverwenden wollen, m<>ssen wir sicherstellen,
// dass EF Core wei<65>, dass die Version jetzt "neu" ist.
// Der einfachste Weg: Wir holen uns das aktuelle Token aus der DB.
await _context.Entry(existingProduct).ReloadAsync();
} }
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<67>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 <20>bernehmen
existingProduct.Name = productDto.Name; existingProduct.Name = productDto.Name;
existingProduct.Description = productDto.Description; existingProduct.Description = productDto.Description;
existingProduct.SKU = productDto.SKU; existingProduct.SKU = productDto.SKU;
@@ -231,17 +263,20 @@ namespace Webshop.Application.Services.Admin
existingProduct.IsFeatured = productDto.IsFeatured; existingProduct.IsFeatured = productDto.IsFeatured;
existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder; existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder;
// Immer aktualisieren
existingProduct.LastModifiedDate = DateTimeOffset.UtcNow; existingProduct.LastModifiedDate = DateTimeOffset.UtcNow;
// Neue Version setzen
existingProduct.RowVersion = Guid.NewGuid().ToByteArray(); existingProduct.RowVersion = Guid.NewGuid().ToByteArray();
try try
{ {
Console.WriteLine("---- RUFE SaveChangesAsync (Properties) AUF ----");
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
catch (DbUpdateConcurrencyException) 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(); return ServiceResult.Ok();