From b3429e2a0d68ede56050be02294c3d097eda08dd Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 18 Feb 2026 08:27:37 +0800 Subject: [PATCH] feat(store): auto-generate store code on create and make update code optional --- .../App/Stores/Commands/CreateStoreCommand.cs | 5 --- .../App/Stores/Commands/UpdateStoreCommand.cs | 4 +- .../Handlers/CreateStoreCommandHandler.cs | 35 ++++++++++++---- .../Handlers/UpdateStoreCommandHandler.cs | 41 +++++++++++-------- .../Validators/CreateStoreCommandValidator.cs | 1 - .../Validators/UpdateStoreCommandValidator.cs | 5 ++- 6 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs index 76ba506..8b9a728 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreCommand.cs @@ -14,11 +14,6 @@ public sealed record CreateStoreCommand : IRequest /// public string Name { get; init; } = string.Empty; - /// - /// 门店编码。 - /// - public string Code { get; init; } = string.Empty; - /// /// 联系电话。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs index 8c0994f..849fafd 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreCommand.cs @@ -23,9 +23,9 @@ public sealed record UpdateStoreCommand : IRequest public string Name { get; init; } = string.Empty; /// - /// 门店编码。 + /// 门店编码(可选,未传时保持原值)。 /// - public string Code { get; init; } = string.Empty; + public string? Code { get; init; } /// /// 联系电话。 diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs index beabb30..6052feb 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs @@ -1,4 +1,5 @@ using MediatR; +using System.Security.Cryptography; using TakeoutSaaS.Application.App.Stores.Commands; using TakeoutSaaS.Application.App.Stores.Enums; using TakeoutSaaS.Application.App.Stores.Services; @@ -24,7 +25,7 @@ public sealed class CreateStoreCommandHandler( // 1. 解析上下文 var context = storeContextService.GetRequiredContext(); - // 2. 校验编码唯一性 + // 2. 生成唯一门店编码 var existingStores = await storeRepository.SearchAsync( context.TenantId, context.MerchantId, @@ -35,12 +36,7 @@ public sealed class CreateStoreCommandHandler( keyword: null, includeDeleted: false, cancellationToken: cancellationToken); - var normalizedCode = request.Code.Trim(); - var hasDuplicateCode = existingStores.Any(store => string.Equals(store.Code, normalizedCode, StringComparison.OrdinalIgnoreCase)); - if (hasDuplicateCode) - { - throw new BusinessException(ErrorCodes.Conflict, "门店编码已存在"); - } + var generatedCode = GenerateUniqueStoreCode(existingStores); // 3. 组装门店实体 var serviceTypes = request.ServiceTypes?.Count > 0 @@ -50,7 +46,7 @@ public sealed class CreateStoreCommandHandler( { TenantId = context.TenantId, MerchantId = context.MerchantId, - Code = normalizedCode, + Code = generatedCode, Name = request.Name.Trim(), Phone = request.ContactPhone.Trim(), ManagerName = request.ManagerName.Trim(), @@ -68,4 +64,27 @@ public sealed class CreateStoreCommandHandler( await storeRepository.AddStoreAsync(store, cancellationToken); await storeRepository.SaveChangesAsync(cancellationToken); } + + /// + /// 生成当前租户商户内唯一的门店编码。 + /// + private static string GenerateUniqueStoreCode(IReadOnlyList existingStores) + { + var existingCodeSet = existingStores + .Select(store => store.Code.Trim()) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + const int maxRetryCount = 20; + for (var attempt = 0; attempt < maxRetryCount; attempt++) + { + var candidate = $"ST{DateTime.UtcNow:yyMMddHHmmss}{RandomNumberGenerator.GetInt32(100, 1000)}"; + if (existingCodeSet.Add(candidate)) + { + return candidate; + } + } + + throw new BusinessException(ErrorCodes.Conflict, "门店编码生成失败,请稍后重试"); + } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs index c5e8efc..e375762 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs @@ -30,28 +30,33 @@ public sealed class UpdateStoreCommandHandler( throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他商户门店"); } - // 3. 校验编码唯一性 - var existingStores = await storeRepository.SearchAsync( - context.TenantId, - context.MerchantId, - status: null, - auditStatus: null, - businessStatus: null, - ownershipType: null, - keyword: null, - includeDeleted: false, - cancellationToken: cancellationToken); - var normalizedCode = request.Code.Trim(); - var hasDuplicateCode = existingStores.Any(store => - store.Id != existing.Id && - string.Equals(store.Code, normalizedCode, StringComparison.OrdinalIgnoreCase)); - if (hasDuplicateCode) + // 3. 若传入编码且发生变化,则校验唯一性并更新编码。 + var normalizedCode = request.Code?.Trim(); + if (!string.IsNullOrWhiteSpace(normalizedCode) && + !string.Equals(existing.Code, normalizedCode, StringComparison.OrdinalIgnoreCase)) { - throw new BusinessException(ErrorCodes.Conflict, "门店编码已存在"); + var existingStores = await storeRepository.SearchAsync( + context.TenantId, + context.MerchantId, + status: null, + auditStatus: null, + businessStatus: null, + ownershipType: null, + keyword: null, + includeDeleted: false, + cancellationToken: cancellationToken); + var hasDuplicateCode = existingStores.Any(store => + store.Id != existing.Id && + string.Equals(store.Code, normalizedCode, StringComparison.OrdinalIgnoreCase)); + if (hasDuplicateCode) + { + throw new BusinessException(ErrorCodes.Conflict, "门店编码已存在"); + } + + existing.Code = normalizedCode; } // 4. 更新门店字段 - existing.Code = normalizedCode; existing.Name = request.Name.Trim(); existing.Phone = request.ContactPhone.Trim(); existing.ManagerName = request.ManagerName.Trim(); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreCommandValidator.cs index dce91ff..f1bd4fa 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreCommandValidator.cs @@ -15,7 +15,6 @@ public sealed class CreateStoreCommandValidator : AbstractValidator command.Name).NotEmpty().MaximumLength(128); - RuleFor(command => command.Code).NotEmpty().MaximumLength(32); RuleFor(command => command.ContactPhone).NotEmpty().MaximumLength(32); RuleFor(command => command.ManagerName).NotEmpty().MaximumLength(64); RuleFor(command => command.Address).NotEmpty().MaximumLength(256); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreCommandValidator.cs index 96a0be9..1427008 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreCommandValidator.cs @@ -16,7 +16,10 @@ public sealed class UpdateStoreCommandValidator : AbstractValidator command.Id).GreaterThan(0); RuleFor(command => command.Name).NotEmpty().MaximumLength(128); - RuleFor(command => command.Code).NotEmpty().MaximumLength(32); + RuleFor(command => command.Code) + .NotEmpty() + .MaximumLength(32) + .When(command => command.Code is not null); RuleFor(command => command.ContactPhone).NotEmpty().MaximumLength(32); RuleFor(command => command.ManagerName).NotEmpty().MaximumLength(64); RuleFor(command => command.Address).NotEmpty().MaximumLength(256);