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);