fix(pickup): prevent null rowversion on settings and slots save
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 43s

This commit is contained in:
2026-02-19 17:57:52 +08:00
parent 53f7c54c82
commit 7ecf069efd
3 changed files with 32 additions and 8 deletions

View File

@@ -1,4 +1,5 @@
using System.Text.Json; using System.Text.Json;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -92,6 +93,7 @@ public sealed class StorePickupController(
setting.AllowDaysAhead = Math.Clamp(request.BasicSettings.BookingDays, 1, 30); setting.AllowDaysAhead = Math.Clamp(request.BasicSettings.BookingDays, 1, 30);
setting.MaxQuantityPerOrder = request.BasicSettings.MaxItemsPerOrder; setting.MaxQuantityPerOrder = request.BasicSettings.MaxItemsPerOrder;
setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode); setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode);
setting.RowVersion = CreateRowVersion();
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
return ApiResponse<object>.Ok(null); return ApiResponse<object>.Ok(null);
@@ -110,6 +112,7 @@ public sealed class StorePickupController(
var setting = await EnsurePickupSettingAsync(tenantId, parsedStoreId, cancellationToken); var setting = await EnsurePickupSettingAsync(tenantId, parsedStoreId, cancellationToken);
setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode); setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode);
setting.RowVersion = CreateRowVersion();
var existingSlots = await dbContext.StorePickupSlots var existingSlots = await dbContext.StorePickupSlots
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId) .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
@@ -137,7 +140,8 @@ public sealed class StorePickupController(
Capacity = capacity, Capacity = capacity,
ReservedCount = Math.Clamp(slot.ReservedCount, 0, capacity), ReservedCount = Math.Clamp(slot.ReservedCount, 0, capacity),
Weekdays = StoreApiHelpers.SerializeWeekdays(slot.DayOfWeeks), Weekdays = StoreApiHelpers.SerializeWeekdays(slot.DayOfWeeks),
IsEnabled = slot.Enabled IsEnabled = slot.Enabled,
RowVersion = CreateRowVersion()
}); });
} }
@@ -163,6 +167,7 @@ public sealed class StorePickupController(
var setting = await EnsurePickupSettingAsync(tenantId, parsedStoreId, cancellationToken); var setting = await EnsurePickupSettingAsync(tenantId, parsedStoreId, cancellationToken);
setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode); setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode);
setting.RowVersion = CreateRowVersion();
var normalizedRule = NormalizeFineRule(request.FineRule); var normalizedRule = NormalizeFineRule(request.FineRule);
setting.FineRuleJson = JsonSerializer.Serialize(normalizedRule, StoreApiHelpers.JsonOptions); setting.FineRuleJson = JsonSerializer.Serialize(normalizedRule, StoreApiHelpers.JsonOptions);
@@ -218,7 +223,8 @@ public sealed class StorePickupController(
{ {
targetSetting = new StorePickupSetting targetSetting = new StorePickupSetting
{ {
StoreId = targetStoreId StoreId = targetStoreId,
RowVersion = CreateRowVersion()
}; };
await dbContext.StorePickupSettings.AddAsync(targetSetting, cancellationToken); await dbContext.StorePickupSettings.AddAsync(targetSetting, cancellationToken);
} }
@@ -229,6 +235,7 @@ public sealed class StorePickupController(
targetSetting.MaxQuantityPerOrder = sourceSetting?.MaxQuantityPerOrder ?? 20; targetSetting.MaxQuantityPerOrder = sourceSetting?.MaxQuantityPerOrder ?? 20;
targetSetting.Mode = sourceSetting?.Mode ?? StorePickupMode.Big; targetSetting.Mode = sourceSetting?.Mode ?? StorePickupMode.Big;
targetSetting.FineRuleJson = sourceSetting?.FineRuleJson; targetSetting.FineRuleJson = sourceSetting?.FineRuleJson;
targetSetting.RowVersion = CreateRowVersion();
} }
var targetSlots = await dbContext.StorePickupSlots var targetSlots = await dbContext.StorePickupSlots
@@ -247,7 +254,8 @@ public sealed class StorePickupController(
Capacity = slot.Capacity, Capacity = slot.Capacity,
ReservedCount = slot.ReservedCount, ReservedCount = slot.ReservedCount,
Weekdays = slot.Weekdays, Weekdays = slot.Weekdays,
IsEnabled = slot.IsEnabled IsEnabled = slot.IsEnabled,
RowVersion = CreateRowVersion()
})) }))
.ToList(); .ToList();
@@ -275,12 +283,18 @@ public sealed class StorePickupController(
setting = new StorePickupSetting setting = new StorePickupSetting
{ {
StoreId = storeId StoreId = storeId,
RowVersion = CreateRowVersion()
}; };
await dbContext.StorePickupSettings.AddAsync(setting, cancellationToken); await dbContext.StorePickupSettings.AddAsync(setting, cancellationToken);
return setting; return setting;
} }
private static byte[] CreateRowVersion()
{
return RandomNumberGenerator.GetBytes(16);
}
private static PickupFineRuleDto ParseFineRule(string? raw) private static PickupFineRuleDto ParseFineRule(string? raw)
{ {
if (!string.IsNullOrWhiteSpace(raw)) if (!string.IsNullOrWhiteSpace(raw))

View File

@@ -1093,7 +1093,9 @@ public sealed class TakeoutAppDbContext(
builder.Property(x => x.Mode).HasConversion<int>(); builder.Property(x => x.Mode).HasConversion<int>();
builder.Property(x => x.FineRuleJson).HasColumnType("text"); builder.Property(x => x.FineRuleJson).HasColumnType("text");
builder.Property(x => x.RowVersion) builder.Property(x => x.RowVersion)
.IsConcurrencyToken(); .IsRequired()
.IsConcurrencyToken()
.ValueGeneratedNever();
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique(); builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
} }
@@ -1106,7 +1108,9 @@ public sealed class TakeoutAppDbContext(
builder.Property(x => x.Weekdays).HasMaxLength(32).IsRequired(); builder.Property(x => x.Weekdays).HasMaxLength(32).IsRequired();
builder.Property(x => x.CutoffMinutes).HasDefaultValue(30); builder.Property(x => x.CutoffMinutes).HasDefaultValue(30);
builder.Property(x => x.RowVersion) builder.Property(x => x.RowVersion)
.IsConcurrencyToken(); .IsRequired()
.IsConcurrencyToken()
.ValueGeneratedNever();
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name }); builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name });
} }

