feat(store): auto-generate store code on create and make update code optional
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 44s
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 44s
This commit is contained in:
@@ -14,11 +14,6 @@ public sealed record CreateStoreCommand : IRequest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 门店编码。
|
|
||||||
/// </summary>
|
|
||||||
public string Code { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 联系电话。
|
/// 联系电话。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ public sealed record UpdateStoreCommand : IRequest
|
|||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 门店编码。
|
/// 门店编码(可选,未传时保持原值)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Code { get; init; } = string.Empty;
|
public string? Code { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 联系电话。
|
/// 联系电话。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using TakeoutSaaS.Application.App.Stores.Commands;
|
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
using TakeoutSaaS.Application.App.Stores.Enums;
|
using TakeoutSaaS.Application.App.Stores.Enums;
|
||||||
using TakeoutSaaS.Application.App.Stores.Services;
|
using TakeoutSaaS.Application.App.Stores.Services;
|
||||||
@@ -24,7 +25,7 @@ public sealed class CreateStoreCommandHandler(
|
|||||||
// 1. 解析上下文
|
// 1. 解析上下文
|
||||||
var context = storeContextService.GetRequiredContext();
|
var context = storeContextService.GetRequiredContext();
|
||||||
|
|
||||||
// 2. 校验编码唯一性
|
// 2. 生成唯一门店编码
|
||||||
var existingStores = await storeRepository.SearchAsync(
|
var existingStores = await storeRepository.SearchAsync(
|
||||||
context.TenantId,
|
context.TenantId,
|
||||||
context.MerchantId,
|
context.MerchantId,
|
||||||
@@ -35,12 +36,7 @@ public sealed class CreateStoreCommandHandler(
|
|||||||
keyword: null,
|
keyword: null,
|
||||||
includeDeleted: false,
|
includeDeleted: false,
|
||||||
cancellationToken: cancellationToken);
|
cancellationToken: cancellationToken);
|
||||||
var normalizedCode = request.Code.Trim();
|
var generatedCode = GenerateUniqueStoreCode(existingStores);
|
||||||
var hasDuplicateCode = existingStores.Any(store => string.Equals(store.Code, normalizedCode, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (hasDuplicateCode)
|
|
||||||
{
|
|
||||||
throw new BusinessException(ErrorCodes.Conflict, "门店编码已存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 组装门店实体
|
// 3. 组装门店实体
|
||||||
var serviceTypes = request.ServiceTypes?.Count > 0
|
var serviceTypes = request.ServiceTypes?.Count > 0
|
||||||
@@ -50,7 +46,7 @@ public sealed class CreateStoreCommandHandler(
|
|||||||
{
|
{
|
||||||
TenantId = context.TenantId,
|
TenantId = context.TenantId,
|
||||||
MerchantId = context.MerchantId,
|
MerchantId = context.MerchantId,
|
||||||
Code = normalizedCode,
|
Code = generatedCode,
|
||||||
Name = request.Name.Trim(),
|
Name = request.Name.Trim(),
|
||||||
Phone = request.ContactPhone.Trim(),
|
Phone = request.ContactPhone.Trim(),
|
||||||
ManagerName = request.ManagerName.Trim(),
|
ManagerName = request.ManagerName.Trim(),
|
||||||
@@ -68,4 +64,27 @@ public sealed class CreateStoreCommandHandler(
|
|||||||
await storeRepository.AddStoreAsync(store, cancellationToken);
|
await storeRepository.AddStoreAsync(store, cancellationToken);
|
||||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成当前租户商户内唯一的门店编码。
|
||||||
|
/// </summary>
|
||||||
|
private static string GenerateUniqueStoreCode(IReadOnlyList<Store> 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, "门店编码生成失败,请稍后重试");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,28 +30,33 @@ public sealed class UpdateStoreCommandHandler(
|
|||||||
throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他商户门店");
|
throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他商户门店");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 校验编码唯一性
|
// 3. 若传入编码且发生变化,则校验唯一性并更新编码。
|
||||||
var existingStores = await storeRepository.SearchAsync(
|
var normalizedCode = request.Code?.Trim();
|
||||||
context.TenantId,
|
if (!string.IsNullOrWhiteSpace(normalizedCode) &&
|
||||||
context.MerchantId,
|
!string.Equals(existing.Code, normalizedCode, StringComparison.OrdinalIgnoreCase))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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. 更新门店字段
|
// 4. 更新门店字段
|
||||||
existing.Code = normalizedCode;
|
|
||||||
existing.Name = request.Name.Trim();
|
existing.Name = request.Name.Trim();
|
||||||
existing.Phone = request.ContactPhone.Trim();
|
existing.Phone = request.ContactPhone.Trim();
|
||||||
existing.ManagerName = request.ManagerName.Trim();
|
existing.ManagerName = request.ManagerName.Trim();
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ public sealed class CreateStoreCommandValidator : AbstractValidator<CreateStoreC
|
|||||||
{
|
{
|
||||||
// 1. 校验核心字段
|
// 1. 校验核心字段
|
||||||
RuleFor(command => command.Name).NotEmpty().MaximumLength(128);
|
RuleFor(command => command.Name).NotEmpty().MaximumLength(128);
|
||||||
RuleFor(command => command.Code).NotEmpty().MaximumLength(32);
|
|
||||||
RuleFor(command => command.ContactPhone).NotEmpty().MaximumLength(32);
|
RuleFor(command => command.ContactPhone).NotEmpty().MaximumLength(32);
|
||||||
RuleFor(command => command.ManagerName).NotEmpty().MaximumLength(64);
|
RuleFor(command => command.ManagerName).NotEmpty().MaximumLength(64);
|
||||||
RuleFor(command => command.Address).NotEmpty().MaximumLength(256);
|
RuleFor(command => command.Address).NotEmpty().MaximumLength(256);
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ public sealed class UpdateStoreCommandValidator : AbstractValidator<UpdateStoreC
|
|||||||
// 1. 校验标识与核心字段
|
// 1. 校验标识与核心字段
|
||||||
RuleFor(command => command.Id).GreaterThan(0);
|
RuleFor(command => command.Id).GreaterThan(0);
|
||||||
RuleFor(command => command.Name).NotEmpty().MaximumLength(128);
|
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.ContactPhone).NotEmpty().MaximumLength(32);
|
||||||
RuleFor(command => command.ManagerName).NotEmpty().MaximumLength(64);
|
RuleFor(command => command.ManagerName).NotEmpty().MaximumLength(64);
|
||||||
RuleFor(command => command.Address).NotEmpty().MaximumLength(256);
|
RuleFor(command => command.Address).NotEmpty().MaximumLength(256);
|
||||||
|
|||||||
Reference in New Issue
Block a user