Compare commits
1 Commits
dbf46fffac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03993211f6 |
@@ -5,7 +5,7 @@ name: Branch - test - Build and Push Backend API Docker Image
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop # Wird ausgelöst bei jedem Push auf den Master-Branch
|
- test # Wird ausgelöst bei jedem Push auf den Master-Branch
|
||||||
|
|
||||||
# Definition der Jobs, die in diesem Workflow ausgeführt werden
|
# Definition der Jobs, die in diesem Workflow ausgeführt werden
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -77,12 +77,15 @@ namespace Webshop.Api.Controllers.Admin
|
|||||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
|
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
|
||||||
public async Task<IActionResult> UpdateAdminProduct(Guid id, [FromForm] UpdateAdminProductDto productDto)
|
public async Task<IActionResult> UpdateAdminProduct(Guid id, [FromForm] UpdateAdminProductDto productDto)
|
||||||
{
|
{
|
||||||
// ==============================================================================
|
if (id != productDto.Id)
|
||||||
// DEINE PERFEKTE L<>SUNG: URL-ID erzwingen
|
{
|
||||||
// ==============================================================================
|
return BadRequest(new { Message = "ID in der URL und im Body stimmen nicht <20>berein." });
|
||||||
productDto.Id = id;
|
}
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
// Jetzt den Service mit dem garantiert korrekten DTO aufrufen.
|
|
||||||
var result = await _adminProductService.UpdateAdminProductAsync(productDto);
|
var result = await _adminProductService.UpdateAdminProductAsync(productDto);
|
||||||
|
|
||||||
return result.Type switch
|
return result.Type switch
|
||||||
@@ -90,7 +93,8 @@ namespace Webshop.Api.Controllers.Admin
|
|||||||
ServiceResultType.Success => NoContent(),
|
ServiceResultType.Success => NoContent(),
|
||||||
ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }),
|
ServiceResultType.NotFound => NotFound(new { Message = result.ErrorMessage }),
|
||||||
ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }),
|
ServiceResultType.Conflict => Conflict(new { Message = result.ErrorMessage }),
|
||||||
_ => BadRequest(new { Message = result.ErrorMessage ?? "Ein Fehler ist aufgetreten." })
|
ServiceResultType.InvalidInput => BadRequest(new { Message = result.ErrorMessage }),
|
||||||
|
_ => StatusCode(StatusCodes.Status500InternalServerError, new { Message = result.ErrorMessage ?? "Ein unerwarteter Fehler ist aufgetreten." })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,8 +163,6 @@ builder.Services.AddControllers()
|
|||||||
.AddJsonOptions(options =>
|
.AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||||
options.JsonSerializerOptions.DefaultIgnoreCondition =
|
|
||||||
System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
|
|
||||||
});
|
});
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
|
||||||
@@ -329,6 +327,6 @@ app.Run();
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
//new image build
|
//new git
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,5 @@ namespace Webshop.Application.DTOs.Products
|
|||||||
// << NEU >>
|
// << NEU >>
|
||||||
public bool IsFeatured { get; set; }
|
public bool IsFeatured { get; set; }
|
||||||
public int FeaturedDisplayOrder { get; set; }
|
public int FeaturedDisplayOrder { get; set; }
|
||||||
public byte[] RowVersion { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ namespace Webshop.Application.DTOs.Products
|
|||||||
public List<IFormFile>? AdditionalImageFiles { get; set; }
|
public List<IFormFile>? AdditionalImageFiles { get; set; }
|
||||||
public List<Guid>? ImagesToDelete { get; set; }
|
public List<Guid>? ImagesToDelete { get; set; }
|
||||||
|
|
||||||
public List<Guid>? CategorieIds { get; set; } = new List<Guid>();
|
public List<Guid> CategorieIds { get; set; } = new List<Guid>();
|
||||||
|
|
||||||
public decimal? Weight { get; set; }
|
public decimal? Weight { get; set; }
|
||||||
public decimal? OldPrice { get; set; }
|
public decimal? OldPrice { get; set; }
|
||||||
@@ -35,7 +35,5 @@ namespace Webshop.Application.DTOs.Products
|
|||||||
public decimal? PurchasePrice { get; set; }
|
public decimal? PurchasePrice { get; set; }
|
||||||
public bool IsFeatured { get; set; }
|
public bool IsFeatured { get; set; }
|
||||||
public int FeaturedDisplayOrder { get; set; }
|
public int FeaturedDisplayOrder { get; set; }
|
||||||
[Required]
|
|
||||||
public byte[]? RowVersion { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,105 +111,18 @@ namespace Webshop.Application.Services.Admin
|
|||||||
#region Unchanged Methods
|
#region Unchanged Methods
|
||||||
public async Task<ServiceResult> UpdateAdminProductAsync(UpdateAdminProductDto productDto)
|
public async Task<ServiceResult> UpdateAdminProductAsync(UpdateAdminProductDto productDto)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"---- UPDATE START: Produkt-ID {productDto.Id} ----");
|
var existingProduct = await _context.Products.Include(p => p.Images).Include(p => p.Productcategories).FirstOrDefaultAsync(p => p.Id == productDto.Id);
|
||||||
// SCHRITT 1: Lade NUR das Hauptprodukt, ohne Relationen.
|
if (existingProduct == null) { return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{productDto.Id}' nicht gefunden."); }
|
||||||
var existingProduct = await _context.Products.FirstOrDefaultAsync(p => p.Id == productDto.Id);
|
|
||||||
if (existingProduct == null)
|
|
||||||
{
|
|
||||||
return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt mit ID '{productDto.Id}' nicht gefunden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// SCHRITT 2: Setze SOFORT den Concurrency Token am "sauberen" Objekt.
|
|
||||||
if (productDto.RowVersion != null && productDto.RowVersion.Length > 0)
|
|
||||||
{
|
|
||||||
_context.Entry(existingProduct).Property(p => p.RowVersion).OriginalValue = productDto.RowVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SCHRITT 3: Lade jetzt die Relationen explizit nach.
|
|
||||||
await _context.Entry(existingProduct).Collection(p => p.Images).LoadAsync();
|
|
||||||
await _context.Entry(existingProduct).Collection(p => p.Productcategories).LoadAsync();
|
|
||||||
|
|
||||||
|
|
||||||
var skuExists = await _context.Products.AnyAsync(p => p.SKU == productDto.SKU && p.Id != productDto.Id);
|
var skuExists = await _context.Products.AnyAsync(p => p.SKU == productDto.SKU && p.Id != productDto.Id);
|
||||||
if (skuExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit der SKU '{productDto.SKU}' existiert bereits."); }
|
if (skuExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit der SKU '{productDto.SKU}' existiert bereits."); }
|
||||||
var slugExists = await _context.Products.AnyAsync(p => p.Slug == productDto.Slug && p.Id != productDto.Id);
|
var slugExists = await _context.Products.AnyAsync(p => p.Slug == productDto.Slug && p.Id != productDto.Id);
|
||||||
if (slugExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit dem Slug '{productDto.Slug}' existiert bereits."); }
|
if (slugExists) { return ServiceResult.Fail(ServiceResultType.Conflict, $"Ein anderes Produkt mit dem Slug '{productDto.Slug}' existiert bereits."); }
|
||||||
|
if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any()) { var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList(); _context.ProductImages.RemoveRange(imagesToRemove); }
|
||||||
// --- BILDER-LOGGING ---
|
if (productDto.MainImageFile != null) { var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage); if (existingMainImage != null) _context.ProductImages.Remove(existingMainImage); await using var stream = productDto.MainImageFile.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = true, DisplayOrder = 1 }); }
|
||||||
if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any())
|
if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) { int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1; foreach (var file in productDto.AdditionalImageFiles) { await using var stream = file.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, file.FileName, file.ContentType); existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = false, DisplayOrder = displayOrder++ }); } }
|
||||||
{
|
existingProduct.Name = productDto.Name; existingProduct.Description = productDto.Description; existingProduct.SKU = productDto.SKU; existingProduct.Price = productDto.Price; existingProduct.IsActive = productDto.IsActive; existingProduct.StockQuantity = productDto.StockQuantity; existingProduct.Slug = productDto.Slug; existingProduct.Weight = productDto.Weight; existingProduct.OldPrice = productDto.OldPrice; existingProduct.SupplierId = productDto.SupplierId; existingProduct.PurchasePrice = productDto.PurchasePrice; existingProduct.LastModifiedDate = DateTimeOffset.UtcNow; existingProduct.IsFeatured = productDto.IsFeatured; existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder;
|
||||||
Console.WriteLine($"---- L<>SCHE {productDto.ImagesToDelete.Count} BILDER ----");
|
existingProduct.Productcategories.Clear(); if (productDto.CategorieIds != null) { foreach (var categorieId in productDto.CategorieIds) { existingProduct.Productcategories.Add(new Productcategorie { categorieId = categorieId }); } }
|
||||||
var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList();
|
await _productRepository.UpdateProductAsync(existingProduct); return ServiceResult.Ok();
|
||||||
_context.ProductImages.RemoveRange(imagesToRemove);
|
|
||||||
}
|
|
||||||
if (productDto.MainImageFile != null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("---- ERSETZE HAUPTBILD ----");
|
|
||||||
var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage);
|
|
||||||
if (existingMainImage != null) _context.ProductImages.Remove(existingMainImage);
|
|
||||||
|
|
||||||
await using var stream = productDto.MainImageFile.OpenReadStream();
|
|
||||||
var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType);
|
|
||||||
Console.WriteLine($"---- NEUE HAUPTBILD-URL: {url} ----");
|
|
||||||
existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = true, DisplayOrder = 1 });
|
|
||||||
}
|
|
||||||
if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any())
|
|
||||||
{
|
|
||||||
Console.WriteLine($"---- F<>GE {productDto.AdditionalImageFiles.Count} NEUE BILDER HINZU ----");
|
|
||||||
int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1;
|
|
||||||
foreach (var file in productDto.AdditionalImageFiles)
|
|
||||||
{
|
|
||||||
await using var stream = file.OpenReadStream();
|
|
||||||
var url = await _fileStorageService.SaveFileAsync(stream, file.FileName, file.ContentType);
|
|
||||||
Console.WriteLine($"---- NEUE BILD-URL: {url} ----");
|
|
||||||
existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = false, DisplayOrder = displayOrder++ });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// --- EIGENSCHAFTEN-UPDATE ---
|
|
||||||
Console.WriteLine("---- AKTUALISIERE PRODUKT-EIGENSCHAFTEN ----");
|
|
||||||
existingProduct.Name = productDto.Name;
|
|
||||||
existingProduct.Description = productDto.Description;
|
|
||||||
existingProduct.SKU = productDto.SKU;
|
|
||||||
existingProduct.Price = productDto.Price;
|
|
||||||
existingProduct.IsActive = productDto.IsActive;
|
|
||||||
existingProduct.StockQuantity = productDto.StockQuantity;
|
|
||||||
existingProduct.Slug = productDto.Slug;
|
|
||||||
existingProduct.Weight = productDto.Weight;
|
|
||||||
existingProduct.OldPrice = productDto.OldPrice;
|
|
||||||
existingProduct.SupplierId = productDto.SupplierId;
|
|
||||||
existingProduct.PurchasePrice = productDto.PurchasePrice;
|
|
||||||
existingProduct.LastModifiedDate = DateTimeOffset.UtcNow;
|
|
||||||
existingProduct.IsFeatured = productDto.IsFeatured;
|
|
||||||
existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder;
|
|
||||||
|
|
||||||
// --- KATEGORIEN-UPDATE ---
|
|
||||||
Console.WriteLine("---- AKTUALISIERE KATEGORIEN ----");
|
|
||||||
existingProduct.Productcategories.Clear();
|
|
||||||
if (productDto.CategorieIds != null)
|
|
||||||
{
|
|
||||||
foreach (var categorieId in productDto.CategorieIds)
|
|
||||||
{
|
|
||||||
existingProduct.Productcategories.Add(new Productcategorie { categorieId = categorieId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SCHRITT 5: Speichern (jetzt mit einem sauberen Change Tracker Zustand)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine("---- RUFE SaveChangesAsync AUF ----");
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
Console.WriteLine("---- UPDATE BEENDET ----");
|
|
||||||
}
|
|
||||||
catch (DbUpdateConcurrencyException)
|
|
||||||
{
|
|
||||||
// Dieser Fehler tritt jetzt nur noch auf, wenn jemand anderes WIRKLICH
|
|
||||||
// die Daten in der Zwischenzeit ge<67>ndert hat.
|
|
||||||
return ServiceResult.Fail(ServiceResultType.Conflict, "Das Produkt wurde in der Zwischenzeit von jemand anderem bearbeitet. Bitte laden Sie die Seite neu.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServiceResult.Ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ServiceResult> DeleteAdminProductAsync(Guid id)
|
public async Task<ServiceResult> DeleteAdminProductAsync(Guid id)
|
||||||
@@ -221,7 +134,7 @@ namespace Webshop.Application.Services.Admin
|
|||||||
|
|
||||||
private AdminProductDto MapToAdminDto(Product product)
|
private AdminProductDto MapToAdminDto(Product product)
|
||||||
{
|
{
|
||||||
return new AdminProductDto { Id = product.Id, Name = product.Name, Description = product.Description, SKU = product.SKU, Price = product.Price, OldPrice = product.OldPrice, IsActive = product.IsActive, IsInStock = product.IsInStock, StockQuantity = product.StockQuantity, Weight = product.Weight, Slug = product.Slug, CreatedDate = product.CreatedDate, LastModifiedDate = product.LastModifiedDate, SupplierId = product.SupplierId, PurchasePrice = product.PurchasePrice, IsFeatured = product.IsFeatured, FeaturedDisplayOrder = product.FeaturedDisplayOrder, categorieIds = product.Productcategories.Select(pc => pc.categorieId).ToList(), Images = product.Images.OrderBy(i => i.DisplayOrder).Select(img => new ProductImageDto { Id = img.Id, Url = img.Url, IsMainImage = img.IsMainImage, DisplayOrder = img.DisplayOrder }).ToList(), RowVersion = product.RowVersion };
|
return new AdminProductDto { Id = product.Id, Name = product.Name, Description = product.Description, SKU = product.SKU, Price = product.Price, OldPrice = product.OldPrice, IsActive = product.IsActive, IsInStock = product.IsInStock, StockQuantity = product.StockQuantity, Weight = product.Weight, Slug = product.Slug, CreatedDate = product.CreatedDate, LastModifiedDate = product.LastModifiedDate, SupplierId = product.SupplierId, PurchasePrice = product.PurchasePrice, IsFeatured = product.IsFeatured, FeaturedDisplayOrder = product.FeaturedDisplayOrder, categorieIds = product.Productcategories.Select(pc => pc.categorieId).ToList(), Images = product.Images.OrderBy(i => i.DisplayOrder).Select(img => new ProductImageDto { Id = img.Id, Url = img.Url, IsMainImage = img.IsMainImage, DisplayOrder = img.DisplayOrder }).ToList() };
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,5 @@ namespace Webshop.Domain.Entities
|
|||||||
public virtual ICollection<ProductDiscount> ProductDiscounts { get; set; } = new List<ProductDiscount>();
|
public virtual ICollection<ProductDiscount> ProductDiscounts { get; set; } = new List<ProductDiscount>();
|
||||||
public virtual ICollection<Productcategorie> Productcategories { get; set; } = new List<Productcategorie>();
|
public virtual ICollection<Productcategorie> Productcategories { get; set; } = new List<Productcategorie>();
|
||||||
public virtual ICollection<ProductImage> Images { get; set; } = new List<ProductImage>();
|
public virtual ICollection<ProductImage> Images { get; set; } = new List<ProductImage>();
|
||||||
[Timestamp] // Diese Annotation ist entscheidend!
|
|
||||||
public byte[] RowVersion { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,6 @@ namespace Webshop.Domain.Interfaces
|
|||||||
Task<Product?> GetBySlugAsync(string slug);
|
Task<Product?> GetBySlugAsync(string slug);
|
||||||
Task AddProductAsync(Product product);
|
Task AddProductAsync(Product product);
|
||||||
Task UpdateProductAsync(Product product);
|
Task UpdateProductAsync(Product product);
|
||||||
Task SaveChangesAsync();
|
|
||||||
Task DeleteProductAsync(Guid id);
|
Task DeleteProductAsync(Guid id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Webshop.Infrastructure.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class row : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<byte[]>(
|
|
||||||
name: "RowVersion",
|
|
||||||
table: "Products",
|
|
||||||
type: "bytea",
|
|
||||||
rowVersion: true,
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new byte[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "RowVersion",
|
|
||||||
table: "Products");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -613,12 +613,6 @@ namespace Webshop.Infrastructure.Migrations
|
|||||||
.HasPrecision(18, 2)
|
.HasPrecision(18, 2)
|
||||||
.HasColumnType("numeric(18,2)");
|
.HasColumnType("numeric(18,2)");
|
||||||
|
|
||||||
b.Property<byte[]>("RowVersion")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("bytea");
|
|
||||||
|
|
||||||
b.Property<string>("SKU")
|
b.Property<string>("SKU")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
|
|||||||
@@ -55,9 +55,5 @@ namespace Webshop.Infrastructure.Repositories
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async Task SaveChangesAsync()
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user