refactor(store): remove fallback config behavior and expose isConfigured
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 43s

This commit is contained in:
2026-02-20 09:55:11 +08:00
parent 938da7469c
commit 8a6fc867c2
10 changed files with 166 additions and 173 deletions

View File

@@ -107,9 +107,13 @@ public sealed class StoreDeliverySettingsDto
/// </summary> /// </summary>
public string StoreId { get; set; } = string.Empty; public string StoreId { get; set; } = string.Empty;
/// <summary> /// <summary>
/// IsConfigured。
/// </summary>
public bool IsConfigured { get; set; }
/// <summary>
/// Mode。 /// Mode。
/// </summary> /// </summary>
public string Mode { get; set; } = "radius"; public string? Mode { get; set; }
/// <summary> /// <summary>
/// RadiusCenterLatitude。 /// RadiusCenterLatitude。
/// </summary> /// </summary>
@@ -129,7 +133,7 @@ public sealed class StoreDeliverySettingsDto
/// <summary> /// <summary>
/// GeneralSettings。 /// GeneralSettings。
/// </summary> /// </summary>
public DeliveryGeneralSettingsDto GeneralSettings { get; set; } = new(); public DeliveryGeneralSettingsDto? GeneralSettings { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -83,9 +83,13 @@ public sealed class StoreDineInSettingsDto
/// </summary> /// </summary>
public string StoreId { get; set; } = string.Empty; public string StoreId { get; set; } = string.Empty;
/// <summary> /// <summary>
/// IsConfigured。
/// </summary>
public bool IsConfigured { get; set; }
/// <summary>
/// BasicSettings。 /// BasicSettings。
/// </summary> /// </summary>
public DineInBasicSettingsDto BasicSettings { get; set; } = new(); public DineInBasicSettingsDto? BasicSettings { get; set; }
/// <summary> /// <summary>
/// Areas。 /// Areas。
/// </summary> /// </summary>

View File

@@ -67,6 +67,10 @@ public sealed class StoreFeesSettingsDto
/// </summary> /// </summary>
public string StoreId { get; set; } = string.Empty; public string StoreId { get; set; } = string.Empty;
/// <summary> /// <summary>
/// IsConfigured。
/// </summary>
public bool IsConfigured { get; set; }
/// <summary>
/// MinimumOrderAmount。 /// MinimumOrderAmount。
/// </summary> /// </summary>
public decimal MinimumOrderAmount { get; set; } public decimal MinimumOrderAmount { get; set; }

View File

@@ -145,13 +145,17 @@ public sealed class StorePickupSettingsDto
/// </summary> /// </summary>
public string StoreId { get; set; } = string.Empty; public string StoreId { get; set; } = string.Empty;
/// <summary> /// <summary>
/// IsConfigured。
/// </summary>
public bool IsConfigured { get; set; }
/// <summary>
/// Mode。 /// Mode。
/// </summary> /// </summary>
public string Mode { get; set; } = "big"; public string? Mode { get; set; }
/// <summary> /// <summary>
/// BasicSettings。 /// BasicSettings。
/// </summary> /// </summary>
public PickupBasicSettingsDto BasicSettings { get; set; } = new(); public PickupBasicSettingsDto? BasicSettings { get; set; }
/// <summary> /// <summary>
/// BigSlots。 /// BigSlots。
/// </summary> /// </summary>
@@ -159,7 +163,7 @@ public sealed class StorePickupSettingsDto
/// <summary> /// <summary>
/// FineRule。 /// FineRule。
/// </summary> /// </summary>
public PickupFineRuleDto FineRule { get; set; } = new(); public PickupFineRuleDto? FineRule { get; set; }
/// <summary> /// <summary>
/// PreviewDays。 /// PreviewDays。
/// </summary> /// </summary>

View File

@@ -48,21 +48,25 @@ public sealed class StoreDeliveryController(
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var radiusTiers = ParseRadiusTiers(setting?.RadiusTiersJson); var radiusTiers = ParseRadiusTiers(setting?.RadiusTiersJson);
var isConfigured = setting is not null || polygonZones.Count > 0 || radiusTiers.Count > 0;
return ApiResponse<StoreDeliverySettingsDto>.Ok(new StoreDeliverySettingsDto return ApiResponse<StoreDeliverySettingsDto>.Ok(new StoreDeliverySettingsDto
{ {
StoreId = parsedStoreId.ToString(), StoreId = parsedStoreId.ToString(),
Mode = StoreApiHelpers.ToDeliveryModeText(setting?.Mode ?? StoreDeliveryMode.Radius), IsConfigured = isConfigured,
Mode = setting is null ? null : StoreApiHelpers.ToDeliveryModeText(setting.Mode),
RadiusCenterLatitude = setting?.RadiusCenterLatitude, RadiusCenterLatitude = setting?.RadiusCenterLatitude,
RadiusCenterLongitude = setting?.RadiusCenterLongitude, RadiusCenterLongitude = setting?.RadiusCenterLongitude,
RadiusTiers = radiusTiers, RadiusTiers = radiusTiers,
PolygonZones = polygonZones.Select(MapPolygonZone).ToList(), PolygonZones = polygonZones.Select(MapPolygonZone).ToList(),
GeneralSettings = new DeliveryGeneralSettingsDto GeneralSettings = setting is null
{ ? null
EtaAdjustmentMinutes = setting?.EtaAdjustmentMinutes ?? 10, : new DeliveryGeneralSettingsDto
FreeDeliveryThreshold = setting?.FreeDeliveryThreshold ?? 30m, {
HourlyCapacityLimit = setting?.HourlyCapacityLimit ?? 50, EtaAdjustmentMinutes = setting.EtaAdjustmentMinutes,
MaxDeliveryDistance = setting?.MaxDeliveryDistance ?? 5m FreeDeliveryThreshold = setting.FreeDeliveryThreshold,
} HourlyCapacityLimit = setting.HourlyCapacityLimit,
MaxDeliveryDistance = setting.MaxDeliveryDistance
}
}); });
} }
@@ -98,6 +102,16 @@ public sealed class StoreDeliveryController(
public async Task<ApiResponse<object>> Save([FromBody] StoreDeliverySettingsDto request, CancellationToken cancellationToken) public async Task<ApiResponse<object>> Save([FromBody] StoreDeliverySettingsDto request, CancellationToken cancellationToken)
{ {
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId"); var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
if (string.IsNullOrWhiteSpace(request.Mode))
{
throw new BusinessException(ErrorCodes.BadRequest, "mode 不能为空");
}
if (request.GeneralSettings is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "generalSettings 不能为空");
}
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService); var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken); await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
@@ -203,6 +217,10 @@ public sealed class StoreDeliveryController(
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId) .Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (sourceSetting is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "源门店未配置配送设置,无法复制");
}
var targetSettings = await dbContext.StoreDeliverySettings var targetSettings = await dbContext.StoreDeliverySettings
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId)) .Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
@@ -220,14 +238,14 @@ public sealed class StoreDeliveryController(
await dbContext.StoreDeliverySettings.AddAsync(targetSetting, cancellationToken); await dbContext.StoreDeliverySettings.AddAsync(targetSetting, cancellationToken);
} }
targetSetting.Mode = sourceSetting?.Mode ?? StoreDeliveryMode.Radius; targetSetting.Mode = sourceSetting.Mode;
targetSetting.EtaAdjustmentMinutes = sourceSetting?.EtaAdjustmentMinutes ?? 10; targetSetting.EtaAdjustmentMinutes = sourceSetting.EtaAdjustmentMinutes;
targetSetting.FreeDeliveryThreshold = sourceSetting?.FreeDeliveryThreshold ?? 30m; targetSetting.FreeDeliveryThreshold = sourceSetting.FreeDeliveryThreshold;
targetSetting.HourlyCapacityLimit = sourceSetting?.HourlyCapacityLimit ?? 50; targetSetting.HourlyCapacityLimit = sourceSetting.HourlyCapacityLimit;
targetSetting.MaxDeliveryDistance = sourceSetting?.MaxDeliveryDistance ?? 5m; targetSetting.MaxDeliveryDistance = sourceSetting.MaxDeliveryDistance;
targetSetting.RadiusCenterLatitude = sourceSetting?.RadiusCenterLatitude; targetSetting.RadiusCenterLatitude = sourceSetting.RadiusCenterLatitude;
targetSetting.RadiusCenterLongitude = sourceSetting?.RadiusCenterLongitude; targetSetting.RadiusCenterLongitude = sourceSetting.RadiusCenterLongitude;
targetSetting.RadiusTiersJson = sourceSetting?.RadiusTiersJson; targetSetting.RadiusTiersJson = sourceSetting.RadiusTiersJson;
} }
var targetZones = await dbContext.StoreDeliveryZones var targetZones = await dbContext.StoreDeliveryZones
@@ -264,55 +282,20 @@ public sealed class StoreDeliveryController(
private static List<RadiusTierDto> ParseRadiusTiers(string? raw) private static List<RadiusTierDto> ParseRadiusTiers(string? raw)
{ {
if (!string.IsNullOrWhiteSpace(raw)) if (string.IsNullOrWhiteSpace(raw))
{ {
try return [];
{
var parsed = JsonSerializer.Deserialize<List<RadiusTierDto>>(raw, StoreApiHelpers.JsonOptions);
if (parsed is not null && parsed.Count > 0)
{
return NormalizeRadiusTiers(parsed);
}
}
catch
{
// 忽略配置反序列化异常并回落默认值
}
} }
return NormalizeRadiusTiers(new List<RadiusTierDto> try
{ {
new() var parsed = JsonSerializer.Deserialize<List<RadiusTierDto>>(raw, StoreApiHelpers.JsonOptions);
{ return NormalizeRadiusTiers(parsed);
Id = "tier-1", }
MinDistance = 0m, catch
MaxDistance = 1m, {
DeliveryFee = 3m, return [];
EtaMinutes = 20, }
MinOrderAmount = 15m,
Color = "#52c41a"
},
new()
{
Id = "tier-2",
MinDistance = 1m,
MaxDistance = 3m,
DeliveryFee = 5m,
EtaMinutes = 35,
MinOrderAmount = 20m,
Color = "#faad14"
},
new()
{
Id = "tier-3",
MinDistance = 3m,
MaxDistance = 5m,
DeliveryFee = 8m,
EtaMinutes = 50,
MinOrderAmount = 25m,
Color = "#ff4d4f"
}
});
} }
private static List<RadiusTierDto> NormalizeRadiusTiers(IEnumerable<RadiusTierDto>? source) private static List<RadiusTierDto> NormalizeRadiusTiers(IEnumerable<RadiusTierDto>? source)
@@ -343,9 +326,9 @@ public sealed class StoreDeliveryController(
{ {
Id = source.Id.ToString(), Id = source.Id.ToString(),
Name = source.ZoneName, Name = source.ZoneName,
Color = source.Color ?? "#1677ff", Color = source.Color ?? string.Empty,
DeliveryFee = source.DeliveryFee ?? 0m, DeliveryFee = source.DeliveryFee ?? 0m,
EtaMinutes = source.EstimatedMinutes ?? 20, EtaMinutes = source.EstimatedMinutes ?? 0,
MinOrderAmount = source.MinimumOrderAmount ?? 0m, MinOrderAmount = source.MinimumOrderAmount ?? 0m,
Priority = source.Priority, Priority = source.Priority,
PolygonGeoJson = source.PolygonGeoJson PolygonGeoJson = source.PolygonGeoJson

View File

@@ -47,16 +47,20 @@ public sealed class StoreDineInController(
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId) .Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
.OrderBy(x => x.TableCode) .OrderBy(x => x.TableCode)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var isConfigured = basic is not null || areas.Count > 0 || tables.Count > 0;
return ApiResponse<StoreDineInSettingsDto>.Ok(new StoreDineInSettingsDto return ApiResponse<StoreDineInSettingsDto>.Ok(new StoreDineInSettingsDto
{ {
StoreId = parsedStoreId.ToString(), StoreId = parsedStoreId.ToString(),
BasicSettings = new DineInBasicSettingsDto IsConfigured = isConfigured,
{ BasicSettings = basic is null
Enabled = basic?.Enabled ?? true, ? null
DefaultDiningMinutes = basic?.DefaultDiningMinutes ?? 90, : new DineInBasicSettingsDto
OvertimeReminderMinutes = basic?.OvertimeReminderMinutes ?? 10 {
}, Enabled = basic.Enabled,
DefaultDiningMinutes = basic.DefaultDiningMinutes,
OvertimeReminderMinutes = basic.OvertimeReminderMinutes
},
Areas = areas.Select(MapArea).ToList(), Areas = areas.Select(MapArea).ToList(),
Tables = tables.Select(MapTable).ToList() Tables = tables.Select(MapTable).ToList()
}); });
@@ -369,6 +373,10 @@ public sealed class StoreDineInController(
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId) .Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
.OrderBy(x => x.TableCode) .OrderBy(x => x.TableCode)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (sourceBasic is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "源门店未配置堂食基础设置,无法复制");
}
foreach (var targetStoreId in accessibleTargetIds) foreach (var targetStoreId in accessibleTargetIds)
{ {
@@ -383,9 +391,9 @@ public sealed class StoreDineInController(
await dbContext.StoreDineInSettings.AddAsync(targetBasic, cancellationToken); await dbContext.StoreDineInSettings.AddAsync(targetBasic, cancellationToken);
} }
targetBasic.Enabled = sourceBasic?.Enabled ?? true; targetBasic.Enabled = sourceBasic.Enabled;
targetBasic.DefaultDiningMinutes = sourceBasic?.DefaultDiningMinutes ?? 90; targetBasic.DefaultDiningMinutes = sourceBasic.DefaultDiningMinutes;
targetBasic.OvertimeReminderMinutes = sourceBasic?.OvertimeReminderMinutes ?? 10; targetBasic.OvertimeReminderMinutes = sourceBasic.OvertimeReminderMinutes;
var targetTables = await dbContext.StoreTables var targetTables = await dbContext.StoreTables
.Where(x => x.TenantId == tenantId && x.StoreId == targetStoreId) .Where(x => x.TenantId == tenantId && x.StoreId == targetStoreId)

View File

@@ -9,6 +9,8 @@ using TakeoutSaaS.Application.App.Stores.Services;
using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Entities;
using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Enums;
using TakeoutSaaS.Infrastructure.App.Persistence; using TakeoutSaaS.Infrastructure.App.Persistence;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api; using TakeoutSaaS.Shared.Web.Api;
using TakeoutSaaS.TenantApi.Contracts.Store; using TakeoutSaaS.TenantApi.Contracts.Store;
@@ -116,6 +118,10 @@ public sealed class StoreFeesController(
var sourceFee = await dbContext.StoreFees var sourceFee = await dbContext.StoreFees
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == sourceStoreId, cancellationToken); .FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == sourceStoreId, cancellationToken);
if (sourceFee is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "源门店未配置费用设置,无法复制");
}
var targetFees = await dbContext.StoreFees var targetFees = await dbContext.StoreFees
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId)) .Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
@@ -133,17 +139,17 @@ public sealed class StoreFeesController(
await dbContext.StoreFees.AddAsync(targetFee, cancellationToken); await dbContext.StoreFees.AddAsync(targetFee, cancellationToken);
} }
targetFee.MinimumOrderAmount = sourceFee?.MinimumOrderAmount ?? 0m; targetFee.MinimumOrderAmount = sourceFee.MinimumOrderAmount;
targetFee.BaseDeliveryFee = sourceFee?.BaseDeliveryFee ?? 0m; targetFee.BaseDeliveryFee = sourceFee.BaseDeliveryFee;
targetFee.FreeDeliveryThreshold = sourceFee?.FreeDeliveryThreshold; targetFee.FreeDeliveryThreshold = sourceFee.FreeDeliveryThreshold;
targetFee.PackagingFeeMode = sourceFee?.PackagingFeeMode ?? PackagingFeeMode.Fixed; targetFee.PackagingFeeMode = sourceFee.PackagingFeeMode;
targetFee.OrderPackagingFeeMode = sourceFee?.OrderPackagingFeeMode ?? OrderPackagingFeeMode.Fixed; targetFee.OrderPackagingFeeMode = sourceFee.OrderPackagingFeeMode;
targetFee.FixedPackagingFee = sourceFee?.FixedPackagingFee ?? 0m; targetFee.FixedPackagingFee = sourceFee.FixedPackagingFee;
targetFee.PackagingFeeTiersJson = sourceFee?.PackagingFeeTiersJson; targetFee.PackagingFeeTiersJson = sourceFee.PackagingFeeTiersJson;
targetFee.CutleryFeeEnabled = sourceFee?.CutleryFeeEnabled ?? false; targetFee.CutleryFeeEnabled = sourceFee.CutleryFeeEnabled;
targetFee.CutleryFeeAmount = sourceFee?.CutleryFeeAmount ?? 0m; targetFee.CutleryFeeAmount = sourceFee.CutleryFeeAmount;
targetFee.RushFeeEnabled = sourceFee?.RushFeeEnabled ?? false; targetFee.RushFeeEnabled = sourceFee.RushFeeEnabled;
targetFee.RushFeeAmount = sourceFee?.RushFeeAmount ?? 0m; targetFee.RushFeeAmount = sourceFee.RushFeeAmount;
} }
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
@@ -171,6 +177,7 @@ public sealed class StoreFeesController(
return new StoreFeesSettingsDto return new StoreFeesSettingsDto
{ {
StoreId = storeId.ToString(), StoreId = storeId.ToString(),
IsConfigured = source is not null,
MinimumOrderAmount = source?.MinimumOrderAmount ?? 0m, MinimumOrderAmount = source?.MinimumOrderAmount ?? 0m,
BaseDeliveryFee = source?.DeliveryFee ?? 0m, BaseDeliveryFee = source?.DeliveryFee ?? 0m,
FreeDeliveryThreshold = source?.FreeDeliveryThreshold, FreeDeliveryThreshold = source?.FreeDeliveryThreshold,
@@ -196,9 +203,17 @@ public sealed class StoreFeesController(
private static PackagingFeeMode ParsePackagingFeeMode(string? value) private static PackagingFeeMode ParsePackagingFeeMode(string? value)
{ {
return string.Equals(value, "item", StringComparison.OrdinalIgnoreCase) if (string.Equals(value, "item", StringComparison.OrdinalIgnoreCase))
? PackagingFeeMode.PerItem {
: PackagingFeeMode.Fixed; return PackagingFeeMode.PerItem;
}
if (string.Equals(value, "order", StringComparison.OrdinalIgnoreCase))
{
return PackagingFeeMode.Fixed;
}
throw new BusinessException(ErrorCodes.BadRequest, "packagingFeeMode 非法");
} }
private static string ToPackagingFeeModeText(PackagingFeeMode value) private static string ToPackagingFeeModeText(PackagingFeeMode value)
@@ -208,9 +223,17 @@ public sealed class StoreFeesController(
private static OrderPackagingFeeMode ParseOrderPackagingFeeMode(string? value) private static OrderPackagingFeeMode ParseOrderPackagingFeeMode(string? value)
{ {
return string.Equals(value, "tiered", StringComparison.OrdinalIgnoreCase) if (string.Equals(value, "tiered", StringComparison.OrdinalIgnoreCase))
? OrderPackagingFeeMode.Tiered {
: OrderPackagingFeeMode.Fixed; return OrderPackagingFeeMode.Tiered;
}
if (string.Equals(value, "fixed", StringComparison.OrdinalIgnoreCase))
{
return OrderPackagingFeeMode.Fixed;
}
throw new BusinessException(ErrorCodes.BadRequest, "orderPackagingFeeMode 非法");
} }
private static string ToOrderPackagingFeeModeText(OrderPackagingFeeMode value) private static string ToOrderPackagingFeeModeText(OrderPackagingFeeMode value)

View File

@@ -7,6 +7,8 @@ using TakeoutSaaS.Application.App.Stores.Services;
using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Entities;
using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Enums;
using TakeoutSaaS.Infrastructure.App.Persistence; using TakeoutSaaS.Infrastructure.App.Persistence;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api; using TakeoutSaaS.Shared.Web.Api;
using TakeoutSaaS.TenantApi.Contracts.Store; using TakeoutSaaS.TenantApi.Contracts.Store;
@@ -46,18 +48,22 @@ public sealed class StorePickupController(
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
var fineRule = ParseFineRule(setting?.FineRuleJson); var fineRule = ParseFineRule(setting?.FineRuleJson);
var previewDays = BuildPreviewDays(fineRule); var previewDays = fineRule is null ? [] : BuildPreviewDays(fineRule);
var isConfigured = setting is not null || slots.Count > 0;
var response = new StorePickupSettingsDto var response = new StorePickupSettingsDto
{ {
StoreId = parsedStoreId.ToString(), StoreId = parsedStoreId.ToString(),
Mode = StoreApiHelpers.ToPickupModeText(setting?.Mode ?? StorePickupMode.Big), IsConfigured = isConfigured,
BasicSettings = new PickupBasicSettingsDto Mode = setting is null ? null : StoreApiHelpers.ToPickupModeText(setting.Mode),
{ BasicSettings = setting is null
AllowSameDayPickup = setting?.AllowToday ?? true, ? null
BookingDays = setting?.AllowDaysAhead ?? 3, : new PickupBasicSettingsDto
MaxItemsPerOrder = setting?.MaxQuantityPerOrder ?? 20 {
}, AllowSameDayPickup = setting.AllowToday,
BookingDays = setting.AllowDaysAhead,
MaxItemsPerOrder = setting.MaxQuantityPerOrder
},
BigSlots = slots.Select(slot => new PickupSlotDto BigSlots = slots.Select(slot => new PickupSlotDto
{ {
Id = slot.Id.ToString(), Id = slot.Id.ToString(),
@@ -211,6 +217,10 @@ public sealed class StorePickupController(
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId) .Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (sourceSetting is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "源门店未配置自提设置,无法复制");
}
var targetSettings = await dbContext.StorePickupSettings var targetSettings = await dbContext.StorePickupSettings
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId)) .Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
@@ -229,12 +239,12 @@ public sealed class StorePickupController(
await dbContext.StorePickupSettings.AddAsync(targetSetting, cancellationToken); await dbContext.StorePickupSettings.AddAsync(targetSetting, cancellationToken);
} }
targetSetting.AllowToday = sourceSetting?.AllowToday ?? true; targetSetting.AllowToday = sourceSetting.AllowToday;
targetSetting.AllowDaysAhead = sourceSetting?.AllowDaysAhead ?? 3; targetSetting.AllowDaysAhead = sourceSetting.AllowDaysAhead;
targetSetting.DefaultCutoffMinutes = sourceSetting?.DefaultCutoffMinutes ?? 30; targetSetting.DefaultCutoffMinutes = sourceSetting.DefaultCutoffMinutes;
targetSetting.MaxQuantityPerOrder = sourceSetting?.MaxQuantityPerOrder ?? 20; targetSetting.MaxQuantityPerOrder = sourceSetting.MaxQuantityPerOrder;
targetSetting.Mode = sourceSetting?.Mode ?? StorePickupMode.Big; targetSetting.Mode = sourceSetting.Mode;
targetSetting.FineRuleJson = sourceSetting?.FineRuleJson; targetSetting.FineRuleJson = sourceSetting.FineRuleJson;
targetSetting.RowVersion = CreateRowVersion(); targetSetting.RowVersion = CreateRowVersion();
} }
@@ -295,46 +305,35 @@ public sealed class StorePickupController(
return RandomNumberGenerator.GetBytes(16); return RandomNumberGenerator.GetBytes(16);
} }
private static PickupFineRuleDto ParseFineRule(string? raw) private static PickupFineRuleDto? ParseFineRule(string? raw)
{ {
if (!string.IsNullOrWhiteSpace(raw)) if (string.IsNullOrWhiteSpace(raw))
{ {
try return null;
{
var parsed = JsonSerializer.Deserialize<PickupFineRuleDto>(raw, StoreApiHelpers.JsonOptions);
if (parsed is not null)
{
return NormalizeFineRule(parsed);
}
}
catch
{
// 忽略反序列化异常,回落默认配置
}
} }
return new PickupFineRuleDto try
{ {
IntervalMinutes = 30, var parsed = JsonSerializer.Deserialize<PickupFineRuleDto>(raw, StoreApiHelpers.JsonOptions);
SlotCapacity = 5, return parsed is null ? null : NormalizeFineRule(parsed);
DayStartTime = "09:00", }
DayEndTime = "20:30", catch
MinAdvanceHours = 2, {
DayOfWeeks = [0, 1, 2, 3, 4, 5, 6] return null;
}; }
} }
private static PickupFineRuleDto NormalizeFineRule(PickupFineRuleDto source) private static PickupFineRuleDto NormalizeFineRule(PickupFineRuleDto source)
{ {
var start = string.IsNullOrWhiteSpace(source.DayStartTime) ? "09:00" : source.DayStartTime; var start = StoreApiHelpers.ParseRequiredTime(source.DayStartTime, "fineRule.dayStartTime");
var end = string.IsNullOrWhiteSpace(source.DayEndTime) ? "20:30" : source.DayEndTime; var end = StoreApiHelpers.ParseRequiredTime(source.DayEndTime, "fineRule.dayEndTime");
return new PickupFineRuleDto return new PickupFineRuleDto
{ {
IntervalMinutes = Math.Clamp(source.IntervalMinutes, 5, 180), IntervalMinutes = Math.Clamp(source.IntervalMinutes, 5, 180),
SlotCapacity = Math.Clamp(source.SlotCapacity, 1, 999), SlotCapacity = Math.Clamp(source.SlotCapacity, 1, 999),
DayStartTime = StoreApiHelpers.ToHHmm(StoreApiHelpers.ParseRequiredTime(start, "fineRule.dayStartTime")), DayStartTime = StoreApiHelpers.ToHHmm(start),
DayEndTime = StoreApiHelpers.ToHHmm(StoreApiHelpers.ParseRequiredTime(end, "fineRule.dayEndTime")), DayEndTime = StoreApiHelpers.ToHHmm(end),
MinAdvanceHours = Math.Clamp(source.MinAdvanceHours, 0, 72), MinAdvanceHours = Math.Clamp(source.MinAdvanceHours, 0, 72),
DayOfWeeks = (source.DayOfWeeks ?? []) DayOfWeeks = (source.DayOfWeeks ?? [])
.Distinct() .Distinct()

View File

@@ -2,7 +2,6 @@ using MediatR;
using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores;
using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Dto;
using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Application.App.Stores.Queries;
using TakeoutSaaS.Domain.Stores.Entities;
using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Domain.Stores.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Exceptions;
@@ -31,25 +30,7 @@ public sealed class GetStoreFeeQueryHandler(
// 2. (空行后) 查询费用配置 // 2. (空行后) 查询费用配置
var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, tenantId, cancellationToken); var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, tenantId, cancellationToken);
if (fee is null)
{
var fallback = new StoreFee
{
StoreId = request.StoreId,
MinimumOrderAmount = 0m,
BaseDeliveryFee = 0m,
PackagingFeeMode = Domain.Stores.Enums.PackagingFeeMode.Fixed,
OrderPackagingFeeMode = Domain.Stores.Enums.OrderPackagingFeeMode.Fixed,
FixedPackagingFee = 0m,
CutleryFeeEnabled = false,
CutleryFeeAmount = 0m,
RushFeeEnabled = false,
RushFeeAmount = 0m
};
return StoreMapping.ToDto(fallback);
}
// 3. (空行后) 返回结果 // 3. (空行后) 返回结果
return StoreMapping.ToDto(fee); return fee is null ? null : StoreMapping.ToDto(fee);
} }
} }

View File

@@ -101,29 +101,12 @@ public static class StoreListMapping
result.Add(ServiceType.DineIn); result.Add(ServiceType.DineIn);
} }
// 2. 兜底默认展示配送服务
if (result.Count == 0)
{
result.Add(ServiceType.Delivery);
}
return result; return result;
} }
private static string ResolveAddress(Store store) private static string ResolveAddress(Store store)
{ {
// 1. 地址优先返回业务地址字段 return string.IsNullOrWhiteSpace(store.Address) ? string.Empty : store.Address;
if (!string.IsNullOrWhiteSpace(store.Address))
{
return store.Address;
}
// 2. 兜底拼接省市区
var parts = new[] { store.Province, store.City, store.District }
.Where(static part => !string.IsNullOrWhiteSpace(part))
.ToArray();
return parts.Length == 0 ? string.Empty : string.Join(string.Empty, parts);
} }
} }