View File

@@ -283,7 +283,10 @@ public sealed class TakeoutTenantAppDbContext(
builder.Property(x => x.AllowDaysAhead).HasDefaultValue(3); builder.Property(x => x.AllowDaysAhead).HasDefaultValue(3);
builder.Property(x => x.DefaultCutoffMinutes).HasDefaultValue(30); builder.Property(x => x.DefaultCutoffMinutes).HasDefaultValue(30);
builder.Property(x => x.MaxQuantityPerOrder); builder.Property(x => x.MaxQuantityPerOrder);
builder.Property(x => x.RowVersion).IsRowVersion(); builder.Property(x => x.RowVersion)
.IsRequired()
.IsConcurrencyToken()
.ValueGeneratedNever();
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique(); builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
} }
@@ -300,7 +303,10 @@ public sealed class TakeoutTenantAppDbContext(
builder.Property(x => x.ReservedCount).HasDefaultValue(0); builder.Property(x => x.ReservedCount).HasDefaultValue(0);
builder.Property(x => x.Weekdays).HasMaxLength(32).HasDefaultValue("1,2,3,4,5,6,7"); builder.Property(x => x.Weekdays).HasMaxLength(32).HasDefaultValue("1,2,3,4,5,6,7");
builder.Property(x => x.IsEnabled).HasDefaultValue(true); builder.Property(x => x.IsEnabled).HasDefaultValue(true);
builder.Property(x => x.RowVersion).IsRowVersion(); builder.Property(x => x.RowVersion)
.IsRequired()
.IsConcurrencyToken()
.ValueGeneratedNever();
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.StartTime, x.EndTime }).IsUnique(); builder.HasIndex(x => new { x.TenantId, x.StoreId, x.StartTime, x.EndTime }).IsUnique();
} }
} }