From b02950e46575c81fd45fe561ae65501246ac0fb3 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Fri, 6 Feb 2026 13:56:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=95=86=E6=88=B7?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E6=9F=A5=E8=AF=A2=20RowVersion=20=E5=88=97?= =?UTF-8?q?=E4=B8=8D=E5=AD=98=E5=9C=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Merchants/Commands/UpdateMerchantCommand.cs | 4 ++-- .../Merchants/Handlers/UpdateMerchantCommandHandler.cs | 8 +------- .../Validators/UpdateMerchantCommandValidator.cs | 1 - .../Merchants/Repositories/IMerchantRepository.cs | 9 +++++++++ .../App/Persistence/TakeoutAppDbContext.cs | 10 ++++++---- .../App/Repositories/EfMerchantRepository.cs | 8 ++++++++ 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs index 034a6f8..a140562 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Commands/UpdateMerchantCommand.cs @@ -44,7 +44,7 @@ public sealed record UpdateMerchantCommand : IRequest public string? ContactEmail { get; init; } /// - /// 并发控制版本。 + /// 并发控制版本(兼容字段,当前由数据库 xmin 托管)。 /// - public byte[] RowVersion { get; init; } = Array.Empty(); + public byte[]? RowVersion { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs index 1a0eb65..3c76c78 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Handlers/UpdateMerchantCommandHandler.cs @@ -29,16 +29,11 @@ public sealed class UpdateMerchantCommandHandler( /// public async Task Handle(UpdateMerchantCommand request, CancellationToken cancellationToken) { - if (request.RowVersion == null || request.RowVersion.Length == 0) - { - throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空"); - } - // 1. 获取操作者权限 var currentTenantId = tenantProvider.GetCurrentTenantId(); // 2. 读取商户信息 - var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken); + var merchant = await merchantRepository.GetForUpdateAsync(request.MerchantId, currentTenantId, cancellationToken); if (merchant == null) { @@ -73,7 +68,6 @@ public sealed class UpdateMerchantCommandHandler( merchant.Address = registeredAddress; merchant.ContactPhone = contactPhone; merchant.ContactEmail = contactEmail; - merchant.RowVersion = request.RowVersion; var requiresReview = merchant.Status == MerchantStatus.Approved && criticalChanged; if (requiresReview) diff --git a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/UpdateMerchantCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/UpdateMerchantCommandValidator.cs index 248f60c..db43914 100644 --- a/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/UpdateMerchantCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Merchants/Validators/UpdateMerchantCommandValidator.cs @@ -21,6 +21,5 @@ public sealed class UpdateMerchantCommandValidator : AbstractValidator x.ContactPhone).NotEmpty().MaximumLength(32); RuleFor(x => x.ContactEmail).EmailAddress().MaximumLength(128) .When(x => !string.IsNullOrWhiteSpace(x.ContactEmail)); - RuleFor(x => x.RowVersion).NotEmpty(); } } diff --git a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs index c1b972c..00b769d 100644 --- a/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Merchants/Repositories/IMerchantRepository.cs @@ -18,6 +18,15 @@ public interface IMerchantRepository /// 商户实体或 null。 Task FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); + /// + /// 依据标识获取商户(用于更新,返回可跟踪实体)。 + /// + /// 商户 ID。 + /// 租户 ID。 + /// 取消标记。 + /// 商户实体或 null。 + Task GetForUpdateAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default); + /// /// 按状态筛选商户列表。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index c0f05da..d4c44b6 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -536,10 +536,12 @@ public sealed class TakeoutAppDbContext( builder.Property(x => x.IsFrozen).HasDefaultValue(false); builder.Property(x => x.FrozenReason).HasMaxLength(500); builder.Property(x => x.ClaimedByName).HasMaxLength(100); - builder.Property(x => x.RowVersion) - .IsRowVersion() - .IsConcurrencyToken() - .HasColumnType("bytea"); + builder.Ignore(x => x.RowVersion); + builder.Property("xmin") + .HasColumnName("xmin") + .HasColumnType("xid") + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); builder.HasIndex(x => x.TenantId); builder.HasIndex(x => new { x.TenantId, x.Status }); builder.HasIndex(x => x.ClaimedBy); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs index 17e9fc4..03e14da 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMerchantRepository.cs @@ -25,6 +25,14 @@ public sealed class EfMerchantRepository(TakeoutAppDbContext context, TakeoutLog .FirstOrDefaultAsync(cancellationToken); } + /// + public Task GetForUpdateAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) + { + return context.Merchants + .Where(x => x.TenantId == tenantId && x.Id == merchantId) + .FirstOrDefaultAsync(cancellationToken); + } + /// public async Task> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default) {