fix(products/update): merge barcodes/prices по ключу + 409 на concurrency
Юзер ловил 500 «DbUpdateConcurrencyException: 0 rows affected» при PUT /api/catalog/products. RemoveRange(всех детей) + Add новых на каждом сохранении генерирует массовый DELETE/INSERT, при котором EF ожидал N rows affected, а реальный DELETE возвращал меньше — и весь батч падал с 500. Чиню по-человечески: - Merge by stable key: barcodes по Code, prices по PriceTypeId. Совпавшие — обновляем поля, лишние удаляем, новые добавляем. Минимум записей в SaveChanges, минимум поводов для 0-affected. - Catch DbUpdateConcurrencyException → 409 «Товар изменён в другом окне или сессии. Перезагрузите страницу и попробуйте снова.» вместо непрозрачного 500. - Удалена мёртвая ветка `if (input.Vat is null) e.Vat = existingVat`: Apply уже не присваивает Vat при null, ничего восстанавливать не нужно. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ad05f9fe30
commit
fd2da58ad4
|
|
@ -213,21 +213,50 @@ public async Task<IActionResult> Update(Guid id, [FromBody] ProductInput input,
|
|||
if (e is null) return NotFound();
|
||||
|
||||
var allowFractional = await AllowFractionalAsync(ct);
|
||||
var existingVat = e.Vat;
|
||||
Apply(e, input);
|
||||
e.PurchasePrice = RoundIfNeeded(e.PurchasePrice, allowFractional);
|
||||
// Если UI не передал Vat (скрыт) — сохраняем что было, не обнуляем.
|
||||
if (input.Vat is null) e.Vat = existingVat;
|
||||
|
||||
_db.ProductBarcodes.RemoveRange(e.Barcodes);
|
||||
e.Barcodes.Clear();
|
||||
foreach (var b in input.Barcodes ?? [])
|
||||
// Merge barcodes по Code: одинаковые — обновляем поля, лишние удаляем,
|
||||
// новые добавляем. Это позволяет избежать массового DELETE+INSERT, на
|
||||
// котором EF может выдать DbUpdateConcurrencyException, если какой-то
|
||||
// child был удалён параллельно из БД.
|
||||
var inputBarcodes = (input.Barcodes ?? []).ToList();
|
||||
var byCode = e.Barcodes.ToDictionary(b => b.Code, b => b);
|
||||
var inputCodes = inputBarcodes.Select(b => b.Code).ToHashSet();
|
||||
foreach (var existing in e.Barcodes.ToList())
|
||||
if (!inputCodes.Contains(existing.Code)) _db.ProductBarcodes.Remove(existing);
|
||||
foreach (var b in inputBarcodes)
|
||||
{
|
||||
if (byCode.TryGetValue(b.Code, out var ex))
|
||||
{
|
||||
ex.Type = b.Type;
|
||||
ex.IsPrimary = b.IsPrimary;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Barcodes.Add(new ProductBarcode { Code = b.Code, Type = b.Type, IsPrimary = b.IsPrimary });
|
||||
}
|
||||
}
|
||||
|
||||
_db.ProductPrices.RemoveRange(e.Prices);
|
||||
e.Prices.Clear();
|
||||
foreach (var pr in input.Prices ?? [])
|
||||
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = RoundIfNeeded(pr.Amount, allowFractional), CurrencyId = pr.CurrencyId });
|
||||
// Merge prices по PriceTypeId.
|
||||
var inputPrices = (input.Prices ?? []).ToList();
|
||||
var byPriceType = e.Prices.ToDictionary(p => p.PriceTypeId, p => p);
|
||||
var inputPriceTypes = inputPrices.Select(p => p.PriceTypeId).ToHashSet();
|
||||
foreach (var existing in e.Prices.ToList())
|
||||
if (!inputPriceTypes.Contains(existing.PriceTypeId)) _db.ProductPrices.Remove(existing);
|
||||
foreach (var pr in inputPrices)
|
||||
{
|
||||
var amount = RoundIfNeeded(pr.Amount, allowFractional);
|
||||
if (byPriceType.TryGetValue(pr.PriceTypeId, out var ex))
|
||||
{
|
||||
ex.Amount = amount;
|
||||
ex.CurrencyId = pr.CurrencyId;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Prices.Add(new ProductPrice { PriceTypeId = pr.PriceTypeId, Amount = amount, CurrencyId = pr.CurrencyId });
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -237,6 +266,10 @@ public async Task<IActionResult> Update(Guid id, [FromBody] ProductInput input,
|
|||
{
|
||||
return BadRequest(new { error = $"Артикул «{e.Article}» уже занят в этой организации." });
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return Conflict(new { error = "Товар изменён в другом окне или сессии. Перезагрузите страницу и попробуйте снова." });
|
||||
}
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue