feat: 完成门店管理剩余接口并补齐文档注释
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 46s
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 46s
This commit is contained in:
@@ -0,0 +1,147 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 半径梯度。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RadiusTierDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// MinDistance。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MinDistance { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// MaxDistance。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MaxDistance { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// DeliveryFee。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DeliveryFee { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// EtaMinutes。
|
||||||
|
/// </summary>
|
||||||
|
public int EtaMinutes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// MinOrderAmount。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MinOrderAmount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Color。
|
||||||
|
/// </summary>
|
||||||
|
public string Color { get; set; } = "#1677ff";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 多边形区域。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PolygonZoneDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Name。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Color。
|
||||||
|
/// </summary>
|
||||||
|
public string Color { get; set; } = "#1677ff";
|
||||||
|
/// <summary>
|
||||||
|
/// DeliveryFee。
|
||||||
|
/// </summary>
|
||||||
|
public decimal DeliveryFee { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// EtaMinutes。
|
||||||
|
/// </summary>
|
||||||
|
public int EtaMinutes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// MinOrderAmount。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MinOrderAmount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Priority。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通用配送设置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeliveryGeneralSettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// EtaAdjustmentMinutes。
|
||||||
|
/// </summary>
|
||||||
|
public int EtaAdjustmentMinutes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// FreeDeliveryThreshold。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? FreeDeliveryThreshold { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// HourlyCapacityLimit。
|
||||||
|
/// </summary>
|
||||||
|
public int HourlyCapacityLimit { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// MaxDeliveryDistance。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MaxDeliveryDistance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店配送设置聚合。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreDeliverySettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Mode。
|
||||||
|
/// </summary>
|
||||||
|
public string Mode { get; set; } = "radius";
|
||||||
|
/// <summary>
|
||||||
|
/// RadiusTiers。
|
||||||
|
/// </summary>
|
||||||
|
public List<RadiusTierDto> RadiusTiers { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// PolygonZones。
|
||||||
|
/// </summary>
|
||||||
|
public List<PolygonZoneDto> PolygonZones { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// GeneralSettings。
|
||||||
|
/// </summary>
|
||||||
|
public DeliveryGeneralSettingsDto GeneralSettings { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制配送设置请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreDeliverySettingsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SourceStoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string SourceStoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// TargetStoreIds。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> TargetStoreIds { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreDeliverySettingsResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CopiedCount。
|
||||||
|
/// </summary>
|
||||||
|
public int CopiedCount { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 堂食基础设置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DineInBasicSettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enabled。
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// DefaultDiningMinutes。
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultDiningMinutes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// OvertimeReminderMinutes。
|
||||||
|
/// </summary>
|
||||||
|
public int OvertimeReminderMinutes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 堂食区域。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DineInAreaDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Name。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Description。
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Sort。
|
||||||
|
/// </summary>
|
||||||
|
public int Sort { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 堂食桌位。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DineInTableDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// AreaId。
|
||||||
|
/// </summary>
|
||||||
|
public string AreaId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Code。
|
||||||
|
/// </summary>
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Seats。
|
||||||
|
/// </summary>
|
||||||
|
public int Seats { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Status。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "free";
|
||||||
|
/// <summary>
|
||||||
|
/// Tags。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Tags { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 堂食设置聚合。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreDineInSettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// BasicSettings。
|
||||||
|
/// </summary>
|
||||||
|
public DineInBasicSettingsDto BasicSettings { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Areas。
|
||||||
|
/// </summary>
|
||||||
|
public List<DineInAreaDto> Areas { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// Tables。
|
||||||
|
/// </summary>
|
||||||
|
public List<DineInTableDto> Tables { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存基础设置请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveStoreDineInBasicSettingsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// BasicSettings。
|
||||||
|
/// </summary>
|
||||||
|
public DineInBasicSettingsDto BasicSettings { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存区域请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveDineInAreaRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Area。
|
||||||
|
/// </summary>
|
||||||
|
public DineInAreaDto Area { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除区域请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteDineInAreaRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// AreaId。
|
||||||
|
/// </summary>
|
||||||
|
public string AreaId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存桌位请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveDineInTableRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Table。
|
||||||
|
/// </summary>
|
||||||
|
public DineInTableDto Table { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除桌位请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteDineInTableRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// TableId。
|
||||||
|
/// </summary>
|
||||||
|
public string TableId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量生成桌位请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BatchCreateDineInTablesRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// AreaId。
|
||||||
|
/// </summary>
|
||||||
|
public string AreaId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// CodePrefix。
|
||||||
|
/// </summary>
|
||||||
|
public string CodePrefix { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// StartNumber。
|
||||||
|
/// </summary>
|
||||||
|
public int StartNumber { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Count。
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Seats。
|
||||||
|
/// </summary>
|
||||||
|
public int Seats { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量生成桌位结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BatchCreateDineInTablesResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CreatedTables。
|
||||||
|
/// </summary>
|
||||||
|
public List<DineInTableDto> CreatedTables { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制堂食设置请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreDineInSettingsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SourceStoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string SourceStoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// TargetStoreIds。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> TargetStoreIds { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreDineInSettingsResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CopiedCount。
|
||||||
|
/// </summary>
|
||||||
|
public int CopiedCount { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 阶梯包装费。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PackagingFeeTierDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// MinAmount。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MinAmount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// MaxAmount。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MaxAmount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Fee。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Fee { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Sort。
|
||||||
|
/// </summary>
|
||||||
|
public int Sort { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附加费用项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AdditionalFeeItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enabled。
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Amount。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 其他费用。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreOtherFeesDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cutlery。
|
||||||
|
/// </summary>
|
||||||
|
public AdditionalFeeItemDto Cutlery { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Rush。
|
||||||
|
/// </summary>
|
||||||
|
public AdditionalFeeItemDto Rush { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店费用设置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreFeesSettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// MinimumOrderAmount。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MinimumOrderAmount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// BaseDeliveryFee。
|
||||||
|
/// </summary>
|
||||||
|
public decimal BaseDeliveryFee { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// FreeDeliveryThreshold。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? FreeDeliveryThreshold { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// PackagingFeeMode。
|
||||||
|
/// </summary>
|
||||||
|
public string PackagingFeeMode { get; set; } = "order";
|
||||||
|
/// <summary>
|
||||||
|
/// OrderPackagingFeeMode。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderPackagingFeeMode { get; set; } = "fixed";
|
||||||
|
/// <summary>
|
||||||
|
/// FixedPackagingFee。
|
||||||
|
/// </summary>
|
||||||
|
public decimal FixedPackagingFee { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// PackagingFeeTiers。
|
||||||
|
/// </summary>
|
||||||
|
public List<PackagingFeeTierDto> PackagingFeeTiers { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// OtherFees。
|
||||||
|
/// </summary>
|
||||||
|
public StoreOtherFeesDto OtherFees { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制费用请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreFeesSettingsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SourceStoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string SourceStoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// TargetStoreIds。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> TargetStoreIds { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreFeesSettingsResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CopiedCount。
|
||||||
|
/// </summary>
|
||||||
|
public int CopiedCount { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 时段类型。
|
||||||
|
/// </summary>
|
||||||
|
public enum StoreHourSlotType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 营业时段。
|
||||||
|
/// </summary>
|
||||||
|
Business = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送时段。
|
||||||
|
/// </summary>
|
||||||
|
Delivery = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提时段。
|
||||||
|
/// </summary>
|
||||||
|
Pickup = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 特殊日期类型。
|
||||||
|
/// </summary>
|
||||||
|
public enum StoreHolidayType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 休息日。
|
||||||
|
/// </summary>
|
||||||
|
Closed = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 特殊营业日。
|
||||||
|
/// </summary>
|
||||||
|
Special = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 时段 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreHourTimeSlotDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Type。
|
||||||
|
/// </summary>
|
||||||
|
public int Type { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// StartTime。
|
||||||
|
/// </summary>
|
||||||
|
public string StartTime { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// EndTime。
|
||||||
|
/// </summary>
|
||||||
|
public string EndTime { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Capacity。
|
||||||
|
/// </summary>
|
||||||
|
public int? Capacity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Remark。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每日营业时间 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreHourDayHoursDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DayOfWeek。
|
||||||
|
/// </summary>
|
||||||
|
public int DayOfWeek { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// IsOpen。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsOpen { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Slots。
|
||||||
|
/// </summary>
|
||||||
|
public List<StoreHourTimeSlotDto> Slots { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 特殊日期 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreHourHolidayDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// StartDate。
|
||||||
|
/// </summary>
|
||||||
|
public string StartDate { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// EndDate。
|
||||||
|
/// </summary>
|
||||||
|
public string EndDate { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Type。
|
||||||
|
/// </summary>
|
||||||
|
public int Type { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// StartTime。
|
||||||
|
/// </summary>
|
||||||
|
public string? StartTime { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// EndTime。
|
||||||
|
/// </summary>
|
||||||
|
public string? EndTime { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Reason。
|
||||||
|
/// </summary>
|
||||||
|
public string Reason { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Remark。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店营业时间聚合。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreHoursDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// WeeklyHours。
|
||||||
|
/// </summary>
|
||||||
|
public List<StoreHourDayHoursDto> WeeklyHours { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// Holidays。
|
||||||
|
/// </summary>
|
||||||
|
public List<StoreHourHolidayDto> Holidays { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存每周时段请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveWeeklyHoursRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// WeeklyHours。
|
||||||
|
/// </summary>
|
||||||
|
public List<StoreHourDayHoursDto> WeeklyHours { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存特殊日期请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveHolidayRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Holiday。
|
||||||
|
/// </summary>
|
||||||
|
public StoreHourHolidayDto Holiday { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除特殊日期请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteHolidayRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制营业时间请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreHoursRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SourceStoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string SourceStoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// TargetStoreIds。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> TargetStoreIds { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// IncludeWeeklyHours。
|
||||||
|
/// </summary>
|
||||||
|
public bool? IncludeWeeklyHours { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// IncludeHolidays。
|
||||||
|
/// </summary>
|
||||||
|
public bool? IncludeHolidays { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreHoursResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CopiedCount。
|
||||||
|
/// </summary>
|
||||||
|
public int CopiedCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// IncludeWeeklyHours。
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeWeeklyHours { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// IncludeHolidays。
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeHolidays { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提基础设置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PickupBasicSettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AllowSameDayPickup。
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowSameDayPickup { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// BookingDays。
|
||||||
|
/// </summary>
|
||||||
|
public int BookingDays { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// MaxItemsPerOrder。
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxItemsPerOrder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提大时段。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PickupSlotDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Name。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// StartTime。
|
||||||
|
/// </summary>
|
||||||
|
public string StartTime { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// EndTime。
|
||||||
|
/// </summary>
|
||||||
|
public string EndTime { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// CutoffMinutes。
|
||||||
|
/// </summary>
|
||||||
|
public int CutoffMinutes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Capacity。
|
||||||
|
/// </summary>
|
||||||
|
public int Capacity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// ReservedCount。
|
||||||
|
/// </summary>
|
||||||
|
public int ReservedCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// DayOfWeeks。
|
||||||
|
/// </summary>
|
||||||
|
public List<int> DayOfWeeks { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// Enabled。
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 精细规则。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PickupFineRuleDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IntervalMinutes。
|
||||||
|
/// </summary>
|
||||||
|
public int IntervalMinutes { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// SlotCapacity。
|
||||||
|
/// </summary>
|
||||||
|
public int SlotCapacity { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// DayStartTime。
|
||||||
|
/// </summary>
|
||||||
|
public string DayStartTime { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// DayEndTime。
|
||||||
|
/// </summary>
|
||||||
|
public string DayEndTime { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// MinAdvanceHours。
|
||||||
|
/// </summary>
|
||||||
|
public int MinAdvanceHours { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// DayOfWeeks。
|
||||||
|
/// </summary>
|
||||||
|
public List<int> DayOfWeeks { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预览时段。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PickupPreviewSlotDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Time。
|
||||||
|
/// </summary>
|
||||||
|
public string Time { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Status。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "available";
|
||||||
|
/// <summary>
|
||||||
|
/// RemainingCount。
|
||||||
|
/// </summary>
|
||||||
|
public int RemainingCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预览日期。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PickupPreviewDayDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Date。
|
||||||
|
/// </summary>
|
||||||
|
public string Date { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Label。
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// SubLabel。
|
||||||
|
/// </summary>
|
||||||
|
public string SubLabel { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Slots。
|
||||||
|
/// </summary>
|
||||||
|
public List<PickupPreviewSlotDto> Slots { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店自提设置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StorePickupSettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Mode。
|
||||||
|
/// </summary>
|
||||||
|
public string Mode { get; set; } = "big";
|
||||||
|
/// <summary>
|
||||||
|
/// BasicSettings。
|
||||||
|
/// </summary>
|
||||||
|
public PickupBasicSettingsDto BasicSettings { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// BigSlots。
|
||||||
|
/// </summary>
|
||||||
|
public List<PickupSlotDto> BigSlots { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// FineRule。
|
||||||
|
/// </summary>
|
||||||
|
public PickupFineRuleDto FineRule { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// PreviewDays。
|
||||||
|
/// </summary>
|
||||||
|
public List<PickupPreviewDayDto> PreviewDays { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存基础设置请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SavePickupBasicSettingsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Mode。
|
||||||
|
/// </summary>
|
||||||
|
public string? Mode { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// BasicSettings。
|
||||||
|
/// </summary>
|
||||||
|
public PickupBasicSettingsDto BasicSettings { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存大时段请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SavePickupSlotsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Mode。
|
||||||
|
/// </summary>
|
||||||
|
public string? Mode { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Slots。
|
||||||
|
/// </summary>
|
||||||
|
public List<PickupSlotDto> Slots { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存精细规则请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SavePickupFineRuleRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Mode。
|
||||||
|
/// </summary>
|
||||||
|
public string? Mode { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// FineRule。
|
||||||
|
/// </summary>
|
||||||
|
public PickupFineRuleDto FineRule { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制自提设置请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStorePickupSettingsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SourceStoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string SourceStoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// TargetStoreIds。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> TargetStoreIds { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStorePickupSettingsResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CopiedCount。
|
||||||
|
/// </summary>
|
||||||
|
public int CopiedCount { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页结构。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PaginatedResultDto<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Items。
|
||||||
|
/// </summary>
|
||||||
|
public List<T> Items { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// Total。
|
||||||
|
/// </summary>
|
||||||
|
public int Total { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Page。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// PageSize。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工档案。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreStaffItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Name。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Phone。
|
||||||
|
/// </summary>
|
||||||
|
public string Phone { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Email。
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// RoleType。
|
||||||
|
/// </summary>
|
||||||
|
public string RoleType { get; set; } = "cashier";
|
||||||
|
/// <summary>
|
||||||
|
/// Status。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
/// <summary>
|
||||||
|
/// Permissions。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Permissions { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// AvatarColor。
|
||||||
|
/// </summary>
|
||||||
|
public string AvatarColor { get; set; } = "#1677ff";
|
||||||
|
/// <summary>
|
||||||
|
/// HiredAt。
|
||||||
|
/// </summary>
|
||||||
|
public string HiredAt { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 班次时间段模板。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StartTime。
|
||||||
|
/// </summary>
|
||||||
|
public string StartTime { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// EndTime。
|
||||||
|
/// </summary>
|
||||||
|
public string EndTime { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店班次模板。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreShiftTemplatesDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Morning。
|
||||||
|
/// </summary>
|
||||||
|
public ShiftTemplateItemDto Morning { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Evening。
|
||||||
|
/// </summary>
|
||||||
|
public ShiftTemplateItemDto Evening { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Full。
|
||||||
|
/// </summary>
|
||||||
|
public ShiftTemplateItemDto Full { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工单日排班。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StaffDayShiftDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DayOfWeek。
|
||||||
|
/// </summary>
|
||||||
|
public int DayOfWeek { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// ShiftType。
|
||||||
|
/// </summary>
|
||||||
|
public string ShiftType { get; set; } = "off";
|
||||||
|
/// <summary>
|
||||||
|
/// StartTime。
|
||||||
|
/// </summary>
|
||||||
|
public string StartTime { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// EndTime。
|
||||||
|
/// </summary>
|
||||||
|
public string EndTime { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工排班。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StaffScheduleDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StaffId。
|
||||||
|
/// </summary>
|
||||||
|
public string StaffId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Shifts。
|
||||||
|
/// </summary>
|
||||||
|
public List<StaffDayShiftDto> Shifts { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店排班聚合。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreStaffScheduleDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// WeekStartDate。
|
||||||
|
/// </summary>
|
||||||
|
public string WeekStartDate { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Templates。
|
||||||
|
/// </summary>
|
||||||
|
public StoreShiftTemplatesDto Templates { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// Schedules。
|
||||||
|
/// </summary>
|
||||||
|
public List<StaffScheduleDto> Schedules { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存员工请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveStoreStaffRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Id。
|
||||||
|
/// </summary>
|
||||||
|
public string? Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Name。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Phone。
|
||||||
|
/// </summary>
|
||||||
|
public string Phone { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Email。
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// RoleType。
|
||||||
|
/// </summary>
|
||||||
|
public string RoleType { get; set; } = "cashier";
|
||||||
|
/// <summary>
|
||||||
|
/// Status。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
/// <summary>
|
||||||
|
/// Permissions。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Permissions { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除员工请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteStoreStaffRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// StaffId。
|
||||||
|
/// </summary>
|
||||||
|
public string StaffId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存班次模板请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveStoreStaffTemplatesRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Templates。
|
||||||
|
/// </summary>
|
||||||
|
public StoreShiftTemplatesDto Templates { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存个人排班请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveStoreStaffPersonalScheduleRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// StaffId。
|
||||||
|
/// </summary>
|
||||||
|
public string StaffId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Shifts。
|
||||||
|
/// </summary>
|
||||||
|
public List<StaffDayShiftDto> Shifts { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存周排班请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveStoreStaffWeeklyScheduleRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// StoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// Schedules。
|
||||||
|
/// </summary>
|
||||||
|
public List<StaffScheduleDto> Schedules { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制排班请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreStaffScheduleRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SourceStoreId。
|
||||||
|
/// </summary>
|
||||||
|
public string SourceStoreId { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// TargetStoreIds。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> TargetStoreIds { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// CopyScope。
|
||||||
|
/// </summary>
|
||||||
|
public string CopyScope { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制排班结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CopyStoreStaffScheduleResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CopiedCount。
|
||||||
|
/// </summary>
|
||||||
|
public int CopiedCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// CopyScope。
|
||||||
|
/// </summary>
|
||||||
|
public string CopyScope { get; set; } = "template_and_schedule";
|
||||||
|
}
|
||||||
379
src/Api/TakeoutSaaS.TenantApi/Controllers/StoreApiHelpers.cs
Normal file
379
src/Api/TakeoutSaaS.TenantApi/Controllers/StoreApiHelpers.cs
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Services;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
internal static class StoreApiHelpers
|
||||||
|
{
|
||||||
|
private static readonly string[] AvatarColors =
|
||||||
|
[
|
||||||
|
"#f56a00",
|
||||||
|
"#7265e6",
|
||||||
|
"#52c41a",
|
||||||
|
"#fa8c16",
|
||||||
|
"#1890ff",
|
||||||
|
"#bfbfbf",
|
||||||
|
"#13c2c2",
|
||||||
|
"#eb2f96"
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public static (long TenantId, long MerchantId) GetTenantMerchantContext(StoreContextService storeContextService)
|
||||||
|
{
|
||||||
|
var (_, tenantId, merchantId) = storeContextService.GetRequiredContext();
|
||||||
|
return (tenantId, merchantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long ParseRequiredSnowflake(string? value, string fieldName)
|
||||||
|
{
|
||||||
|
if (!long.TryParse(value, out var id) || id <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 非法");
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long? ParseSnowflakeOrNull(string? value)
|
||||||
|
{
|
||||||
|
return long.TryParse(value, out var id) && id > 0 ? id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<long> ParseSnowflakeList(IEnumerable<string>? values)
|
||||||
|
{
|
||||||
|
if (values is null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
.Select(ParseSnowflakeOrNull)
|
||||||
|
.Where(id => id.HasValue)
|
||||||
|
.Select(id => id!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeSpan ParseRequiredTime(string? value, string fieldName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value) ||
|
||||||
|
!TimeSpan.TryParseExact(value, "hh\\:mm", CultureInfo.InvariantCulture, out var parsed))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 时间格式必须为 HH:mm");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToHHmm(TimeSpan value)
|
||||||
|
{
|
||||||
|
return value.ToString("hh\\:mm", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ToHHmm(TimeSpan? value)
|
||||||
|
{
|
||||||
|
return value.HasValue ? ToHHmm(value.Value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime ParseDateOnly(string? value, string fieldName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value) ||
|
||||||
|
!DateTime.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 日期格式必须为 yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToDateOnly(DateTime value)
|
||||||
|
{
|
||||||
|
return value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DayOfWeek UiDayOfWeekToDotNet(int uiDayOfWeek)
|
||||||
|
{
|
||||||
|
return uiDayOfWeek switch
|
||||||
|
{
|
||||||
|
0 => DayOfWeek.Monday,
|
||||||
|
1 => DayOfWeek.Tuesday,
|
||||||
|
2 => DayOfWeek.Wednesday,
|
||||||
|
3 => DayOfWeek.Thursday,
|
||||||
|
4 => DayOfWeek.Friday,
|
||||||
|
5 => DayOfWeek.Saturday,
|
||||||
|
6 => DayOfWeek.Sunday,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "dayOfWeek 必须在 0-6 之间")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int DotNetDayOfWeekToUi(DayOfWeek dayOfWeek)
|
||||||
|
{
|
||||||
|
return dayOfWeek switch
|
||||||
|
{
|
||||||
|
DayOfWeek.Monday => 0,
|
||||||
|
DayOfWeek.Tuesday => 1,
|
||||||
|
DayOfWeek.Wednesday => 2,
|
||||||
|
DayOfWeek.Thursday => 3,
|
||||||
|
DayOfWeek.Friday => 4,
|
||||||
|
DayOfWeek.Saturday => 5,
|
||||||
|
DayOfWeek.Sunday => 6,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SerializeWeekdays(IEnumerable<int>? uiDaysOfWeek)
|
||||||
|
{
|
||||||
|
var normalized = (uiDaysOfWeek ?? [])
|
||||||
|
.Distinct()
|
||||||
|
.Where(day => day is >= 0 and <= 6)
|
||||||
|
.OrderBy(day => day)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return normalized.Count == 0 ? string.Empty : string.Join(',', normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<int> DeserializeWeekdays(string? storedValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(storedValue))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = storedValue
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(item => int.TryParse(item, out var parsed) ? parsed : -1)
|
||||||
|
.Select(day =>
|
||||||
|
{
|
||||||
|
// 兼容旧数据(1-7)
|
||||||
|
if (day is >= 1 and <= 7)
|
||||||
|
{
|
||||||
|
return day == 7 ? 6 : day - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return day;
|
||||||
|
})
|
||||||
|
.Where(day => day is >= 0 and <= 6)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(day => day)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BusinessHourType ToBusinessHourType(int slotType)
|
||||||
|
{
|
||||||
|
return slotType switch
|
||||||
|
{
|
||||||
|
1 => BusinessHourType.Normal,
|
||||||
|
2 => BusinessHourType.PickupOrDelivery,
|
||||||
|
3 => BusinessHourType.ReservationOnly,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "slot.type 非法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int ToSlotType(BusinessHourType hourType)
|
||||||
|
{
|
||||||
|
return hourType switch
|
||||||
|
{
|
||||||
|
BusinessHourType.Normal => 1,
|
||||||
|
BusinessHourType.PickupOrDelivery => 2,
|
||||||
|
BusinessHourType.ReservationOnly => 3,
|
||||||
|
_ => 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StaffRoleType ToStaffRoleType(string? value)
|
||||||
|
{
|
||||||
|
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"manager" => StaffRoleType.Admin,
|
||||||
|
"cashier" => StaffRoleType.FrontDesk,
|
||||||
|
"chef" => StaffRoleType.Kitchen,
|
||||||
|
"courier" => StaffRoleType.Courier,
|
||||||
|
_ => StaffRoleType.FrontDesk
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToStaffRoleTypeText(StaffRoleType value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
StaffRoleType.Admin => "manager",
|
||||||
|
StaffRoleType.FrontDesk => "cashier",
|
||||||
|
StaffRoleType.Kitchen => "chef",
|
||||||
|
StaffRoleType.Courier => "courier",
|
||||||
|
StaffRoleType.Operator => "manager",
|
||||||
|
_ => "cashier"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StaffStatus ToStaffStatus(string? value)
|
||||||
|
{
|
||||||
|
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"active" => StaffStatus.Active,
|
||||||
|
"leave" => StaffStatus.Disabled,
|
||||||
|
"resigned" => StaffStatus.Resigned,
|
||||||
|
_ => StaffStatus.Active
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToStaffStatusText(StaffStatus value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
StaffStatus.Active => "active",
|
||||||
|
StaffStatus.Disabled => "leave",
|
||||||
|
StaffStatus.Resigned => "resigned",
|
||||||
|
_ => "active"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreTableStatus ToTableStatus(string? value)
|
||||||
|
{
|
||||||
|
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"free" => StoreTableStatus.Idle,
|
||||||
|
"disabled" => StoreTableStatus.Disabled,
|
||||||
|
"dining" => StoreTableStatus.Occupied,
|
||||||
|
"reserved" => StoreTableStatus.Cleaning,
|
||||||
|
_ => StoreTableStatus.Idle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToTableStatusText(StoreTableStatus value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
StoreTableStatus.Idle => "free",
|
||||||
|
StoreTableStatus.Disabled => "disabled",
|
||||||
|
StoreTableStatus.Occupied => "dining",
|
||||||
|
StoreTableStatus.Cleaning => "reserved",
|
||||||
|
_ => "free"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StorePickupMode ToPickupMode(string? value)
|
||||||
|
{
|
||||||
|
return string.Equals(value, "fine", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? StorePickupMode.Fine
|
||||||
|
: StorePickupMode.Big;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToPickupModeText(StorePickupMode value)
|
||||||
|
{
|
||||||
|
return value == StorePickupMode.Fine ? "fine" : "big";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreDeliveryMode ToDeliveryMode(string? value)
|
||||||
|
{
|
||||||
|
return string.Equals(value, "polygon", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? StoreDeliveryMode.Polygon
|
||||||
|
: StoreDeliveryMode.Radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToDeliveryModeText(StoreDeliveryMode value)
|
||||||
|
{
|
||||||
|
return value == StoreDeliveryMode.Polygon ? "polygon" : "radius";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreStaffShiftType ToShiftType(string? value)
|
||||||
|
{
|
||||||
|
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"morning" => StoreStaffShiftType.Morning,
|
||||||
|
"evening" => StoreStaffShiftType.Evening,
|
||||||
|
"full" => StoreStaffShiftType.Full,
|
||||||
|
"off" => StoreStaffShiftType.Off,
|
||||||
|
_ => StoreStaffShiftType.Off
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToShiftTypeText(StoreStaffShiftType value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
StoreStaffShiftType.Morning => "morning",
|
||||||
|
StoreStaffShiftType.Evening => "evening",
|
||||||
|
StoreStaffShiftType.Full => "full",
|
||||||
|
StoreStaffShiftType.Off => "off",
|
||||||
|
_ => "off"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<Store> EnsureStoreAccessibleAsync(
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
long tenantId,
|
||||||
|
long merchantId,
|
||||||
|
long storeId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var store = await dbContext.Stores
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == storeId && x.TenantId == tenantId && x.MerchantId == merchantId, cancellationToken);
|
||||||
|
|
||||||
|
if (store is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "门店不存在或无权限访问");
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<HashSet<long>> FilterAccessibleStoreIdsAsync(
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
long tenantId,
|
||||||
|
long merchantId,
|
||||||
|
IEnumerable<long> storeIds,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var ids = storeIds.Distinct().ToList();
|
||||||
|
if (ids.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dbContext.Stores
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId && ids.Contains(x.Id))
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToHashSetAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveAvatarColor(string? seed)
|
||||||
|
{
|
||||||
|
var source = string.IsNullOrWhiteSpace(seed) ? "store-staff" : seed;
|
||||||
|
var hash = 0;
|
||||||
|
foreach (var ch in source)
|
||||||
|
{
|
||||||
|
hash = (hash * 31 + ch) & int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AvatarColors[hash % AvatarColors.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveWeekStartDate(string? requestedWeekStartDate)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(requestedWeekStartDate) &&
|
||||||
|
DateTime.TryParseExact(requestedWeekStartDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed))
|
||||||
|
{
|
||||||
|
return ToDateOnly(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
var today = DateTime.UtcNow.Date;
|
||||||
|
var diff = today.DayOfWeek == DayOfWeek.Sunday ? -6 : 1 - (int)today.DayOfWeek;
|
||||||
|
var monday = today.AddDays(diff);
|
||||||
|
return ToDateOnly(monday);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Services;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
using TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店配送设置模块。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/store")]
|
||||||
|
public sealed class StoreDeliveryController(
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店配送设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("delivery")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreDeliverySettingsDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreDeliverySettingsDto>> Get([FromQuery] string storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var setting = await dbContext.StoreDeliverySettings
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == parsedStoreId, cancellationToken);
|
||||||
|
var polygonZones = await dbContext.StoreDeliveryZones
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.OrderBy(x => x.Priority)
|
||||||
|
.ThenBy(x => x.SortOrder)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var radiusTiers = ParseRadiusTiers(setting?.RadiusTiersJson);
|
||||||
|
return ApiResponse<StoreDeliverySettingsDto>.Ok(new StoreDeliverySettingsDto
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId.ToString(),
|
||||||
|
Mode = StoreApiHelpers.ToDeliveryModeText(setting?.Mode ?? StoreDeliveryMode.Radius),
|
||||||
|
RadiusTiers = radiusTiers,
|
||||||
|
PolygonZones = polygonZones.Select(MapPolygonZone).ToList(),
|
||||||
|
GeneralSettings = new DeliveryGeneralSettingsDto
|
||||||
|
{
|
||||||
|
EtaAdjustmentMinutes = setting?.EtaAdjustmentMinutes ?? 10,
|
||||||
|
FreeDeliveryThreshold = setting?.FreeDeliveryThreshold ?? 30m,
|
||||||
|
HourlyCapacityLimit = setting?.HourlyCapacityLimit ?? 50,
|
||||||
|
MaxDeliveryDistance = setting?.MaxDeliveryDistance ?? 5m
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存门店配送设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("delivery/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> Save([FromBody] StoreDeliverySettingsDto request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var setting = await dbContext.StoreDeliverySettings
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == parsedStoreId, cancellationToken);
|
||||||
|
if (setting is null)
|
||||||
|
{
|
||||||
|
setting = new StoreDeliverySetting
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreDeliverySettings.AddAsync(setting, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.Mode = StoreApiHelpers.ToDeliveryMode(request.Mode);
|
||||||
|
setting.EtaAdjustmentMinutes = Math.Clamp(request.GeneralSettings.EtaAdjustmentMinutes, 0, 240);
|
||||||
|
setting.FreeDeliveryThreshold = request.GeneralSettings.FreeDeliveryThreshold;
|
||||||
|
setting.HourlyCapacityLimit = Math.Clamp(request.GeneralSettings.HourlyCapacityLimit, 1, 9999);
|
||||||
|
setting.MaxDeliveryDistance = Math.Max(0m, request.GeneralSettings.MaxDeliveryDistance);
|
||||||
|
setting.RadiusTiersJson = JsonSerializer.Serialize(NormalizeRadiusTiers(request.RadiusTiers), StoreApiHelpers.JsonOptions);
|
||||||
|
|
||||||
|
var existingZones = await dbContext.StoreDeliveryZones
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var existingZoneMap = existingZones.ToDictionary(x => x.Id);
|
||||||
|
var retainedIds = new HashSet<long>();
|
||||||
|
|
||||||
|
foreach (var zone in request.PolygonZones ?? [])
|
||||||
|
{
|
||||||
|
var zoneId = StoreApiHelpers.ParseSnowflakeOrNull(zone.Id);
|
||||||
|
StoreDeliveryZone? entity = null;
|
||||||
|
if (zoneId.HasValue && existingZoneMap.TryGetValue(zoneId.Value, out var existing))
|
||||||
|
{
|
||||||
|
entity = existing;
|
||||||
|
retainedIds.Add(existing.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity is null)
|
||||||
|
{
|
||||||
|
entity = new StoreDeliveryZone
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId,
|
||||||
|
PolygonGeoJson = "{}"
|
||||||
|
};
|
||||||
|
await dbContext.StoreDeliveryZones.AddAsync(entity, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.ZoneName = zone.Name?.Trim() ?? string.Empty;
|
||||||
|
entity.Color = string.IsNullOrWhiteSpace(zone.Color) ? "#1677ff" : zone.Color.Trim();
|
||||||
|
entity.MinimumOrderAmount = Math.Max(0m, zone.MinOrderAmount);
|
||||||
|
entity.DeliveryFee = Math.Max(0m, zone.DeliveryFee);
|
||||||
|
entity.EstimatedMinutes = Math.Max(1, zone.EtaMinutes);
|
||||||
|
entity.Priority = Math.Max(1, zone.Priority);
|
||||||
|
entity.SortOrder = entity.Priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
var toDelete = existingZones
|
||||||
|
.Where(x => !retainedIds.Contains(x.Id))
|
||||||
|
.ToList();
|
||||||
|
dbContext.StoreDeliveryZones.RemoveRange(toDelete);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制门店配送设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("delivery/copy")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<CopyStoreDeliverySettingsResult>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<CopyStoreDeliverySettingsResult>> Copy([FromBody] CopyStoreDeliverySettingsRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sourceStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.SourceStoreId, "sourceStoreId");
|
||||||
|
var targetStoreIds = StoreApiHelpers.ParseSnowflakeList(request.TargetStoreIds);
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, sourceStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var accessibleTargetIds = await StoreApiHelpers.FilterAccessibleStoreIdsAsync(
|
||||||
|
dbContext,
|
||||||
|
tenantId,
|
||||||
|
merchantId,
|
||||||
|
targetStoreIds,
|
||||||
|
cancellationToken);
|
||||||
|
accessibleTargetIds.Remove(sourceStoreId);
|
||||||
|
|
||||||
|
if (accessibleTargetIds.Count == 0)
|
||||||
|
{
|
||||||
|
return ApiResponse<CopyStoreDeliverySettingsResult>.Ok(new CopyStoreDeliverySettingsResult
|
||||||
|
{
|
||||||
|
CopiedCount = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceSetting = await dbContext.StoreDeliverySettings
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == sourceStoreId, cancellationToken);
|
||||||
|
var sourceZones = await dbContext.StoreDeliveryZones
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var targetSettings = await dbContext.StoreDeliverySettings
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var targetSettingMap = targetSettings.ToDictionary(x => x.StoreId);
|
||||||
|
|
||||||
|
foreach (var targetStoreId in accessibleTargetIds)
|
||||||
|
{
|
||||||
|
if (!targetSettingMap.TryGetValue(targetStoreId, out var targetSetting))
|
||||||
|
{
|
||||||
|
targetSetting = new StoreDeliverySetting
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreDeliverySettings.AddAsync(targetSetting, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
targetSetting.Mode = sourceSetting?.Mode ?? StoreDeliveryMode.Radius;
|
||||||
|
targetSetting.EtaAdjustmentMinutes = sourceSetting?.EtaAdjustmentMinutes ?? 10;
|
||||||
|
targetSetting.FreeDeliveryThreshold = sourceSetting?.FreeDeliveryThreshold ?? 30m;
|
||||||
|
targetSetting.HourlyCapacityLimit = sourceSetting?.HourlyCapacityLimit ?? 50;
|
||||||
|
targetSetting.MaxDeliveryDistance = sourceSetting?.MaxDeliveryDistance ?? 5m;
|
||||||
|
targetSetting.RadiusTiersJson = sourceSetting?.RadiusTiersJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetZones = await dbContext.StoreDeliveryZones
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
dbContext.StoreDeliveryZones.RemoveRange(targetZones);
|
||||||
|
|
||||||
|
var clonedZones = accessibleTargetIds
|
||||||
|
.SelectMany(targetStoreId => sourceZones.Select(zone => new StoreDeliveryZone
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId,
|
||||||
|
ZoneName = zone.ZoneName,
|
||||||
|
PolygonGeoJson = zone.PolygonGeoJson,
|
||||||
|
MinimumOrderAmount = zone.MinimumOrderAmount,
|
||||||
|
DeliveryFee = zone.DeliveryFee,
|
||||||
|
EstimatedMinutes = zone.EstimatedMinutes,
|
||||||
|
Color = zone.Color,
|
||||||
|
Priority = zone.Priority,
|
||||||
|
SortOrder = zone.SortOrder
|
||||||
|
}))
|
||||||
|
.ToList();
|
||||||
|
if (clonedZones.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StoreDeliveryZones.AddRangeAsync(clonedZones, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<CopyStoreDeliverySettingsResult>.Ok(new CopyStoreDeliverySettingsResult
|
||||||
|
{
|
||||||
|
CopiedCount = accessibleTargetIds.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<RadiusTierDto> ParseRadiusTiers(string? raw)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(raw))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "tier-1",
|
||||||
|
MinDistance = 0m,
|
||||||
|
MaxDistance = 1m,
|
||||||
|
DeliveryFee = 3m,
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return (source ?? [])
|
||||||
|
.Select((tier, index) =>
|
||||||
|
{
|
||||||
|
var minDistance = Math.Max(0m, tier.MinDistance);
|
||||||
|
var maxDistance = Math.Max(minDistance + 0.01m, tier.MaxDistance);
|
||||||
|
return new RadiusTierDto
|
||||||
|
{
|
||||||
|
Id = string.IsNullOrWhiteSpace(tier.Id) ? $"tier-{index + 1}" : tier.Id,
|
||||||
|
MinDistance = minDistance,
|
||||||
|
MaxDistance = maxDistance,
|
||||||
|
DeliveryFee = Math.Max(0m, tier.DeliveryFee),
|
||||||
|
EtaMinutes = Math.Max(1, tier.EtaMinutes),
|
||||||
|
MinOrderAmount = Math.Max(0m, tier.MinOrderAmount),
|
||||||
|
Color = string.IsNullOrWhiteSpace(tier.Color) ? "#1677ff" : tier.Color
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.OrderBy(x => x.MinDistance)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PolygonZoneDto MapPolygonZone(StoreDeliveryZone source)
|
||||||
|
{
|
||||||
|
return new PolygonZoneDto
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
Name = source.ZoneName,
|
||||||
|
Color = source.Color ?? "#1677ff",
|
||||||
|
DeliveryFee = source.DeliveryFee ?? 0m,
|
||||||
|
EtaMinutes = source.EstimatedMinutes ?? 20,
|
||||||
|
MinOrderAmount = source.MinimumOrderAmount ?? 0m,
|
||||||
|
Priority = source.Priority
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Services;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
using TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店堂食管理模块。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/store")]
|
||||||
|
public sealed class StoreDineInController(
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店堂食设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("dinein")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreDineInSettingsDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreDineInSettingsDto>> Get([FromQuery] string storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var basic = await dbContext.StoreDineInSettings
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == parsedStoreId, cancellationToken);
|
||||||
|
var areas = await dbContext.StoreTableAreas
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.Name)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var tables = await dbContext.StoreTables
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.OrderBy(x => x.TableCode)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoreDineInSettingsDto>.Ok(new StoreDineInSettingsDto
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId.ToString(),
|
||||||
|
BasicSettings = new DineInBasicSettingsDto
|
||||||
|
{
|
||||||
|
Enabled = basic?.Enabled ?? true,
|
||||||
|
DefaultDiningMinutes = basic?.DefaultDiningMinutes ?? 90,
|
||||||
|
OvertimeReminderMinutes = basic?.OvertimeReminderMinutes ?? 10
|
||||||
|
},
|
||||||
|
Areas = areas.Select(MapArea).ToList(),
|
||||||
|
Tables = tables.Select(MapTable).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存门店堂食基础设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("dinein/basic/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> SaveBasic([FromBody] SaveStoreDineInBasicSettingsRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var basic = await dbContext.StoreDineInSettings
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == parsedStoreId, cancellationToken);
|
||||||
|
if (basic is null)
|
||||||
|
{
|
||||||
|
basic = new StoreDineInSetting
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreDineInSettings.AddAsync(basic, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
basic.Enabled = request.BasicSettings.Enabled;
|
||||||
|
basic.DefaultDiningMinutes = Math.Clamp(request.BasicSettings.DefaultDiningMinutes, 1, 999);
|
||||||
|
basic.OvertimeReminderMinutes = Math.Clamp(request.BasicSettings.OvertimeReminderMinutes, 0, 999);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存堂食区域。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("dinein/area/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<DineInAreaDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<DineInAreaDto>> SaveArea([FromBody] SaveDineInAreaRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var areaName = request.Area.Name?.Trim() ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(areaName))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "区域名称不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
var areaId = StoreApiHelpers.ParseSnowflakeOrNull(request.Area.Id);
|
||||||
|
StoreTableArea? area = null;
|
||||||
|
if (areaId.HasValue)
|
||||||
|
{
|
||||||
|
area = await dbContext.StoreTableAreas.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == areaId.Value && x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (area is null)
|
||||||
|
{
|
||||||
|
area = new StoreTableArea
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreTableAreas.AddAsync(area, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
area.Name = areaName;
|
||||||
|
area.Description = request.Area.Description?.Trim();
|
||||||
|
area.SortOrder = Math.Max(1, request.Area.Sort);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<DineInAreaDto>.Ok(MapArea(area));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除堂食区域。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("dinein/area/delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> DeleteArea([FromBody] DeleteDineInAreaRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var parsedAreaId = StoreApiHelpers.ParseRequiredSnowflake(request.AreaId, "areaId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var hasTables = await dbContext.StoreTables
|
||||||
|
.AnyAsync(x => x.TenantId == tenantId && x.StoreId == parsedStoreId && x.AreaId == parsedAreaId, cancellationToken);
|
||||||
|
if (hasTables)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "该区域仍有桌位,请先迁移或删除桌位");
|
||||||
|
}
|
||||||
|
|
||||||
|
var area = await dbContext.StoreTableAreas.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == parsedAreaId && x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken);
|
||||||
|
if (area is null)
|
||||||
|
{
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.StoreTableAreas.Remove(area);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存堂食桌位。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("dinein/table/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<DineInTableDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<DineInTableDto>> SaveTable([FromBody] SaveDineInTableRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var parsedAreaId = StoreApiHelpers.ParseRequiredSnowflake(request.Table.AreaId, "table.areaId");
|
||||||
|
var areaExists = await dbContext.StoreTableAreas
|
||||||
|
.AnyAsync(x => x.Id == parsedAreaId && x.TenantId == tenantId && x.StoreId == parsedStoreId, cancellationToken);
|
||||||
|
if (!areaExists)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "区域不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableCode = (request.Table.Code ?? string.Empty).Trim().ToUpperInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(tableCode))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "桌位编号不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableId = StoreApiHelpers.ParseSnowflakeOrNull(request.Table.Id);
|
||||||
|
StoreTable? table = null;
|
||||||
|
if (tableId.HasValue)
|
||||||
|
{
|
||||||
|
table = await dbContext.StoreTables.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == tableId.Value && x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicateCode = await dbContext.StoreTables.AnyAsync(
|
||||||
|
x => x.TenantId == tenantId
|
||||||
|
&& x.StoreId == parsedStoreId
|
||||||
|
&& x.TableCode == tableCode
|
||||||
|
&& (!tableId.HasValue || x.Id != tableId.Value),
|
||||||
|
cancellationToken);
|
||||||
|
if (duplicateCode)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, "桌位编号已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (table is null)
|
||||||
|
{
|
||||||
|
table = new StoreTable
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreTables.AddAsync(table, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.AreaId = parsedAreaId;
|
||||||
|
table.TableCode = tableCode;
|
||||||
|
table.Capacity = Math.Clamp(request.Table.Seats, 1, 20);
|
||||||
|
table.Status = StoreApiHelpers.ToTableStatus(request.Table.Status);
|
||||||
|
table.Tags = string.Join(',', (request.Table.Tags ?? [])
|
||||||
|
.Select(x => x.Trim())
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Distinct());
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<DineInTableDto>.Ok(MapTable(table));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除堂食桌位。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("dinein/table/delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> DeleteTable([FromBody] DeleteDineInTableRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var parsedTableId = StoreApiHelpers.ParseRequiredSnowflake(request.TableId, "tableId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var table = await dbContext.StoreTables.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == parsedTableId && x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken);
|
||||||
|
if (table is null)
|
||||||
|
{
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.StoreTables.Remove(table);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量生成堂食桌位。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("dinein/table/batch-create")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<BatchCreateDineInTablesResultDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<BatchCreateDineInTablesResultDto>> BatchCreateTables(
|
||||||
|
[FromBody] BatchCreateDineInTablesRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var parsedAreaId = StoreApiHelpers.ParseRequiredSnowflake(request.AreaId, "areaId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var areaExists = await dbContext.StoreTableAreas
|
||||||
|
.AnyAsync(x => x.Id == parsedAreaId && x.TenantId == tenantId && x.StoreId == parsedStoreId, cancellationToken);
|
||||||
|
if (!areaExists)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "区域不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = Math.Clamp(request.Count, 1, 50);
|
||||||
|
var startNumber = Math.Clamp(request.StartNumber, 1, 9999);
|
||||||
|
var seats = Math.Clamp(request.Seats, 1, 20);
|
||||||
|
var prefix = string.IsNullOrWhiteSpace(request.CodePrefix) ? "A" : request.CodePrefix.Trim().ToUpperInvariant();
|
||||||
|
var width = Math.Max(2, (startNumber + count - 1).ToString().Length);
|
||||||
|
|
||||||
|
var existingCodes = await dbContext.StoreTables
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.Select(x => x.TableCode)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var existingCodeSet = new HashSet<string>(existingCodes, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var created = new List<StoreTable>();
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var code = $"{prefix}{(startNumber + i).ToString().PadLeft(width, '0')}";
|
||||||
|
if (existingCodeSet.Contains(code))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var table = new StoreTable
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId,
|
||||||
|
AreaId = parsedAreaId,
|
||||||
|
TableCode = code,
|
||||||
|
Capacity = seats,
|
||||||
|
Status = StoreApiHelpers.ToTableStatus("free"),
|
||||||
|
Tags = null
|
||||||
|
};
|
||||||
|
created.Add(table);
|
||||||
|
existingCodeSet.Add(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (created.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StoreTables.AddRangeAsync(created, cancellationToken);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<BatchCreateDineInTablesResultDto>.Ok(new BatchCreateDineInTablesResultDto
|
||||||
|
{
|
||||||
|
CreatedTables = created.Select(MapTable).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制堂食设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("dinein/copy")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<CopyStoreDineInSettingsResult>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<CopyStoreDineInSettingsResult>> Copy([FromBody] CopyStoreDineInSettingsRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sourceStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.SourceStoreId, "sourceStoreId");
|
||||||
|
var targetStoreIds = StoreApiHelpers.ParseSnowflakeList(request.TargetStoreIds);
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, sourceStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var accessibleTargetIds = await StoreApiHelpers.FilterAccessibleStoreIdsAsync(
|
||||||
|
dbContext,
|
||||||
|
tenantId,
|
||||||
|
merchantId,
|
||||||
|
targetStoreIds,
|
||||||
|
cancellationToken);
|
||||||
|
accessibleTargetIds.Remove(sourceStoreId);
|
||||||
|
|
||||||
|
if (accessibleTargetIds.Count == 0)
|
||||||
|
{
|
||||||
|
return ApiResponse<CopyStoreDineInSettingsResult>.Ok(new CopyStoreDineInSettingsResult
|
||||||
|
{
|
||||||
|
CopiedCount = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceBasic = await dbContext.StoreDineInSettings
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == sourceStoreId, cancellationToken);
|
||||||
|
var sourceAreas = await dbContext.StoreTableAreas
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var sourceTables = await dbContext.StoreTables
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
|
||||||
|
.OrderBy(x => x.TableCode)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
foreach (var targetStoreId in accessibleTargetIds)
|
||||||
|
{
|
||||||
|
var targetBasic = await dbContext.StoreDineInSettings
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == targetStoreId, cancellationToken);
|
||||||
|
if (targetBasic is null)
|
||||||
|
{
|
||||||
|
targetBasic = new StoreDineInSetting
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreDineInSettings.AddAsync(targetBasic, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
targetBasic.Enabled = sourceBasic?.Enabled ?? true;
|
||||||
|
targetBasic.DefaultDiningMinutes = sourceBasic?.DefaultDiningMinutes ?? 90;
|
||||||
|
targetBasic.OvertimeReminderMinutes = sourceBasic?.OvertimeReminderMinutes ?? 10;
|
||||||
|
|
||||||
|
var targetTables = await dbContext.StoreTables
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == targetStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var targetAreas = await dbContext.StoreTableAreas
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == targetStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
dbContext.StoreTables.RemoveRange(targetTables);
|
||||||
|
dbContext.StoreTableAreas.RemoveRange(targetAreas);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
var createdAreas = sourceAreas.Select(area => new StoreTableArea
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId,
|
||||||
|
Name = area.Name,
|
||||||
|
Description = area.Description,
|
||||||
|
SortOrder = area.SortOrder
|
||||||
|
}).ToList();
|
||||||
|
if (createdAreas.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StoreTableAreas.AddRangeAsync(createdAreas, cancellationToken);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var areaIdMap = new Dictionary<long, long>();
|
||||||
|
for (var index = 0; index < sourceAreas.Count && index < createdAreas.Count; index++)
|
||||||
|
{
|
||||||
|
areaIdMap[sourceAreas[index].Id] = createdAreas[index].Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdTables = sourceTables.Select(table => new StoreTable
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId,
|
||||||
|
AreaId = table.AreaId.HasValue && areaIdMap.TryGetValue(table.AreaId.Value, out var mappedAreaId)
|
||||||
|
? mappedAreaId
|
||||||
|
: null,
|
||||||
|
TableCode = table.TableCode,
|
||||||
|
Capacity = table.Capacity,
|
||||||
|
Status = table.Status,
|
||||||
|
Tags = table.Tags
|
||||||
|
}).ToList();
|
||||||
|
if (createdTables.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StoreTables.AddRangeAsync(createdTables, cancellationToken);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<CopyStoreDineInSettingsResult>.Ok(new CopyStoreDineInSettingsResult
|
||||||
|
{
|
||||||
|
CopiedCount = accessibleTargetIds.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DineInAreaDto MapArea(StoreTableArea source)
|
||||||
|
{
|
||||||
|
return new DineInAreaDto
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
Name = source.Name,
|
||||||
|
Description = source.Description ?? string.Empty,
|
||||||
|
Sort = source.SortOrder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DineInTableDto MapTable(StoreTable source)
|
||||||
|
{
|
||||||
|
return new DineInTableDto
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
AreaId = source.AreaId?.ToString() ?? string.Empty,
|
||||||
|
Code = source.TableCode,
|
||||||
|
Seats = source.Capacity,
|
||||||
|
Status = StoreApiHelpers.ToTableStatusText(source.Status),
|
||||||
|
Tags = string.IsNullOrWhiteSpace(source.Tags)
|
||||||
|
? []
|
||||||
|
: source.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
220
src/Api/TakeoutSaaS.TenantApi/Controllers/StoreFeesController.cs
Normal file
220
src/Api/TakeoutSaaS.TenantApi/Controllers/StoreFeesController.cs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Queries;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Services;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
using TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店费用设置模块。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/store")]
|
||||||
|
public sealed class StoreFeesController(
|
||||||
|
IMediator mediator,
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店费用设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("fees")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreFeesSettingsDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreFeesSettingsDto>> Get([FromQuery] string storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var fee = await mediator.Send(new GetStoreFeeQuery
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
var response = MapFeeSettings(parsedStoreId, fee);
|
||||||
|
return ApiResponse<StoreFeesSettingsDto>.Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存门店费用设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("fees/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreFeesSettingsDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreFeesSettingsDto>> Save([FromBody] StoreFeesSettingsDto request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new UpdateStoreFeeCommand
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId,
|
||||||
|
MinimumOrderAmount = request.MinimumOrderAmount,
|
||||||
|
DeliveryFee = request.BaseDeliveryFee,
|
||||||
|
FreeDeliveryThreshold = request.FreeDeliveryThreshold,
|
||||||
|
PackagingFeeMode = ParsePackagingFeeMode(request.PackagingFeeMode),
|
||||||
|
OrderPackagingFeeMode = ParseOrderPackagingFeeMode(request.OrderPackagingFeeMode),
|
||||||
|
FixedPackagingFee = request.FixedPackagingFee,
|
||||||
|
PackagingFeeTiers = (request.PackagingFeeTiers ?? [])
|
||||||
|
.OrderBy(x => x.Sort)
|
||||||
|
.ThenBy(x => x.MinAmount)
|
||||||
|
.Select(x => new StoreFeeTierDto
|
||||||
|
{
|
||||||
|
MinPrice = x.MinAmount,
|
||||||
|
MaxPrice = x.MaxAmount,
|
||||||
|
Fee = x.Fee
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
CutleryFeeEnabled = request.OtherFees.Cutlery.Enabled,
|
||||||
|
CutleryFeeAmount = request.OtherFees.Cutlery.Amount,
|
||||||
|
RushFeeEnabled = request.OtherFees.Rush.Enabled,
|
||||||
|
RushFeeAmount = request.OtherFees.Rush.Amount
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoreFeesSettingsDto>.Ok(MapFeeSettings(parsedStoreId, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制费用设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("fees/copy")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<CopyStoreFeesSettingsResult>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<CopyStoreFeesSettingsResult>> Copy([FromBody] CopyStoreFeesSettingsRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sourceStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.SourceStoreId, "sourceStoreId");
|
||||||
|
var targetStoreIds = StoreApiHelpers.ParseSnowflakeList(request.TargetStoreIds);
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, sourceStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var accessibleTargetIds = await StoreApiHelpers.FilterAccessibleStoreIdsAsync(
|
||||||
|
dbContext,
|
||||||
|
tenantId,
|
||||||
|
merchantId,
|
||||||
|
targetStoreIds,
|
||||||
|
cancellationToken);
|
||||||
|
accessibleTargetIds.Remove(sourceStoreId);
|
||||||
|
|
||||||
|
if (accessibleTargetIds.Count == 0)
|
||||||
|
{
|
||||||
|
return ApiResponse<CopyStoreFeesSettingsResult>.Ok(new CopyStoreFeesSettingsResult
|
||||||
|
{
|
||||||
|
CopiedCount = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceFee = await dbContext.StoreFees
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == sourceStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var targetFees = await dbContext.StoreFees
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var targetFeeMap = targetFees.ToDictionary(x => x.StoreId);
|
||||||
|
|
||||||
|
foreach (var targetStoreId in accessibleTargetIds)
|
||||||
|
{
|
||||||
|
if (!targetFeeMap.TryGetValue(targetStoreId, out var targetFee))
|
||||||
|
{
|
||||||
|
targetFee = new StoreFee
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreFees.AddAsync(targetFee, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFee.MinimumOrderAmount = sourceFee?.MinimumOrderAmount ?? 0m;
|
||||||
|
targetFee.BaseDeliveryFee = sourceFee?.BaseDeliveryFee ?? 0m;
|
||||||
|
targetFee.FreeDeliveryThreshold = sourceFee?.FreeDeliveryThreshold;
|
||||||
|
targetFee.PackagingFeeMode = sourceFee?.PackagingFeeMode ?? PackagingFeeMode.Fixed;
|
||||||
|
targetFee.OrderPackagingFeeMode = sourceFee?.OrderPackagingFeeMode ?? OrderPackagingFeeMode.Fixed;
|
||||||
|
targetFee.FixedPackagingFee = sourceFee?.FixedPackagingFee ?? 0m;
|
||||||
|
targetFee.PackagingFeeTiersJson = sourceFee?.PackagingFeeTiersJson;
|
||||||
|
targetFee.CutleryFeeEnabled = sourceFee?.CutleryFeeEnabled ?? false;
|
||||||
|
targetFee.CutleryFeeAmount = sourceFee?.CutleryFeeAmount ?? 0m;
|
||||||
|
targetFee.RushFeeEnabled = sourceFee?.RushFeeEnabled ?? false;
|
||||||
|
targetFee.RushFeeAmount = sourceFee?.RushFeeAmount ?? 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<CopyStoreFeesSettingsResult>.Ok(new CopyStoreFeesSettingsResult
|
||||||
|
{
|
||||||
|
CopiedCount = accessibleTargetIds.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoreFeesSettingsDto MapFeeSettings(long storeId, StoreFeeDto? source)
|
||||||
|
{
|
||||||
|
var tiers = (source?.PackagingFeeTiers ?? [])
|
||||||
|
.OrderBy(x => x.MinPrice)
|
||||||
|
.Select((tier, index) => new PackagingFeeTierDto
|
||||||
|
{
|
||||||
|
Id = $"tier-{index + 1}",
|
||||||
|
MinAmount = tier.MinPrice,
|
||||||
|
MaxAmount = tier.MaxPrice,
|
||||||
|
Fee = tier.Fee,
|
||||||
|
Sort = index + 1
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new StoreFeesSettingsDto
|
||||||
|
{
|
||||||
|
StoreId = storeId.ToString(),
|
||||||
|
MinimumOrderAmount = source?.MinimumOrderAmount ?? 0m,
|
||||||
|
BaseDeliveryFee = source?.DeliveryFee ?? 0m,
|
||||||
|
FreeDeliveryThreshold = source?.FreeDeliveryThreshold,
|
||||||
|
PackagingFeeMode = ToPackagingFeeModeText(source?.PackagingFeeMode ?? PackagingFeeMode.Fixed),
|
||||||
|
OrderPackagingFeeMode = ToOrderPackagingFeeModeText(source?.OrderPackagingFeeMode ?? OrderPackagingFeeMode.Fixed),
|
||||||
|
FixedPackagingFee = source?.FixedPackagingFee ?? 0m,
|
||||||
|
PackagingFeeTiers = tiers,
|
||||||
|
OtherFees = new StoreOtherFeesDto
|
||||||
|
{
|
||||||
|
Cutlery = new AdditionalFeeItemDto
|
||||||
|
{
|
||||||
|
Enabled = source?.CutleryFeeEnabled ?? false,
|
||||||
|
Amount = source?.CutleryFeeAmount ?? 0m
|
||||||
|
},
|
||||||
|
Rush = new AdditionalFeeItemDto
|
||||||
|
{
|
||||||
|
Enabled = source?.RushFeeEnabled ?? false,
|
||||||
|
Amount = source?.RushFeeAmount ?? 0m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PackagingFeeMode ParsePackagingFeeMode(string? value)
|
||||||
|
{
|
||||||
|
return string.Equals(value, "item", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? PackagingFeeMode.PerItem
|
||||||
|
: PackagingFeeMode.Fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToPackagingFeeModeText(PackagingFeeMode value)
|
||||||
|
{
|
||||||
|
return value == PackagingFeeMode.PerItem ? "item" : "order";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrderPackagingFeeMode ParseOrderPackagingFeeMode(string? value)
|
||||||
|
{
|
||||||
|
return string.Equals(value, "tiered", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? OrderPackagingFeeMode.Tiered
|
||||||
|
: OrderPackagingFeeMode.Fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToOrderPackagingFeeModeText(OrderPackagingFeeMode value)
|
||||||
|
{
|
||||||
|
return value == OrderPackagingFeeMode.Tiered ? "tiered" : "fixed";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Services;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
using TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店营业时间模块。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/store")]
|
||||||
|
public sealed class StoreHoursController(
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店营业时间。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("hours")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreHoursDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreHoursDto>> Get([FromQuery] string storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var hours = await dbContext.StoreBusinessHours
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.OrderBy(x => x.DayOfWeek)
|
||||||
|
.ThenBy(x => x.StartTime)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var holidays = await dbContext.StoreHolidays
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.OrderBy(x => x.Date)
|
||||||
|
.ThenBy(x => x.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var weeklyHours = Enumerable.Range(0, 7)
|
||||||
|
.Select(day =>
|
||||||
|
{
|
||||||
|
var slots = hours
|
||||||
|
.Where(x => StoreApiHelpers.DotNetDayOfWeekToUi(x.DayOfWeek) == day)
|
||||||
|
.Select(MapSlot)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new StoreHourDayHoursDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
IsOpen = slots.Count > 0,
|
||||||
|
Slots = slots
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var result = new StoreHoursDto
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId.ToString(),
|
||||||
|
WeeklyHours = weeklyHours,
|
||||||
|
Holidays = holidays.Select(MapHoliday).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
return ApiResponse<StoreHoursDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存每周营业时间。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("hours/weekly")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> SaveWeekly([FromBody] SaveWeeklyHoursRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var existingHours = await dbContext.StoreBusinessHours
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
dbContext.StoreBusinessHours.RemoveRange(existingHours);
|
||||||
|
|
||||||
|
var toCreate = new List<StoreBusinessHour>();
|
||||||
|
foreach (var day in request.WeeklyHours ?? [])
|
||||||
|
{
|
||||||
|
if (day.DayOfWeek is < 0 or > 6 || !day.IsOpen)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var slot in day.Slots ?? [])
|
||||||
|
{
|
||||||
|
var startTime = StoreApiHelpers.ParseRequiredTime(slot.StartTime, "slot.startTime");
|
||||||
|
var endTime = StoreApiHelpers.ParseRequiredTime(slot.EndTime, "slot.endTime");
|
||||||
|
if (startTime >= endTime)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "营业时段开始时间必须早于结束时间");
|
||||||
|
}
|
||||||
|
|
||||||
|
var hourType = StoreApiHelpers.ToBusinessHourType(slot.Type);
|
||||||
|
toCreate.Add(new StoreBusinessHour
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId,
|
||||||
|
DayOfWeek = StoreApiHelpers.UiDayOfWeekToDotNet(day.DayOfWeek),
|
||||||
|
HourType = hourType,
|
||||||
|
StartTime = startTime,
|
||||||
|
EndTime = endTime,
|
||||||
|
CapacityLimit = hourType == BusinessHourType.PickupOrDelivery ? slot.Capacity : null,
|
||||||
|
Notes = string.IsNullOrWhiteSpace(slot.Remark) ? null : slot.Remark.Trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toCreate.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StoreBusinessHours.AddRangeAsync(toCreate, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存特殊日期。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("hours/holiday")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreHourHolidayDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreHourHolidayDto>> SaveHoliday([FromBody] SaveHolidayRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var holidayInput = request.Holiday;
|
||||||
|
var startDate = StoreApiHelpers.ParseDateOnly(holidayInput.StartDate, "holiday.startDate");
|
||||||
|
var endDate = StoreApiHelpers.ParseDateOnly(string.IsNullOrWhiteSpace(holidayInput.EndDate) ? holidayInput.StartDate : holidayInput.EndDate, "holiday.endDate");
|
||||||
|
if (startDate > endDate)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "holiday.startDate 不能晚于 holiday.endDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = holidayInput.Type == 2 ? StoreHolidayType.Special : StoreHolidayType.Closed;
|
||||||
|
var hasTimeRange = type == StoreHolidayType.Special
|
||||||
|
&& !string.IsNullOrWhiteSpace(holidayInput.StartTime)
|
||||||
|
&& !string.IsNullOrWhiteSpace(holidayInput.EndTime);
|
||||||
|
|
||||||
|
var startTime = hasTimeRange ? StoreApiHelpers.ParseRequiredTime(holidayInput.StartTime, "holiday.startTime") : (TimeSpan?)null;
|
||||||
|
var endTime = hasTimeRange ? StoreApiHelpers.ParseRequiredTime(holidayInput.EndTime, "holiday.endTime") : (TimeSpan?)null;
|
||||||
|
if (hasTimeRange && startTime >= endTime)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "holiday.startTime 必须早于 holiday.endTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
var holidayId = StoreApiHelpers.ParseSnowflakeOrNull(holidayInput.Id);
|
||||||
|
var entity = holidayId.HasValue
|
||||||
|
? await dbContext.StoreHolidays.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == holidayId.Value && x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (entity is null)
|
||||||
|
{
|
||||||
|
entity = new StoreHoliday
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreHolidays.AddAsync(entity, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.Date = startDate;
|
||||||
|
entity.EndDate = endDate;
|
||||||
|
entity.IsAllDay = !hasTimeRange;
|
||||||
|
entity.StartTime = hasTimeRange ? startTime : null;
|
||||||
|
entity.EndTime = hasTimeRange ? endTime : null;
|
||||||
|
entity.OverrideType = type == StoreHolidayType.Closed
|
||||||
|
? OverrideType.Closed
|
||||||
|
: hasTimeRange
|
||||||
|
? OverrideType.ModifiedHours
|
||||||
|
: OverrideType.TemporaryOpen;
|
||||||
|
entity.IsClosed = type == StoreHolidayType.Closed;
|
||||||
|
entity.Reason = string.IsNullOrWhiteSpace(holidayInput.Reason) ? null : holidayInput.Reason.Trim();
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
var result = MapHoliday(entity);
|
||||||
|
result.Remark = holidayInput.Remark;
|
||||||
|
return ApiResponse<StoreHourHolidayDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除特殊日期。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("hours/holiday/delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> DeleteHoliday([FromBody] DeleteHolidayRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var holidayId = StoreApiHelpers.ParseRequiredSnowflake(request.Id, "id");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
|
||||||
|
var holiday = await dbContext.StoreHolidays
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == holidayId && x.TenantId == tenantId, cancellationToken);
|
||||||
|
|
||||||
|
if (holiday is null)
|
||||||
|
{
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAccess = await dbContext.Stores
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(x => x.Id == holiday.StoreId && x.TenantId == tenantId && x.MerchantId == merchantId, cancellationToken);
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, "特殊日期不存在或无权限访问");
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.StoreHolidays.Remove(holiday);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制营业时间到其他门店。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("hours/copy")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<CopyStoreHoursResult>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<CopyStoreHoursResult>> Copy([FromBody] CopyStoreHoursRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sourceStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.SourceStoreId, "sourceStoreId");
|
||||||
|
var targetStoreIds = StoreApiHelpers.ParseSnowflakeList(request.TargetStoreIds);
|
||||||
|
var includeWeeklyHours = request.IncludeWeeklyHours ?? true;
|
||||||
|
var includeHolidays = request.IncludeHolidays ?? true;
|
||||||
|
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, sourceStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var accessibleTargetIds = await StoreApiHelpers.FilterAccessibleStoreIdsAsync(
|
||||||
|
dbContext,
|
||||||
|
tenantId,
|
||||||
|
merchantId,
|
||||||
|
targetStoreIds,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
accessibleTargetIds.Remove(sourceStoreId);
|
||||||
|
if (accessibleTargetIds.Count == 0)
|
||||||
|
{
|
||||||
|
return ApiResponse<CopyStoreHoursResult>.Ok(new CopyStoreHoursResult
|
||||||
|
{
|
||||||
|
CopiedCount = 0,
|
||||||
|
IncludeWeeklyHours = includeWeeklyHours,
|
||||||
|
IncludeHolidays = includeHolidays
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeWeeklyHours)
|
||||||
|
{
|
||||||
|
var sourceHours = await dbContext.StoreBusinessHours
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var targetHours = await dbContext.StoreBusinessHours
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
dbContext.StoreBusinessHours.RemoveRange(targetHours);
|
||||||
|
|
||||||
|
var clonedHours = accessibleTargetIds
|
||||||
|
.SelectMany(targetStoreId => sourceHours.Select(hour => new StoreBusinessHour
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId,
|
||||||
|
DayOfWeek = hour.DayOfWeek,
|
||||||
|
HourType = hour.HourType,
|
||||||
|
StartTime = hour.StartTime,
|
||||||
|
EndTime = hour.EndTime,
|
||||||
|
CapacityLimit = hour.CapacityLimit,
|
||||||
|
Notes = hour.Notes
|
||||||
|
}))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (clonedHours.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StoreBusinessHours.AddRangeAsync(clonedHours, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeHolidays)
|
||||||
|
{
|
||||||
|
var sourceHolidays = await dbContext.StoreHolidays
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var targetHolidays = await dbContext.StoreHolidays
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
dbContext.StoreHolidays.RemoveRange(targetHolidays);
|
||||||
|
|
||||||
|
var clonedHolidays = accessibleTargetIds
|
||||||
|
.SelectMany(targetStoreId => sourceHolidays.Select(holiday => new StoreHoliday
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId,
|
||||||
|
Date = holiday.Date,
|
||||||
|
EndDate = holiday.EndDate,
|
||||||
|
IsAllDay = holiday.IsAllDay,
|
||||||
|
StartTime = holiday.StartTime,
|
||||||
|
EndTime = holiday.EndTime,
|
||||||
|
OverrideType = holiday.OverrideType,
|
||||||
|
IsClosed = holiday.IsClosed,
|
||||||
|
Reason = holiday.Reason
|
||||||
|
}))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (clonedHolidays.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StoreHolidays.AddRangeAsync(clonedHolidays, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<CopyStoreHoursResult>.Ok(new CopyStoreHoursResult
|
||||||
|
{
|
||||||
|
CopiedCount = accessibleTargetIds.Count,
|
||||||
|
IncludeWeeklyHours = includeWeeklyHours,
|
||||||
|
IncludeHolidays = includeHolidays
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoreHourTimeSlotDto MapSlot(StoreBusinessHour source)
|
||||||
|
{
|
||||||
|
return new StoreHourTimeSlotDto
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
Type = StoreApiHelpers.ToSlotType(source.HourType),
|
||||||
|
StartTime = StoreApiHelpers.ToHHmm(source.StartTime),
|
||||||
|
EndTime = StoreApiHelpers.ToHHmm(source.EndTime),
|
||||||
|
Capacity = source.CapacityLimit,
|
||||||
|
Remark = source.Notes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoreHourHolidayDto MapHoliday(StoreHoliday source)
|
||||||
|
{
|
||||||
|
var type = source.OverrideType == OverrideType.Closed || source.IsClosed
|
||||||
|
? StoreHolidayType.Closed
|
||||||
|
: StoreHolidayType.Special;
|
||||||
|
|
||||||
|
return new StoreHourHolidayDto
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
StartDate = StoreApiHelpers.ToDateOnly(source.Date),
|
||||||
|
EndDate = StoreApiHelpers.ToDateOnly(source.EndDate ?? source.Date),
|
||||||
|
Type = (int)type,
|
||||||
|
StartTime = type == StoreHolidayType.Special ? StoreApiHelpers.ToHHmm(source.StartTime) : null,
|
||||||
|
EndTime = type == StoreHolidayType.Special ? StoreApiHelpers.ToHHmm(source.EndTime) : null,
|
||||||
|
Reason = source.Reason ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum StoreHolidayType
|
||||||
|
{
|
||||||
|
Closed = 1,
|
||||||
|
Special = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Services;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
using TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店自提设置模块。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/store")]
|
||||||
|
public sealed class StorePickupController(
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店自提设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("pickup")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StorePickupSettingsDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StorePickupSettingsDto>> Get([FromQuery] string storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var setting = await dbContext.StorePickupSettings
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var slots = await dbContext.StorePickupSlots
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.OrderBy(x => x.StartTime)
|
||||||
|
.ThenBy(x => x.Name)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var fineRule = ParseFineRule(setting?.FineRuleJson);
|
||||||
|
var previewDays = BuildPreviewDays(fineRule);
|
||||||
|
|
||||||
|
var response = new StorePickupSettingsDto
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId.ToString(),
|
||||||
|
Mode = StoreApiHelpers.ToPickupModeText(setting?.Mode ?? StorePickupMode.Big),
|
||||||
|
BasicSettings = new PickupBasicSettingsDto
|
||||||
|
{
|
||||||
|
AllowSameDayPickup = setting?.AllowToday ?? true,
|
||||||
|
BookingDays = setting?.AllowDaysAhead ?? 3,
|
||||||
|
MaxItemsPerOrder = setting?.MaxQuantityPerOrder ?? 20
|
||||||
|
},
|
||||||
|
BigSlots = slots.Select(slot => new PickupSlotDto
|
||||||
|
{
|
||||||
|
Id = slot.Id.ToString(),
|
||||||
|
Name = slot.Name,
|
||||||
|
StartTime = StoreApiHelpers.ToHHmm(slot.StartTime),
|
||||||
|
EndTime = StoreApiHelpers.ToHHmm(slot.EndTime),
|
||||||
|
CutoffMinutes = slot.CutoffMinutes,
|
||||||
|
Capacity = slot.Capacity,
|
||||||
|
ReservedCount = slot.ReservedCount,
|
||||||
|
DayOfWeeks = StoreApiHelpers.DeserializeWeekdays(slot.Weekdays),
|
||||||
|
Enabled = slot.IsEnabled
|
||||||
|
}).ToList(),
|
||||||
|
FineRule = fineRule,
|
||||||
|
PreviewDays = previewDays
|
||||||
|
};
|
||||||
|
|
||||||
|
return ApiResponse<StorePickupSettingsDto>.Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存自提基础设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("pickup/basic/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> SaveBasic([FromBody] SavePickupBasicSettingsRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var setting = await EnsurePickupSettingAsync(tenantId, parsedStoreId, cancellationToken);
|
||||||
|
setting.AllowToday = request.BasicSettings.AllowSameDayPickup;
|
||||||
|
setting.AllowDaysAhead = Math.Clamp(request.BasicSettings.BookingDays, 1, 30);
|
||||||
|
setting.MaxQuantityPerOrder = request.BasicSettings.MaxItemsPerOrder;
|
||||||
|
setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存自提大时段。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("pickup/slots/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> SaveSlots([FromBody] SavePickupSlotsRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var setting = await EnsurePickupSettingAsync(tenantId, parsedStoreId, cancellationToken);
|
||||||
|
setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode);
|
||||||
|
|
||||||
|
var existingSlots = await dbContext.StorePickupSlots
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
dbContext.StorePickupSlots.RemoveRange(existingSlots);
|
||||||
|
|
||||||
|
var toCreate = new List<StorePickupSlot>();
|
||||||
|
foreach (var slot in request.Slots ?? [])
|
||||||
|
{
|
||||||
|
var startTime = StoreApiHelpers.ParseRequiredTime(slot.StartTime, "slot.startTime");
|
||||||
|
var endTime = StoreApiHelpers.ParseRequiredTime(slot.EndTime, "slot.endTime");
|
||||||
|
if (startTime >= endTime)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var capacity = Math.Max(0, slot.Capacity);
|
||||||
|
toCreate.Add(new StorePickupSlot
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId,
|
||||||
|
Name = string.IsNullOrWhiteSpace(slot.Name) ? "时段" : slot.Name.Trim(),
|
||||||
|
StartTime = startTime,
|
||||||
|
EndTime = endTime,
|
||||||
|
CutoffMinutes = Math.Max(0, slot.CutoffMinutes),
|
||||||
|
Capacity = capacity,
|
||||||
|
ReservedCount = Math.Clamp(slot.ReservedCount, 0, capacity),
|
||||||
|
Weekdays = StoreApiHelpers.SerializeWeekdays(slot.DayOfWeeks),
|
||||||
|
IsEnabled = slot.Enabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toCreate.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StorePickupSlots.AddRangeAsync(toCreate, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存自提精细规则。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("pickup/fine-rule/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> SaveFineRule([FromBody] SavePickupFineRuleRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var setting = await EnsurePickupSettingAsync(tenantId, parsedStoreId, cancellationToken);
|
||||||
|
setting.Mode = request.Mode is null ? setting.Mode : StoreApiHelpers.ToPickupMode(request.Mode);
|
||||||
|
|
||||||
|
var normalizedRule = NormalizeFineRule(request.FineRule);
|
||||||
|
setting.FineRuleJson = JsonSerializer.Serialize(normalizedRule, StoreApiHelpers.JsonOptions);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制自提设置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("pickup/copy")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<CopyStorePickupSettingsResult>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<CopyStorePickupSettingsResult>> Copy([FromBody] CopyStorePickupSettingsRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sourceStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.SourceStoreId, "sourceStoreId");
|
||||||
|
var targetStoreIds = StoreApiHelpers.ParseSnowflakeList(request.TargetStoreIds);
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, sourceStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var accessibleTargetIds = await StoreApiHelpers.FilterAccessibleStoreIdsAsync(
|
||||||
|
dbContext,
|
||||||
|
tenantId,
|
||||||
|
merchantId,
|
||||||
|
targetStoreIds,
|
||||||
|
cancellationToken);
|
||||||
|
accessibleTargetIds.Remove(sourceStoreId);
|
||||||
|
|
||||||
|
if (accessibleTargetIds.Count == 0)
|
||||||
|
{
|
||||||
|
return ApiResponse<CopyStorePickupSettingsResult>.Ok(new CopyStorePickupSettingsResult
|
||||||
|
{
|
||||||
|
CopiedCount = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceSetting = await dbContext.StorePickupSettings
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == sourceStoreId, cancellationToken);
|
||||||
|
var sourceSlots = await dbContext.StorePickupSlots
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var targetSettings = await dbContext.StorePickupSettings
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var targetSettingMap = targetSettings.ToDictionary(x => x.StoreId);
|
||||||
|
|
||||||
|
foreach (var targetStoreId in accessibleTargetIds)
|
||||||
|
{
|
||||||
|
if (!targetSettingMap.TryGetValue(targetStoreId, out var targetSetting))
|
||||||
|
{
|
||||||
|
targetSetting = new StorePickupSetting
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StorePickupSettings.AddAsync(targetSetting, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
targetSetting.AllowToday = sourceSetting?.AllowToday ?? true;
|
||||||
|
targetSetting.AllowDaysAhead = sourceSetting?.AllowDaysAhead ?? 3;
|
||||||
|
targetSetting.DefaultCutoffMinutes = sourceSetting?.DefaultCutoffMinutes ?? 30;
|
||||||
|
targetSetting.MaxQuantityPerOrder = sourceSetting?.MaxQuantityPerOrder ?? 20;
|
||||||
|
targetSetting.Mode = sourceSetting?.Mode ?? StorePickupMode.Big;
|
||||||
|
targetSetting.FineRuleJson = sourceSetting?.FineRuleJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetSlots = await dbContext.StorePickupSlots
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
dbContext.StorePickupSlots.RemoveRange(targetSlots);
|
||||||
|
|
||||||
|
var clonedSlots = accessibleTargetIds
|
||||||
|
.SelectMany(targetStoreId => sourceSlots.Select(slot => new StorePickupSlot
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId,
|
||||||
|
Name = slot.Name,
|
||||||
|
StartTime = slot.StartTime,
|
||||||
|
EndTime = slot.EndTime,
|
||||||
|
CutoffMinutes = slot.CutoffMinutes,
|
||||||
|
Capacity = slot.Capacity,
|
||||||
|
ReservedCount = slot.ReservedCount,
|
||||||
|
Weekdays = slot.Weekdays,
|
||||||
|
IsEnabled = slot.IsEnabled
|
||||||
|
}))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (clonedSlots.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StorePickupSlots.AddRangeAsync(clonedSlots, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<CopyStorePickupSettingsResult>.Ok(new CopyStorePickupSettingsResult
|
||||||
|
{
|
||||||
|
CopiedCount = accessibleTargetIds.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<StorePickupSetting> EnsurePickupSettingAsync(long tenantId, long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var setting = await dbContext.StorePickupSettings
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == storeId, cancellationToken);
|
||||||
|
if (setting is not null)
|
||||||
|
{
|
||||||
|
return setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
setting = new StorePickupSetting
|
||||||
|
{
|
||||||
|
StoreId = storeId
|
||||||
|
};
|
||||||
|
await dbContext.StorePickupSettings.AddAsync(setting, cancellationToken);
|
||||||
|
return setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickupFineRuleDto ParseFineRule(string? raw)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(raw))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JsonSerializer.Deserialize<PickupFineRuleDto>(raw, StoreApiHelpers.JsonOptions);
|
||||||
|
if (parsed is not null)
|
||||||
|
{
|
||||||
|
return NormalizeFineRule(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略反序列化异常,回落默认配置
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PickupFineRuleDto
|
||||||
|
{
|
||||||
|
IntervalMinutes = 30,
|
||||||
|
SlotCapacity = 5,
|
||||||
|
DayStartTime = "09:00",
|
||||||
|
DayEndTime = "20:30",
|
||||||
|
MinAdvanceHours = 2,
|
||||||
|
DayOfWeeks = [0, 1, 2, 3, 4, 5, 6]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickupFineRuleDto NormalizeFineRule(PickupFineRuleDto source)
|
||||||
|
{
|
||||||
|
var start = string.IsNullOrWhiteSpace(source.DayStartTime) ? "09:00" : source.DayStartTime;
|
||||||
|
var end = string.IsNullOrWhiteSpace(source.DayEndTime) ? "20:30" : source.DayEndTime;
|
||||||
|
|
||||||
|
return new PickupFineRuleDto
|
||||||
|
{
|
||||||
|
IntervalMinutes = Math.Clamp(source.IntervalMinutes, 5, 180),
|
||||||
|
SlotCapacity = Math.Clamp(source.SlotCapacity, 1, 999),
|
||||||
|
DayStartTime = StoreApiHelpers.ToHHmm(StoreApiHelpers.ParseRequiredTime(start, "fineRule.dayStartTime")),
|
||||||
|
DayEndTime = StoreApiHelpers.ToHHmm(StoreApiHelpers.ParseRequiredTime(end, "fineRule.dayEndTime")),
|
||||||
|
MinAdvanceHours = Math.Clamp(source.MinAdvanceHours, 0, 72),
|
||||||
|
DayOfWeeks = (source.DayOfWeeks ?? [])
|
||||||
|
.Distinct()
|
||||||
|
.Where(x => x is >= 0 and <= 6)
|
||||||
|
.OrderBy(x => x)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PickupPreviewDayDto> BuildPreviewDays(PickupFineRuleDto fineRule)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var startMinutes = StoreApiHelpers.ParseRequiredTime(fineRule.DayStartTime, "fineRule.dayStartTime").Hours * 60
|
||||||
|
+ StoreApiHelpers.ParseRequiredTime(fineRule.DayStartTime, "fineRule.dayStartTime").Minutes;
|
||||||
|
var endMinutes = StoreApiHelpers.ParseRequiredTime(fineRule.DayEndTime, "fineRule.dayEndTime").Hours * 60
|
||||||
|
+ StoreApiHelpers.ParseRequiredTime(fineRule.DayEndTime, "fineRule.dayEndTime").Minutes;
|
||||||
|
if (fineRule.IntervalMinutes <= 0 || endMinutes <= startMinutes)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = new List<PickupPreviewDayDto>();
|
||||||
|
for (var offset = 0; offset < 3; offset++)
|
||||||
|
{
|
||||||
|
var date = now.Date.AddDays(offset);
|
||||||
|
var uiDayOfWeek = StoreApiHelpers.DotNetDayOfWeekToUi(date.DayOfWeek);
|
||||||
|
var enabled = fineRule.DayOfWeeks.Contains(uiDayOfWeek);
|
||||||
|
var dateText = StoreApiHelpers.ToDateOnly(date);
|
||||||
|
|
||||||
|
var subLabel = $"{GetWeekdayLabel(uiDayOfWeek)} {(offset == 0 ? "今天" : offset == 1 ? "明天" : "后天")}";
|
||||||
|
var slots = enabled
|
||||||
|
? BuildPreviewSlots(date, startMinutes, endMinutes, fineRule.IntervalMinutes, fineRule.SlotCapacity, fineRule.MinAdvanceHours)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
results.Add(new PickupPreviewDayDto
|
||||||
|
{
|
||||||
|
Date = dateText,
|
||||||
|
Label = $"{date.Month}/{date.Day}",
|
||||||
|
SubLabel = subLabel,
|
||||||
|
Slots = slots
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PickupPreviewSlotDto> BuildPreviewSlots(
|
||||||
|
DateTime date,
|
||||||
|
int startMinutes,
|
||||||
|
int endMinutes,
|
||||||
|
int intervalMinutes,
|
||||||
|
int slotCapacity,
|
||||||
|
int minAdvanceHours)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var results = new List<PickupPreviewSlotDto>();
|
||||||
|
for (var minutes = startMinutes; minutes <= endMinutes; minutes += intervalMinutes)
|
||||||
|
{
|
||||||
|
var slotHour = minutes / 60;
|
||||||
|
var slotMinute = minutes % 60;
|
||||||
|
var slotTime = new TimeSpan(slotHour, slotMinute, 0);
|
||||||
|
var slotDateTime = date.Date.Add(slotTime);
|
||||||
|
var hash = GetStableHash($"{StoreApiHelpers.ToDateOnly(date)}|{slotHour:00}:{slotMinute:00}");
|
||||||
|
var booked = hash % (slotCapacity + 1);
|
||||||
|
if (hash % 7 == 0)
|
||||||
|
{
|
||||||
|
booked = slotCapacity;
|
||||||
|
}
|
||||||
|
else if (hash % 5 == 0)
|
||||||
|
{
|
||||||
|
booked = Math.Max(0, slotCapacity - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining = Math.Max(0, slotCapacity - booked);
|
||||||
|
var status = slotDateTime <= now.AddHours(minAdvanceHours)
|
||||||
|
? "expired"
|
||||||
|
: remaining == 0
|
||||||
|
? "full"
|
||||||
|
: remaining <= 1
|
||||||
|
? "almost"
|
||||||
|
: "available";
|
||||||
|
|
||||||
|
results.Add(new PickupPreviewSlotDto
|
||||||
|
{
|
||||||
|
Time = $"{slotHour:00}:{slotMinute:00}",
|
||||||
|
RemainingCount = remaining,
|
||||||
|
Status = status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetStableHash(string value)
|
||||||
|
{
|
||||||
|
var hash = 0;
|
||||||
|
foreach (var ch in value)
|
||||||
|
{
|
||||||
|
hash = (hash * 31 + ch) & int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetWeekdayLabel(int dayOfWeek)
|
||||||
|
{
|
||||||
|
return dayOfWeek switch
|
||||||
|
{
|
||||||
|
0 => "周一",
|
||||||
|
1 => "周二",
|
||||||
|
2 => "周三",
|
||||||
|
3 => "周四",
|
||||||
|
4 => "周五",
|
||||||
|
5 => "周六",
|
||||||
|
6 => "周日",
|
||||||
|
_ => "周一"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,908 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Application.App.Stores.Services;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Merchants.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
using TakeoutSaaS.TenantApi.Contracts.Store;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店员工与排班模块。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/store")]
|
||||||
|
public sealed class StoreStaffController(
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService) : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取员工分页列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("staff")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PaginatedResultDto<StoreStaffItemDto>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PaginatedResultDto<StoreStaffItemDto>>> List(
|
||||||
|
[FromQuery] string storeId,
|
||||||
|
[FromQuery] string? keyword,
|
||||||
|
[FromQuery] string? roleType,
|
||||||
|
[FromQuery] string? status,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 10,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var normalizedKeyword = keyword?.Trim();
|
||||||
|
StaffRoleType? normalizedRoleType = string.IsNullOrWhiteSpace(roleType) ? null : StoreApiHelpers.ToStaffRoleType(roleType);
|
||||||
|
StaffStatus? normalizedStatus = string.IsNullOrWhiteSpace(status) ? null : StoreApiHelpers.ToStaffStatus(status);
|
||||||
|
var normalizedPage = Math.Max(1, page);
|
||||||
|
var normalizedPageSize = Math.Clamp(pageSize, 1, 200);
|
||||||
|
|
||||||
|
var query = dbContext.MerchantStaff
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||||
|
{
|
||||||
|
var lowered = normalizedKeyword.ToLowerInvariant();
|
||||||
|
query = query.Where(x =>
|
||||||
|
x.Name.ToLower().Contains(lowered) ||
|
||||||
|
x.Phone.Contains(normalizedKeyword) ||
|
||||||
|
(x.Email != null && x.Email.ToLower().Contains(lowered)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedRoleType.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.RoleType == normalizedRoleType.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedStatus.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.Status == normalizedStatus.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await query.CountAsync(cancellationToken);
|
||||||
|
var staffs = await query
|
||||||
|
.OrderBy(x => x.CreatedAt)
|
||||||
|
.ThenBy(x => x.Name)
|
||||||
|
.Skip((normalizedPage - 1) * normalizedPageSize)
|
||||||
|
.Take(normalizedPageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var result = new PaginatedResultDto<StoreStaffItemDto>
|
||||||
|
{
|
||||||
|
Items = staffs.Select(MapStaff).ToList(),
|
||||||
|
Total = total,
|
||||||
|
Page = normalizedPage,
|
||||||
|
PageSize = normalizedPageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
return ApiResponse<PaginatedResultDto<StoreStaffItemDto>>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存员工。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("staff/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreStaffItemDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreStaffItemDto>> Save([FromBody] SaveStoreStaffRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
var store = await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var staffId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id);
|
||||||
|
var roleType = StoreApiHelpers.ToStaffRoleType(request.RoleType);
|
||||||
|
var staffStatus = StoreApiHelpers.ToStaffStatus(request.Status);
|
||||||
|
var permissions = NormalizePermissions(request.Permissions, roleType);
|
||||||
|
|
||||||
|
MerchantStaff? entity = null;
|
||||||
|
if (staffId.HasValue)
|
||||||
|
{
|
||||||
|
entity = await dbContext.MerchantStaff.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == staffId.Value && x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity is null)
|
||||||
|
{
|
||||||
|
entity = new MerchantStaff
|
||||||
|
{
|
||||||
|
MerchantId = store.MerchantId,
|
||||||
|
StoreId = parsedStoreId
|
||||||
|
};
|
||||||
|
await dbContext.MerchantStaff.AddAsync(entity, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.Name = request.Name.Trim();
|
||||||
|
entity.Phone = request.Phone.Trim();
|
||||||
|
entity.Email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim();
|
||||||
|
entity.RoleType = roleType;
|
||||||
|
entity.Status = staffStatus;
|
||||||
|
entity.PermissionsJson = JsonSerializer.Serialize(permissions, StoreApiHelpers.JsonOptions);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
var template = await GetOrCreateTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
|
||||||
|
await EnsureStaffScheduleAsync(tenantId, parsedStoreId, entity, template, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoreStaffItemDto>.Ok(MapStaff(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除员工。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("staff/delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> Delete([FromBody] DeleteStoreStaffRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var staffId = StoreApiHelpers.ParseRequiredSnowflake(request.StaffId, "staffId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var staff = await dbContext.MerchantStaff.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == staffId && x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken);
|
||||||
|
if (staff is null)
|
||||||
|
{
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.MerchantStaff.Remove(staff);
|
||||||
|
|
||||||
|
var schedules = await dbContext.StoreStaffWeeklySchedules
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId && x.StaffId == staffId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
dbContext.StoreStaffWeeklySchedules.RemoveRange(schedules);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取门店排班配置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("staff/schedule")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreStaffScheduleDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreStaffScheduleDto>> GetSchedule(
|
||||||
|
[FromQuery] string storeId,
|
||||||
|
[FromQuery] string? weekStartDate,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(storeId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var template = await GetOrCreateTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
|
||||||
|
var staffs = await dbContext.MerchantStaff
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.OrderBy(x => x.CreatedAt)
|
||||||
|
.ThenBy(x => x.Name)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var scheduleRows = await dbContext.StoreStaffWeeklySchedules
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var scheduleMap = scheduleRows
|
||||||
|
.GroupBy(x => x.StaffId)
|
||||||
|
.ToDictionary(x => x.Key, x => x.ToList());
|
||||||
|
|
||||||
|
var schedules = staffs.Select(staff =>
|
||||||
|
{
|
||||||
|
var shifts = scheduleMap.TryGetValue(staff.Id, out var rows)
|
||||||
|
? NormalizeRowsToShifts(rows, staff, template)
|
||||||
|
: CreateDefaultWeekByRole(staff.RoleType, template);
|
||||||
|
|
||||||
|
if (staff.Status == StaffStatus.Resigned)
|
||||||
|
{
|
||||||
|
shifts = CreateOffWeekShifts();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StaffScheduleDto
|
||||||
|
{
|
||||||
|
StaffId = staff.Id.ToString(),
|
||||||
|
Shifts = shifts
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return ApiResponse<StoreStaffScheduleDto>.Ok(new StoreStaffScheduleDto
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId.ToString(),
|
||||||
|
WeekStartDate = StoreApiHelpers.ResolveWeekStartDate(weekStartDate),
|
||||||
|
Templates = template,
|
||||||
|
Schedules = schedules
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存班次模板。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("staff/template/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreShiftTemplatesDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreShiftTemplatesDto>> SaveTemplate(
|
||||||
|
[FromBody] SaveStoreStaffTemplatesRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var normalizedTemplate = NormalizeTemplate(request.Templates);
|
||||||
|
var templateEntity = await dbContext.StoreStaffTemplates.FirstOrDefaultAsync(
|
||||||
|
x => x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken);
|
||||||
|
if (templateEntity is null)
|
||||||
|
{
|
||||||
|
templateEntity = new StoreStaffTemplate
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreStaffTemplates.AddAsync(templateEntity, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
templateEntity.MorningStartTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Morning.StartTime, "templates.morning.startTime");
|
||||||
|
templateEntity.MorningEndTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Morning.EndTime, "templates.morning.endTime");
|
||||||
|
templateEntity.EveningStartTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Evening.StartTime, "templates.evening.startTime");
|
||||||
|
templateEntity.EveningEndTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Evening.EndTime, "templates.evening.endTime");
|
||||||
|
templateEntity.FullStartTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Full.StartTime, "templates.full.startTime");
|
||||||
|
templateEntity.FullEndTime = StoreApiHelpers.ParseRequiredTime(normalizedTemplate.Full.EndTime, "templates.full.endTime");
|
||||||
|
|
||||||
|
var scheduleRows = await dbContext.StoreStaffWeeklySchedules
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
foreach (var row in scheduleRows)
|
||||||
|
{
|
||||||
|
var (start, end) = ResolveShiftTimeRange(row.ShiftType, normalizedTemplate);
|
||||||
|
row.StartTime = start;
|
||||||
|
row.EndTime = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
return ApiResponse<StoreShiftTemplatesDto>.Ok(normalizedTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存单员工排班。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("staff/schedule/personal/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StaffScheduleDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StaffScheduleDto>> SavePersonalSchedule(
|
||||||
|
[FromBody] SaveStoreStaffPersonalScheduleRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var parsedStaffId = StoreApiHelpers.ParseRequiredSnowflake(request.StaffId, "staffId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var staff = await dbContext.MerchantStaff.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == parsedStaffId && x.TenantId == tenantId && x.StoreId == parsedStoreId,
|
||||||
|
cancellationToken);
|
||||||
|
if (staff is null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "员工不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
var template = await GetOrCreateTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
|
||||||
|
var existingRows = await dbContext.StoreStaffWeeklySchedules
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId && x.StaffId == parsedStaffId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var fallback = NormalizeRowsToShifts(existingRows, staff, template);
|
||||||
|
var shifts = staff.Status == StaffStatus.Resigned
|
||||||
|
? CreateOffWeekShifts()
|
||||||
|
: NormalizeShifts(request.Shifts, fallback, template);
|
||||||
|
|
||||||
|
dbContext.StoreStaffWeeklySchedules.RemoveRange(existingRows);
|
||||||
|
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(
|
||||||
|
ToWeeklyEntities(parsedStoreId, parsedStaffId, shifts),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StaffScheduleDto>.Ok(new StaffScheduleDto
|
||||||
|
{
|
||||||
|
StaffId = parsedStaffId.ToString(),
|
||||||
|
Shifts = shifts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存门店周排班。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("staff/schedule/weekly/save")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoreStaffScheduleDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoreStaffScheduleDto>> SaveWeeklySchedule(
|
||||||
|
[FromBody] SaveStoreStaffWeeklyScheduleRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var parsedStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, "storeId");
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
|
||||||
|
|
||||||
|
var staffs = await dbContext.MerchantStaff
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.OrderBy(x => x.CreatedAt)
|
||||||
|
.ThenBy(x => x.Name)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var staffMap = staffs.ToDictionary(x => x.Id);
|
||||||
|
|
||||||
|
var template = await GetOrCreateTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
|
||||||
|
var existingRows = await dbContext.StoreStaffWeeklySchedules
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var existingMap = existingRows
|
||||||
|
.GroupBy(x => x.StaffId)
|
||||||
|
.ToDictionary(
|
||||||
|
x => x.Key,
|
||||||
|
x => NormalizeRowsToShifts(x.ToList(), staffMap.GetValueOrDefault(x.Key), template));
|
||||||
|
|
||||||
|
var incomingMap = new Dictionary<long, List<StaffDayShiftDto>>();
|
||||||
|
foreach (var schedule in request.Schedules ?? [])
|
||||||
|
{
|
||||||
|
var staffId = StoreApiHelpers.ParseSnowflakeOrNull(schedule.StaffId);
|
||||||
|
if (!staffId.HasValue || !staffMap.TryGetValue(staffId.Value, out var staff))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staff.Status == StaffStatus.Resigned)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallback = existingMap.GetValueOrDefault(staffId.Value) ?? CreateDefaultWeekByRole(staff.RoleType, template);
|
||||||
|
incomingMap[staffId.Value] = NormalizeShifts(schedule.Shifts, fallback, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalSchedules = new List<StaffScheduleDto>();
|
||||||
|
foreach (var staff in staffs)
|
||||||
|
{
|
||||||
|
List<StaffDayShiftDto> shifts;
|
||||||
|
if (staff.Status == StaffStatus.Resigned)
|
||||||
|
{
|
||||||
|
shifts = CreateOffWeekShifts();
|
||||||
|
}
|
||||||
|
else if (incomingMap.TryGetValue(staff.Id, out var incomingShifts))
|
||||||
|
{
|
||||||
|
shifts = incomingShifts;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shifts = existingMap.GetValueOrDefault(staff.Id) ?? CreateDefaultWeekByRole(staff.RoleType, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
finalSchedules.Add(new StaffScheduleDto
|
||||||
|
{
|
||||||
|
StaffId = staff.Id.ToString(),
|
||||||
|
Shifts = shifts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.StoreStaffWeeklySchedules.RemoveRange(existingRows);
|
||||||
|
var entities = finalSchedules
|
||||||
|
.SelectMany(x => ToWeeklyEntities(parsedStoreId, long.Parse(x.StaffId), x.Shifts))
|
||||||
|
.ToList();
|
||||||
|
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(entities, cancellationToken);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoreStaffScheduleDto>.Ok(new StoreStaffScheduleDto
|
||||||
|
{
|
||||||
|
StoreId = parsedStoreId.ToString(),
|
||||||
|
WeekStartDate = StoreApiHelpers.ResolveWeekStartDate(null),
|
||||||
|
Templates = template,
|
||||||
|
Schedules = finalSchedules
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制班次模板与排班。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("staff/copy")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<CopyStoreStaffScheduleResult>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<CopyStoreStaffScheduleResult>> Copy([FromBody] CopyStoreStaffScheduleRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sourceStoreId = StoreApiHelpers.ParseRequiredSnowflake(request.SourceStoreId, "sourceStoreId");
|
||||||
|
var targetStoreIds = StoreApiHelpers.ParseSnowflakeList(request.TargetStoreIds);
|
||||||
|
var copyScope = string.IsNullOrWhiteSpace(request.CopyScope) ? "template_and_schedule" : request.CopyScope;
|
||||||
|
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, sourceStoreId, cancellationToken);
|
||||||
|
|
||||||
|
if (!string.Equals(copyScope, "template_and_schedule", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ApiResponse<CopyStoreStaffScheduleResult>.Ok(new CopyStoreStaffScheduleResult
|
||||||
|
{
|
||||||
|
CopiedCount = 0,
|
||||||
|
CopyScope = "template_and_schedule"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessibleTargetIds = await StoreApiHelpers.FilterAccessibleStoreIdsAsync(
|
||||||
|
dbContext,
|
||||||
|
tenantId,
|
||||||
|
merchantId,
|
||||||
|
targetStoreIds,
|
||||||
|
cancellationToken);
|
||||||
|
accessibleTargetIds.Remove(sourceStoreId);
|
||||||
|
|
||||||
|
if (accessibleTargetIds.Count == 0)
|
||||||
|
{
|
||||||
|
return ApiResponse<CopyStoreStaffScheduleResult>.Ok(new CopyStoreStaffScheduleResult
|
||||||
|
{
|
||||||
|
CopiedCount = 0,
|
||||||
|
CopyScope = "template_and_schedule"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceTemplate = await GetOrCreateTemplateDtoAsync(tenantId, sourceStoreId, cancellationToken);
|
||||||
|
var sourceRows = await dbContext.StoreStaffWeeklySchedules
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var sourceStaffs = await dbContext.MerchantStaff
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var sourceStaffMap = sourceStaffs.ToDictionary(x => x.Id);
|
||||||
|
var sourceScheduleMap = sourceRows
|
||||||
|
.GroupBy(x => x.StaffId)
|
||||||
|
.ToDictionary(
|
||||||
|
x => x.Key,
|
||||||
|
x => NormalizeRowsToShifts(x.ToList(), sourceStaffMap.GetValueOrDefault(x.Key), sourceTemplate));
|
||||||
|
var sourceScheduleSequence = sourceStaffs
|
||||||
|
.OrderBy(x => x.CreatedAt)
|
||||||
|
.ThenBy(x => x.Name)
|
||||||
|
.ThenBy(x => x.Id)
|
||||||
|
.Select(staff => staff.Status == StaffStatus.Resigned
|
||||||
|
? CreateOffWeekShifts()
|
||||||
|
: sourceScheduleMap.GetValueOrDefault(staff.Id) ?? CreateDefaultWeekByRole(staff.RoleType, sourceTemplate))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var targetTemplates = await dbContext.StoreStaffTemplates
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var targetTemplateMap = targetTemplates.ToDictionary(x => x.StoreId);
|
||||||
|
|
||||||
|
foreach (var targetStoreId in accessibleTargetIds)
|
||||||
|
{
|
||||||
|
if (!targetTemplateMap.TryGetValue(targetStoreId, out var targetTemplateEntity))
|
||||||
|
{
|
||||||
|
targetTemplateEntity = new StoreStaffTemplate
|
||||||
|
{
|
||||||
|
StoreId = targetStoreId
|
||||||
|
};
|
||||||
|
await dbContext.StoreStaffTemplates.AddAsync(targetTemplateEntity, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
targetTemplateEntity.MorningStartTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Morning.StartTime, "templates.morning.startTime");
|
||||||
|
targetTemplateEntity.MorningEndTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Morning.EndTime, "templates.morning.endTime");
|
||||||
|
targetTemplateEntity.EveningStartTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Evening.StartTime, "templates.evening.startTime");
|
||||||
|
targetTemplateEntity.EveningEndTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Evening.EndTime, "templates.evening.endTime");
|
||||||
|
targetTemplateEntity.FullStartTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Full.StartTime, "templates.full.startTime");
|
||||||
|
targetTemplateEntity.FullEndTime = StoreApiHelpers.ParseRequiredTime(sourceTemplate.Full.EndTime, "templates.full.endTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetRows = await dbContext.StoreStaffWeeklySchedules
|
||||||
|
.Where(x => x.TenantId == tenantId && accessibleTargetIds.Contains(x.StoreId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
dbContext.StoreStaffWeeklySchedules.RemoveRange(targetRows);
|
||||||
|
|
||||||
|
var targetStaffs = await dbContext.MerchantStaff
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId.HasValue && accessibleTargetIds.Contains(x.StoreId.Value))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var entities = new List<StoreStaffWeeklySchedule>();
|
||||||
|
foreach (var targetStoreId in accessibleTargetIds)
|
||||||
|
{
|
||||||
|
var targetStoreStaffs = targetStaffs
|
||||||
|
.Where(x => x.StoreId == targetStoreId)
|
||||||
|
.OrderBy(x => x.CreatedAt)
|
||||||
|
.ThenBy(x => x.Name)
|
||||||
|
.ThenBy(x => x.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
for (var index = 0; index < targetStoreStaffs.Count; index++)
|
||||||
|
{
|
||||||
|
var staff = targetStoreStaffs[index];
|
||||||
|
var shifts = staff.Status == StaffStatus.Resigned
|
||||||
|
? CreateOffWeekShifts()
|
||||||
|
: sourceScheduleSequence.Count > 0
|
||||||
|
? sourceScheduleSequence[index % sourceScheduleSequence.Count]
|
||||||
|
: CreateDefaultWeekByRole(staff.RoleType, sourceTemplate);
|
||||||
|
|
||||||
|
entities.AddRange(ToWeeklyEntities(targetStoreId, staff.Id, shifts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entities.Count > 0)
|
||||||
|
{
|
||||||
|
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(entities, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<CopyStoreStaffScheduleResult>.Ok(new CopyStoreStaffScheduleResult
|
||||||
|
{
|
||||||
|
CopiedCount = accessibleTargetIds.Count,
|
||||||
|
CopyScope = "template_and_schedule"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private static StoreStaffItemDto MapStaff(MerchantStaff source)
|
||||||
|
{
|
||||||
|
var permissions = ParsePermissions(source.PermissionsJson, source.RoleType);
|
||||||
|
var hiredAt = source.CreatedAt == default ? DateTime.UtcNow : source.CreatedAt;
|
||||||
|
|
||||||
|
return new StoreStaffItemDto
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
Name = source.Name,
|
||||||
|
Phone = source.Phone,
|
||||||
|
Email = source.Email ?? string.Empty,
|
||||||
|
RoleType = StoreApiHelpers.ToStaffRoleTypeText(source.RoleType),
|
||||||
|
Status = StoreApiHelpers.ToStaffStatusText(source.Status),
|
||||||
|
Permissions = permissions,
|
||||||
|
AvatarColor = StoreApiHelpers.ResolveAvatarColor($"{source.Name}-{source.Id}"),
|
||||||
|
HiredAt = StoreApiHelpers.ToDateOnly(hiredAt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ParsePermissions(string? rawJson, StaffRoleType roleType)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(rawJson))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JsonSerializer.Deserialize<List<string>>(rawJson, StoreApiHelpers.JsonOptions);
|
||||||
|
if (parsed is not null)
|
||||||
|
{
|
||||||
|
return NormalizePermissions(parsed, roleType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略权限反序列化异常并回落默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleType is StaffRoleType.Admin or StaffRoleType.Operator ? ["全部权限"] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> NormalizePermissions(IEnumerable<string>? source, StaffRoleType roleType)
|
||||||
|
{
|
||||||
|
var normalized = (source ?? [])
|
||||||
|
.Select(x => x?.Trim())
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Select(x => x!)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (roleType is StaffRoleType.Admin or StaffRoleType.Operator && normalized.Count == 0)
|
||||||
|
{
|
||||||
|
normalized.Add("全部权限");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<StoreShiftTemplatesDto> GetOrCreateTemplateDtoAsync(long tenantId, long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var entity = await dbContext.StoreStaffTemplates
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.StoreId == storeId, cancellationToken);
|
||||||
|
|
||||||
|
if (entity is null)
|
||||||
|
{
|
||||||
|
return CreateDefaultTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StoreShiftTemplatesDto
|
||||||
|
{
|
||||||
|
Morning = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = StoreApiHelpers.ToHHmm(entity.MorningStartTime),
|
||||||
|
EndTime = StoreApiHelpers.ToHHmm(entity.MorningEndTime)
|
||||||
|
},
|
||||||
|
Evening = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = StoreApiHelpers.ToHHmm(entity.EveningStartTime),
|
||||||
|
EndTime = StoreApiHelpers.ToHHmm(entity.EveningEndTime)
|
||||||
|
},
|
||||||
|
Full = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = StoreApiHelpers.ToHHmm(entity.FullStartTime),
|
||||||
|
EndTime = StoreApiHelpers.ToHHmm(entity.FullEndTime)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureStaffScheduleAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
MerchantStaff staff,
|
||||||
|
StoreShiftTemplatesDto template,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var existingRows = await dbContext.StoreStaffWeeklySchedules
|
||||||
|
.Where(x => x.TenantId == tenantId && x.StoreId == storeId && x.StaffId == staff.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (existingRows.Count == 0)
|
||||||
|
{
|
||||||
|
var initialShifts = staff.Status == StaffStatus.Resigned
|
||||||
|
? CreateOffWeekShifts()
|
||||||
|
: CreateDefaultWeekByRole(staff.RoleType, template);
|
||||||
|
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(
|
||||||
|
ToWeeklyEntities(storeId, staff.Id, initialShifts),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
else if (staff.Status == StaffStatus.Resigned)
|
||||||
|
{
|
||||||
|
dbContext.StoreStaffWeeklySchedules.RemoveRange(existingRows);
|
||||||
|
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(
|
||||||
|
ToWeeklyEntities(storeId, staff.Id, CreateOffWeekShifts()),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoreShiftTemplatesDto NormalizeTemplate(StoreShiftTemplatesDto source)
|
||||||
|
{
|
||||||
|
var fallback = CreateDefaultTemplate();
|
||||||
|
return new StoreShiftTemplatesDto
|
||||||
|
{
|
||||||
|
Morning = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = NormalizeTime(source.Morning.StartTime, fallback.Morning.StartTime),
|
||||||
|
EndTime = NormalizeTime(source.Morning.EndTime, fallback.Morning.EndTime)
|
||||||
|
},
|
||||||
|
Evening = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = NormalizeTime(source.Evening.StartTime, fallback.Evening.StartTime),
|
||||||
|
EndTime = NormalizeTime(source.Evening.EndTime, fallback.Evening.EndTime)
|
||||||
|
},
|
||||||
|
Full = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = NormalizeTime(source.Full.StartTime, fallback.Full.StartTime),
|
||||||
|
EndTime = NormalizeTime(source.Full.EndTime, fallback.Full.EndTime)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<StaffDayShiftDto> NormalizeRowsToShifts(
|
||||||
|
List<StoreStaffWeeklySchedule> rows,
|
||||||
|
MerchantStaff? staff,
|
||||||
|
StoreShiftTemplatesDto template)
|
||||||
|
{
|
||||||
|
var fallback = staff is null ? CreateOffWeekShifts() : CreateDefaultWeekByRole(staff.RoleType, template);
|
||||||
|
var rowMap = rows.ToDictionary(x => x.DayOfWeek, x => x);
|
||||||
|
var results = new List<StaffDayShiftDto>();
|
||||||
|
|
||||||
|
for (var day = 0; day < 7; day++)
|
||||||
|
{
|
||||||
|
if (!rowMap.TryGetValue(day, out var row))
|
||||||
|
{
|
||||||
|
results.Add(fallback[day]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shiftType = StoreApiHelpers.ToShiftTypeText(row.ShiftType);
|
||||||
|
if (row.ShiftType == StoreStaffShiftType.Off)
|
||||||
|
{
|
||||||
|
results.Add(new StaffDayShiftDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
ShiftType = shiftType,
|
||||||
|
StartTime = string.Empty,
|
||||||
|
EndTime = string.Empty
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (defaultStart, defaultEnd) = ResolveShiftTimeRange(row.ShiftType, template);
|
||||||
|
results.Add(new StaffDayShiftDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
ShiftType = shiftType,
|
||||||
|
StartTime = StoreApiHelpers.ToHHmm(row.StartTime ?? defaultStart ?? TimeSpan.Zero),
|
||||||
|
EndTime = StoreApiHelpers.ToHHmm(row.EndTime ?? defaultEnd ?? TimeSpan.Zero)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<StaffDayShiftDto> NormalizeShifts(
|
||||||
|
IEnumerable<StaffDayShiftDto>? source,
|
||||||
|
List<StaffDayShiftDto> fallback,
|
||||||
|
StoreShiftTemplatesDto template)
|
||||||
|
{
|
||||||
|
var inputMap = (source ?? [])
|
||||||
|
.Where(x => x.DayOfWeek is >= 0 and <= 6)
|
||||||
|
.GroupBy(x => x.DayOfWeek)
|
||||||
|
.ToDictionary(x => x.Key, x => x.Last());
|
||||||
|
|
||||||
|
var normalized = new List<StaffDayShiftDto>();
|
||||||
|
for (var day = 0; day < 7; day++)
|
||||||
|
{
|
||||||
|
var fallbackShift = fallback.FirstOrDefault(x => x.DayOfWeek == day) ?? new StaffDayShiftDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
ShiftType = "off",
|
||||||
|
StartTime = string.Empty,
|
||||||
|
EndTime = string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!inputMap.TryGetValue(day, out var input))
|
||||||
|
{
|
||||||
|
normalized.Add(new StaffDayShiftDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
ShiftType = fallbackShift.ShiftType,
|
||||||
|
StartTime = fallbackShift.StartTime,
|
||||||
|
EndTime = fallbackShift.EndTime
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shiftType = StoreApiHelpers.ToShiftType(input.ShiftType);
|
||||||
|
if (shiftType == StoreStaffShiftType.Off)
|
||||||
|
{
|
||||||
|
normalized.Add(new StaffDayShiftDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
ShiftType = "off",
|
||||||
|
StartTime = string.Empty,
|
||||||
|
EndTime = string.Empty
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (defaultStart, defaultEnd) = ResolveShiftTimeRange(shiftType, template);
|
||||||
|
normalized.Add(new StaffDayShiftDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
ShiftType = StoreApiHelpers.ToShiftTypeText(shiftType),
|
||||||
|
StartTime = NormalizeTime(input.StartTime, defaultStart.HasValue ? StoreApiHelpers.ToHHmm(defaultStart.Value) : fallbackShift.StartTime),
|
||||||
|
EndTime = NormalizeTime(input.EndTime, defaultEnd.HasValue ? StoreApiHelpers.ToHHmm(defaultEnd.Value) : fallbackShift.EndTime)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<StoreStaffWeeklySchedule> ToWeeklyEntities(
|
||||||
|
long storeId,
|
||||||
|
long staffId,
|
||||||
|
IEnumerable<StaffDayShiftDto> shifts)
|
||||||
|
{
|
||||||
|
foreach (var shift in shifts)
|
||||||
|
{
|
||||||
|
var shiftType = StoreApiHelpers.ToShiftType(shift.ShiftType);
|
||||||
|
var (defaultStart, defaultEnd) = ResolveShiftTimeRange(shiftType, CreateDefaultTemplate());
|
||||||
|
var startTime = shiftType == StoreStaffShiftType.Off
|
||||||
|
? null
|
||||||
|
: (TimeSpan?)StoreApiHelpers.ParseRequiredTime(
|
||||||
|
NormalizeTime(shift.StartTime, defaultStart.HasValue ? StoreApiHelpers.ToHHmm(defaultStart.Value) : "09:00"),
|
||||||
|
"shift.startTime");
|
||||||
|
var endTime = shiftType == StoreStaffShiftType.Off
|
||||||
|
? null
|
||||||
|
: (TimeSpan?)StoreApiHelpers.ParseRequiredTime(
|
||||||
|
NormalizeTime(shift.EndTime, defaultEnd.HasValue ? StoreApiHelpers.ToHHmm(defaultEnd.Value) : "21:00"),
|
||||||
|
"shift.endTime");
|
||||||
|
|
||||||
|
yield return new StoreStaffWeeklySchedule
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
StaffId = staffId,
|
||||||
|
DayOfWeek = shift.DayOfWeek,
|
||||||
|
ShiftType = shiftType,
|
||||||
|
StartTime = startTime,
|
||||||
|
EndTime = endTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<StaffDayShiftDto> CreateDefaultWeekByRole(StaffRoleType roleType, StoreShiftTemplatesDto template)
|
||||||
|
{
|
||||||
|
var pattern = roleType switch
|
||||||
|
{
|
||||||
|
StaffRoleType.Admin or StaffRoleType.Operator => new[] { "full", "full", "full", "full", "full", "morning", "off" },
|
||||||
|
StaffRoleType.FrontDesk => new[] { "morning", "morning", "off", "morning", "evening", "full", "full" },
|
||||||
|
StaffRoleType.Courier => new[] { "morning", "evening", "morning", "evening", "morning", "evening", "off" },
|
||||||
|
StaffRoleType.Kitchen => new[] { "full", "full", "evening", "off", "full", "full", "morning" },
|
||||||
|
_ => new[] { "morning", "morning", "off", "morning", "evening", "full", "full" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var results = new List<StaffDayShiftDto>();
|
||||||
|
for (var day = 0; day < 7; day++)
|
||||||
|
{
|
||||||
|
var shiftType = StoreApiHelpers.ToShiftType(pattern[day]);
|
||||||
|
var (start, end) = ResolveShiftTimeRange(shiftType, template);
|
||||||
|
results.Add(new StaffDayShiftDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
ShiftType = StoreApiHelpers.ToShiftTypeText(shiftType),
|
||||||
|
StartTime = start.HasValue ? StoreApiHelpers.ToHHmm(start.Value) : string.Empty,
|
||||||
|
EndTime = end.HasValue ? StoreApiHelpers.ToHHmm(end.Value) : string.Empty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<StaffDayShiftDto> CreateOffWeekShifts()
|
||||||
|
{
|
||||||
|
return Enumerable.Range(0, 7).Select(day => new StaffDayShiftDto
|
||||||
|
{
|
||||||
|
DayOfWeek = day,
|
||||||
|
ShiftType = "off",
|
||||||
|
StartTime = string.Empty,
|
||||||
|
EndTime = string.Empty
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (TimeSpan? Start, TimeSpan? End) ResolveShiftTimeRange(StoreStaffShiftType shiftType, StoreShiftTemplatesDto template)
|
||||||
|
{
|
||||||
|
return shiftType switch
|
||||||
|
{
|
||||||
|
StoreStaffShiftType.Morning => (
|
||||||
|
StoreApiHelpers.ParseRequiredTime(template.Morning.StartTime, "templates.morning.startTime"),
|
||||||
|
StoreApiHelpers.ParseRequiredTime(template.Morning.EndTime, "templates.morning.endTime")),
|
||||||
|
StoreStaffShiftType.Evening => (
|
||||||
|
StoreApiHelpers.ParseRequiredTime(template.Evening.StartTime, "templates.evening.startTime"),
|
||||||
|
StoreApiHelpers.ParseRequiredTime(template.Evening.EndTime, "templates.evening.endTime")),
|
||||||
|
StoreStaffShiftType.Full => (
|
||||||
|
StoreApiHelpers.ParseRequiredTime(template.Full.StartTime, "templates.full.startTime"),
|
||||||
|
StoreApiHelpers.ParseRequiredTime(template.Full.EndTime, "templates.full.endTime")),
|
||||||
|
_ => (null, null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeTime(string? value, string fallback)
|
||||||
|
{
|
||||||
|
return TimeSpan.TryParseExact(value, "hh\\:mm", null, out var parsed)
|
||||||
|
? StoreApiHelpers.ToHHmm(parsed)
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoreShiftTemplatesDto CreateDefaultTemplate()
|
||||||
|
{
|
||||||
|
return new StoreShiftTemplatesDto
|
||||||
|
{
|
||||||
|
Morning = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = "09:00",
|
||||||
|
EndTime = "14:00"
|
||||||
|
},
|
||||||
|
Evening = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = "14:00",
|
||||||
|
EndTime = "21:00"
|
||||||
|
},
|
||||||
|
Full = new ShiftTemplateItemDto
|
||||||
|
{
|
||||||
|
StartTime = "09:00",
|
||||||
|
EndTime = "21:00"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,16 @@ public sealed record CreateStoreDeliveryZoneCommand : IRequest<StoreDeliveryZone
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EstimatedMinutes { get; init; }
|
public int? EstimatedMinutes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 区域颜色。
|
||||||
|
/// </summary>
|
||||||
|
public string? Color { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; init; } = 100;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 排序。
|
/// 排序。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ public sealed record UpdateStoreDeliveryZoneCommand : IRequest<StoreDeliveryZone
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EstimatedMinutes { get; init; }
|
public int? EstimatedMinutes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 区域颜色。
|
||||||
|
/// </summary>
|
||||||
|
public string? Color { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; init; } = 100;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 排序。
|
/// 排序。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -48,4 +48,24 @@ public sealed record UpdateStoreFeeCommand : IRequest<StoreFeeDto>
|
|||||||
/// 免配送费门槛。
|
/// 免配送费门槛。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? FreeDeliveryThreshold { get; init; }
|
public decimal? FreeDeliveryThreshold { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 餐具费是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool CutleryFeeEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 餐具费金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CutleryFeeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加急费是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool RushFeeEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加急费金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RushFeeAmount { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.App.Stores.Dto;
|
using TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
namespace TakeoutSaaS.Application.App.Stores.Commands;
|
||||||
|
|
||||||
@@ -32,4 +33,14 @@ public sealed record UpsertStorePickupSettingCommand : IRequest<StorePickupSetti
|
|||||||
/// 单笔最大份数。
|
/// 单笔最大份数。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MaxQuantityPerOrder { get; init; }
|
public int? MaxQuantityPerOrder { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提模式。
|
||||||
|
/// </summary>
|
||||||
|
public StorePickupMode Mode { get; init; } = StorePickupMode.Big;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 精细规则 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public string? FineRuleJson { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ public sealed record StoreDeliveryZoneDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EstimatedMinutes { get; init; }
|
public int? EstimatedMinutes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 区域颜色。
|
||||||
|
/// </summary>
|
||||||
|
public string? Color { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 排序。
|
/// 排序。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -31,6 +31,26 @@ public sealed record StoreFeeDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal DeliveryFee { get; init; }
|
public decimal DeliveryFee { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 餐具费是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool CutleryFeeEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 餐具费金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CutleryFeeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加急费是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool RushFeeEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加急费金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RushFeeAmount { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 打包费模式。
|
/// 打包费模式。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
using TakeoutSaaS.Shared.Abstractions.Serialization;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.App.Stores.Dto;
|
namespace TakeoutSaaS.Application.App.Stores.Dto;
|
||||||
@@ -39,4 +40,14 @@ public sealed record StorePickupSettingDto
|
|||||||
/// 单笔最大自提份数。
|
/// 单笔最大自提份数。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MaxQuantityPerOrder { get; init; }
|
public int? MaxQuantityPerOrder { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提模式。
|
||||||
|
/// </summary>
|
||||||
|
public StorePickupMode Mode { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 精细规则 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public string? FineRuleJson { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ public sealed class CreateStoreDeliveryZoneCommandHandler(
|
|||||||
MinimumOrderAmount = request.MinimumOrderAmount,
|
MinimumOrderAmount = request.MinimumOrderAmount,
|
||||||
DeliveryFee = request.DeliveryFee,
|
DeliveryFee = request.DeliveryFee,
|
||||||
EstimatedMinutes = request.EstimatedMinutes,
|
EstimatedMinutes = request.EstimatedMinutes,
|
||||||
|
Color = request.Color?.Trim(),
|
||||||
|
Priority = request.Priority,
|
||||||
SortOrder = request.SortOrder
|
SortOrder = request.SortOrder
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ public sealed class GetStoreFeeQueryHandler(
|
|||||||
BaseDeliveryFee = 0m,
|
BaseDeliveryFee = 0m,
|
||||||
PackagingFeeMode = Domain.Stores.Enums.PackagingFeeMode.Fixed,
|
PackagingFeeMode = Domain.Stores.Enums.PackagingFeeMode.Fixed,
|
||||||
OrderPackagingFeeMode = Domain.Stores.Enums.OrderPackagingFeeMode.Fixed,
|
OrderPackagingFeeMode = Domain.Stores.Enums.OrderPackagingFeeMode.Fixed,
|
||||||
FixedPackagingFee = 0m
|
FixedPackagingFee = 0m,
|
||||||
|
CutleryFeeEnabled = false,
|
||||||
|
CutleryFeeAmount = 0m,
|
||||||
|
RushFeeEnabled = false,
|
||||||
|
RushFeeAmount = 0m
|
||||||
};
|
};
|
||||||
return StoreMapping.ToDto(fallback);
|
return StoreMapping.ToDto(fallback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ public sealed class GetStorePickupSettingQueryHandler(
|
|||||||
AllowToday = setting.AllowToday,
|
AllowToday = setting.AllowToday,
|
||||||
AllowDaysAhead = setting.AllowDaysAhead,
|
AllowDaysAhead = setting.AllowDaysAhead,
|
||||||
DefaultCutoffMinutes = setting.DefaultCutoffMinutes,
|
DefaultCutoffMinutes = setting.DefaultCutoffMinutes,
|
||||||
MaxQuantityPerOrder = setting.MaxQuantityPerOrder
|
MaxQuantityPerOrder = setting.MaxQuantityPerOrder,
|
||||||
|
Mode = setting.Mode,
|
||||||
|
FineRuleJson = setting.FineRuleJson
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ public sealed class UpdateStoreDeliveryZoneCommandHandler(
|
|||||||
existing.MinimumOrderAmount = request.MinimumOrderAmount;
|
existing.MinimumOrderAmount = request.MinimumOrderAmount;
|
||||||
existing.DeliveryFee = request.DeliveryFee;
|
existing.DeliveryFee = request.DeliveryFee;
|
||||||
existing.EstimatedMinutes = request.EstimatedMinutes;
|
existing.EstimatedMinutes = request.EstimatedMinutes;
|
||||||
|
existing.Color = request.Color?.Trim();
|
||||||
|
existing.Priority = request.Priority;
|
||||||
existing.SortOrder = request.SortOrder;
|
existing.SortOrder = request.SortOrder;
|
||||||
|
|
||||||
// 5. (空行后) 持久化
|
// 5. (空行后) 持久化
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ public sealed class UpdateStoreFeeCommandHandler(
|
|||||||
fee.PackagingFeeTiersJson = null;
|
fee.PackagingFeeTiersJson = null;
|
||||||
}
|
}
|
||||||
fee.FreeDeliveryThreshold = request.FreeDeliveryThreshold;
|
fee.FreeDeliveryThreshold = request.FreeDeliveryThreshold;
|
||||||
|
fee.CutleryFeeEnabled = request.CutleryFeeEnabled;
|
||||||
|
fee.CutleryFeeAmount = request.CutleryFeeAmount;
|
||||||
|
fee.RushFeeEnabled = request.RushFeeEnabled;
|
||||||
|
fee.RushFeeAmount = request.RushFeeAmount;
|
||||||
|
|
||||||
// 4. (空行后) 保存并返回
|
// 4. (空行后) 保存并返回
|
||||||
if (isNew)
|
if (isNew)
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ public sealed class UpsertStorePickupSettingCommandHandler(
|
|||||||
setting.AllowDaysAhead = request.AllowDaysAhead;
|
setting.AllowDaysAhead = request.AllowDaysAhead;
|
||||||
setting.DefaultCutoffMinutes = request.DefaultCutoffMinutes;
|
setting.DefaultCutoffMinutes = request.DefaultCutoffMinutes;
|
||||||
setting.MaxQuantityPerOrder = request.MaxQuantityPerOrder;
|
setting.MaxQuantityPerOrder = request.MaxQuantityPerOrder;
|
||||||
|
setting.Mode = request.Mode;
|
||||||
|
setting.FineRuleJson = request.FineRuleJson;
|
||||||
await storeRepository.UpdatePickupSettingAsync(setting, cancellationToken);
|
await storeRepository.UpdatePickupSettingAsync(setting, cancellationToken);
|
||||||
await storeRepository.SaveChangesAsync(cancellationToken);
|
await storeRepository.SaveChangesAsync(cancellationToken);
|
||||||
logger.LogInformation("更新门店 {StoreId} 自提配置", request.StoreId);
|
logger.LogInformation("更新门店 {StoreId} 自提配置", request.StoreId);
|
||||||
@@ -57,7 +59,9 @@ public sealed class UpsertStorePickupSettingCommandHandler(
|
|||||||
AllowToday = setting.AllowToday,
|
AllowToday = setting.AllowToday,
|
||||||
AllowDaysAhead = setting.AllowDaysAhead,
|
AllowDaysAhead = setting.AllowDaysAhead,
|
||||||
DefaultCutoffMinutes = setting.DefaultCutoffMinutes,
|
DefaultCutoffMinutes = setting.DefaultCutoffMinutes,
|
||||||
MaxQuantityPerOrder = setting.MaxQuantityPerOrder
|
MaxQuantityPerOrder = setting.MaxQuantityPerOrder,
|
||||||
|
Mode = setting.Mode,
|
||||||
|
FineRuleJson = setting.FineRuleJson
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ public static class StoreMapping
|
|||||||
StoreId = fee.StoreId,
|
StoreId = fee.StoreId,
|
||||||
MinimumOrderAmount = fee.MinimumOrderAmount,
|
MinimumOrderAmount = fee.MinimumOrderAmount,
|
||||||
DeliveryFee = fee.BaseDeliveryFee,
|
DeliveryFee = fee.BaseDeliveryFee,
|
||||||
|
CutleryFeeEnabled = fee.CutleryFeeEnabled,
|
||||||
|
CutleryFeeAmount = fee.CutleryFeeAmount,
|
||||||
|
RushFeeEnabled = fee.RushFeeEnabled,
|
||||||
|
RushFeeAmount = fee.RushFeeAmount,
|
||||||
PackagingFeeMode = fee.PackagingFeeMode,
|
PackagingFeeMode = fee.PackagingFeeMode,
|
||||||
OrderPackagingFeeMode = fee.OrderPackagingFeeMode,
|
OrderPackagingFeeMode = fee.OrderPackagingFeeMode,
|
||||||
FixedPackagingFee = fee.FixedPackagingFee,
|
FixedPackagingFee = fee.FixedPackagingFee,
|
||||||
@@ -137,6 +141,8 @@ public static class StoreMapping
|
|||||||
MinimumOrderAmount = zone.MinimumOrderAmount,
|
MinimumOrderAmount = zone.MinimumOrderAmount,
|
||||||
DeliveryFee = zone.DeliveryFee,
|
DeliveryFee = zone.DeliveryFee,
|
||||||
EstimatedMinutes = zone.EstimatedMinutes,
|
EstimatedMinutes = zone.EstimatedMinutes,
|
||||||
|
Color = zone.Color,
|
||||||
|
Priority = zone.Priority,
|
||||||
SortOrder = zone.SortOrder,
|
SortOrder = zone.SortOrder,
|
||||||
CreatedAt = zone.CreatedAt
|
CreatedAt = zone.CreatedAt
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ public sealed class CreateStoreDeliveryZoneCommandValidator : AbstractValidator<
|
|||||||
RuleFor(x => x.MinimumOrderAmount).GreaterThanOrEqualTo(0).When(x => x.MinimumOrderAmount.HasValue);
|
RuleFor(x => x.MinimumOrderAmount).GreaterThanOrEqualTo(0).When(x => x.MinimumOrderAmount.HasValue);
|
||||||
RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).When(x => x.DeliveryFee.HasValue);
|
RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).When(x => x.DeliveryFee.HasValue);
|
||||||
RuleFor(x => x.EstimatedMinutes).GreaterThan(0).When(x => x.EstimatedMinutes.HasValue);
|
RuleFor(x => x.EstimatedMinutes).GreaterThan(0).When(x => x.EstimatedMinutes.HasValue);
|
||||||
|
RuleFor(x => x.Color).MaximumLength(32);
|
||||||
|
RuleFor(x => x.Priority).GreaterThanOrEqualTo(0);
|
||||||
RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public sealed class UpdateStoreDeliveryZoneCommandValidator : AbstractValidator<
|
|||||||
RuleFor(x => x.MinimumOrderAmount).GreaterThanOrEqualTo(0).When(x => x.MinimumOrderAmount.HasValue);
|
RuleFor(x => x.MinimumOrderAmount).GreaterThanOrEqualTo(0).When(x => x.MinimumOrderAmount.HasValue);
|
||||||
RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).When(x => x.DeliveryFee.HasValue);
|
RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).When(x => x.DeliveryFee.HasValue);
|
||||||
RuleFor(x => x.EstimatedMinutes).GreaterThan(0).When(x => x.EstimatedMinutes.HasValue);
|
RuleFor(x => x.EstimatedMinutes).GreaterThan(0).When(x => x.EstimatedMinutes.HasValue);
|
||||||
|
RuleFor(x => x.Color).MaximumLength(32);
|
||||||
|
RuleFor(x => x.Priority).GreaterThanOrEqualTo(0);
|
||||||
RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ public sealed class UpdateStoreFeeCommandValidator : AbstractValidator<UpdateSto
|
|||||||
.Must(fee => !fee.HasValue || fee.Value >= 0)
|
.Must(fee => !fee.HasValue || fee.Value >= 0)
|
||||||
.WithMessage("固定打包费不能为负数");
|
.WithMessage("固定打包费不能为负数");
|
||||||
|
|
||||||
|
RuleFor(x => x.CutleryFeeAmount)
|
||||||
|
.GreaterThanOrEqualTo(0)
|
||||||
|
.LessThanOrEqualTo(99.99m);
|
||||||
|
|
||||||
|
RuleFor(x => x.RushFeeAmount)
|
||||||
|
.GreaterThanOrEqualTo(0)
|
||||||
|
.LessThanOrEqualTo(99.99m);
|
||||||
|
|
||||||
RuleFor(x => x)
|
RuleFor(x => x)
|
||||||
.Custom((command, context) =>
|
.Custom((command, context) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ public sealed class UpsertStorePickupSettingCommandValidator : AbstractValidator
|
|||||||
RuleFor(x => x.AllowDaysAhead).GreaterThanOrEqualTo(0);
|
RuleFor(x => x.AllowDaysAhead).GreaterThanOrEqualTo(0);
|
||||||
RuleFor(x => x.DefaultCutoffMinutes).GreaterThanOrEqualTo(0);
|
RuleFor(x => x.DefaultCutoffMinutes).GreaterThanOrEqualTo(0);
|
||||||
RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue);
|
RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue);
|
||||||
|
RuleFor(x => x.FineRuleJson).MaximumLength(20000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店配送设置聚合。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreDeliverySetting : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送模式。
|
||||||
|
/// </summary>
|
||||||
|
public StoreDeliveryMode Mode { get; set; } = StoreDeliveryMode.Polygon;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配送时效加成(分钟)。
|
||||||
|
/// </summary>
|
||||||
|
public int EtaAdjustmentMinutes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 免配送费门槛。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? FreeDeliveryThreshold { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每小时配送上限。
|
||||||
|
/// </summary>
|
||||||
|
public int HourlyCapacityLimit { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大配送距离(公里)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MaxDeliveryDistance { get; set; } = 5m;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 半径梯度配置 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public string? RadiusTiersJson { get; set; }
|
||||||
|
}
|
||||||
@@ -37,6 +37,16 @@ public sealed class StoreDeliveryZone : MultiTenantEntityBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EstimatedMinutes { get; set; }
|
public int? EstimatedMinutes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 区域颜色。
|
||||||
|
/// </summary>
|
||||||
|
public string? Color { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优先级(数值越小越优先)。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; } = 100;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 排序值。
|
/// 排序值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店堂食基础设置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreDineInSetting : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用堂食。
|
||||||
|
/// </summary>
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认用餐时长(分钟)。
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultDiningMinutes { get; set; } = 90;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超时提醒阈值(分钟)。
|
||||||
|
/// </summary>
|
||||||
|
public int OvertimeReminderMinutes { get; set; } = 10;
|
||||||
|
}
|
||||||
@@ -47,4 +47,24 @@ public sealed class StoreFee : MultiTenantEntityBase
|
|||||||
/// 免配送费门槛。
|
/// 免配送费门槛。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? FreeDeliveryThreshold { get; set; }
|
public decimal? FreeDeliveryThreshold { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用餐具费。
|
||||||
|
/// </summary>
|
||||||
|
public bool CutleryFeeEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 餐具费金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CutleryFeeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用加急费。
|
||||||
|
/// </summary>
|
||||||
|
public bool RushFeeEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加急费金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RushFeeAmount { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Domain.Stores.Entities;
|
namespace TakeoutSaaS.Domain.Stores.Entities;
|
||||||
@@ -33,6 +34,16 @@ public sealed class StorePickupSetting : MultiTenantEntityBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MaxQuantityPerOrder { get; set; }
|
public int? MaxQuantityPerOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自提配置模式。
|
||||||
|
/// </summary>
|
||||||
|
public StorePickupMode Mode { get; set; } = StorePickupMode.Big;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 精细规则 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public string? FineRuleJson { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 并发控制字段。
|
/// 并发控制字段。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店员工班次模板。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreStaffTemplate : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 早班开始时间。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan MorningStartTime { get; set; } = new(9, 0, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 早班结束时间。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan MorningEndTime { get; set; } = new(14, 0, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 晚班开始时间。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan EveningStartTime { get; set; } = new(14, 0, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 晚班结束时间。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan EveningEndTime { get; set; } = new(21, 0, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全天班开始时间。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan FullStartTime { get; set; } = new(9, 0, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全天班结束时间。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan FullEndTime { get; set; } = new(21, 0, 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Stores.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店员工每周排班。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoreStaffWeeklySchedule : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StaffId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 星期(0=周一,6=周日)。
|
||||||
|
/// </summary>
|
||||||
|
public int DayOfWeek { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 班次类型。
|
||||||
|
/// </summary>
|
||||||
|
public StoreStaffShiftType ShiftType { get; set; } = StoreStaffShiftType.Off;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间(休息时为空)。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? StartTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束时间(休息时为空)。
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? EndTime { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店配送配置模式。
|
||||||
|
/// </summary>
|
||||||
|
public enum StoreDeliveryMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 多边形配送范围。
|
||||||
|
/// </summary>
|
||||||
|
Polygon = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 半径梯度配送。
|
||||||
|
/// </summary>
|
||||||
|
Radius = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店自提模式。
|
||||||
|
/// </summary>
|
||||||
|
public enum StorePickupMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 大时段模式。
|
||||||
|
/// </summary>
|
||||||
|
Big = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 精细规则模式。
|
||||||
|
/// </summary>
|
||||||
|
Fine = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Stores.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 员工班次类型。
|
||||||
|
/// </summary>
|
||||||
|
public enum StoreStaffShiftType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 早班。
|
||||||
|
/// </summary>
|
||||||
|
Morning = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 晚班。
|
||||||
|
/// </summary>
|
||||||
|
Evening = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全天班。
|
||||||
|
/// </summary>
|
||||||
|
Full = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 休息。
|
||||||
|
/// </summary>
|
||||||
|
Off = 3
|
||||||
|
}
|
||||||
@@ -148,6 +148,14 @@ public sealed class TakeoutAppDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<StoreDeliveryZone> StoreDeliveryZones => Set<StoreDeliveryZone>();
|
public DbSet<StoreDeliveryZone> StoreDeliveryZones => Set<StoreDeliveryZone>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// 门店配送设置。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<StoreDeliverySetting> StoreDeliverySettings => Set<StoreDeliverySetting>();
|
||||||
|
/// <summary>
|
||||||
|
/// 门店堂食设置。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<StoreDineInSetting> StoreDineInSettings => Set<StoreDineInSetting>();
|
||||||
|
/// <summary>
|
||||||
/// 门店桌台区域。
|
/// 门店桌台区域。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<StoreTableArea> StoreTableAreas => Set<StoreTableArea>();
|
public DbSet<StoreTableArea> StoreTableAreas => Set<StoreTableArea>();
|
||||||
@@ -168,6 +176,14 @@ public sealed class TakeoutAppDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<StorePickupSlot> StorePickupSlots => Set<StorePickupSlot>();
|
public DbSet<StorePickupSlot> StorePickupSlots => Set<StorePickupSlot>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// 门店班次模板。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<StoreStaffTemplate> StoreStaffTemplates => Set<StoreStaffTemplate>();
|
||||||
|
/// <summary>
|
||||||
|
/// 门店每周排班。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<StoreStaffWeeklySchedule> StoreStaffWeeklySchedules => Set<StoreStaffWeeklySchedule>();
|
||||||
|
/// <summary>
|
||||||
/// 商品分类。
|
/// 商品分类。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
|
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
|
||||||
@@ -411,11 +427,15 @@ public sealed class TakeoutAppDbContext(
|
|||||||
ConfigureStoreBusinessHour(modelBuilder.Entity<StoreBusinessHour>());
|
ConfigureStoreBusinessHour(modelBuilder.Entity<StoreBusinessHour>());
|
||||||
ConfigureStoreHoliday(modelBuilder.Entity<StoreHoliday>());
|
ConfigureStoreHoliday(modelBuilder.Entity<StoreHoliday>());
|
||||||
ConfigureStoreDeliveryZone(modelBuilder.Entity<StoreDeliveryZone>());
|
ConfigureStoreDeliveryZone(modelBuilder.Entity<StoreDeliveryZone>());
|
||||||
|
ConfigureStoreDeliverySetting(modelBuilder.Entity<StoreDeliverySetting>());
|
||||||
|
ConfigureStoreDineInSetting(modelBuilder.Entity<StoreDineInSetting>());
|
||||||
ConfigureStoreTableArea(modelBuilder.Entity<StoreTableArea>());
|
ConfigureStoreTableArea(modelBuilder.Entity<StoreTableArea>());
|
||||||
ConfigureStoreTable(modelBuilder.Entity<StoreTable>());
|
ConfigureStoreTable(modelBuilder.Entity<StoreTable>());
|
||||||
ConfigureStoreEmployeeShift(modelBuilder.Entity<StoreEmployeeShift>());
|
ConfigureStoreEmployeeShift(modelBuilder.Entity<StoreEmployeeShift>());
|
||||||
ConfigureStorePickupSetting(modelBuilder.Entity<StorePickupSetting>());
|
ConfigureStorePickupSetting(modelBuilder.Entity<StorePickupSetting>());
|
||||||
ConfigureStorePickupSlot(modelBuilder.Entity<StorePickupSlot>());
|
ConfigureStorePickupSlot(modelBuilder.Entity<StorePickupSlot>());
|
||||||
|
ConfigureStoreStaffTemplate(modelBuilder.Entity<StoreStaffTemplate>());
|
||||||
|
ConfigureStoreStaffWeeklySchedule(modelBuilder.Entity<StoreStaffWeeklySchedule>());
|
||||||
ConfigureProductCategory(modelBuilder.Entity<ProductCategory>());
|
ConfigureProductCategory(modelBuilder.Entity<ProductCategory>());
|
||||||
ConfigureProduct(modelBuilder.Entity<Product>());
|
ConfigureProduct(modelBuilder.Entity<Product>());
|
||||||
ConfigureProductAttributeGroup(modelBuilder.Entity<ProductAttributeGroup>());
|
ConfigureProductAttributeGroup(modelBuilder.Entity<ProductAttributeGroup>());
|
||||||
@@ -603,6 +623,10 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.Property(x => x.FixedPackagingFee).HasPrecision(10, 2);
|
builder.Property(x => x.FixedPackagingFee).HasPrecision(10, 2);
|
||||||
builder.Property(x => x.PackagingFeeTiersJson).HasColumnType("text");
|
builder.Property(x => x.PackagingFeeTiersJson).HasColumnType("text");
|
||||||
builder.Property(x => x.FreeDeliveryThreshold).HasPrecision(10, 2);
|
builder.Property(x => x.FreeDeliveryThreshold).HasPrecision(10, 2);
|
||||||
|
builder.Property(x => x.CutleryFeeEnabled).HasDefaultValue(false);
|
||||||
|
builder.Property(x => x.CutleryFeeAmount).HasPrecision(10, 2);
|
||||||
|
builder.Property(x => x.RushFeeEnabled).HasDefaultValue(false);
|
||||||
|
builder.Property(x => x.RushFeeAmount).HasPrecision(10, 2);
|
||||||
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
||||||
builder.HasIndex(x => x.TenantId);
|
builder.HasIndex(x => x.TenantId);
|
||||||
}
|
}
|
||||||
@@ -981,9 +1005,34 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.Property(x => x.PolygonGeoJson).HasColumnType("text").IsRequired();
|
builder.Property(x => x.PolygonGeoJson).HasColumnType("text").IsRequired();
|
||||||
builder.Property(x => x.MinimumOrderAmount).HasPrecision(18, 2);
|
builder.Property(x => x.MinimumOrderAmount).HasPrecision(18, 2);
|
||||||
builder.Property(x => x.DeliveryFee).HasPrecision(18, 2);
|
builder.Property(x => x.DeliveryFee).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.Color).HasMaxLength(32);
|
||||||
|
builder.Property(x => x.Priority).HasDefaultValue(100);
|
||||||
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ZoneName });
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ZoneName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConfigureStoreDeliverySetting(EntityTypeBuilder<StoreDeliverySetting> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("store_delivery_settings");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.Mode).HasConversion<int>();
|
||||||
|
builder.Property(x => x.FreeDeliveryThreshold).HasPrecision(10, 2);
|
||||||
|
builder.Property(x => x.MaxDeliveryDistance).HasPrecision(10, 2);
|
||||||
|
builder.Property(x => x.RadiusTiersJson).HasColumnType("text");
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureStoreDineInSetting(EntityTypeBuilder<StoreDineInSetting> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("store_dinein_settings");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.Enabled).HasDefaultValue(true);
|
||||||
|
builder.Property(x => x.DefaultDiningMinutes).HasDefaultValue(90);
|
||||||
|
builder.Property(x => x.OvertimeReminderMinutes).HasDefaultValue(10);
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
||||||
|
}
|
||||||
|
|
||||||
private static void ConfigureStoreTableArea(EntityTypeBuilder<StoreTableArea> builder)
|
private static void ConfigureStoreTableArea(EntityTypeBuilder<StoreTableArea> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("store_table_areas");
|
builder.ToTable("store_table_areas");
|
||||||
@@ -1022,6 +1071,8 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.HasKey(x => x.Id);
|
builder.HasKey(x => x.Id);
|
||||||
builder.Property(x => x.StoreId).IsRequired();
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
builder.Property(x => x.DefaultCutoffMinutes).HasDefaultValue(30);
|
builder.Property(x => x.DefaultCutoffMinutes).HasDefaultValue(30);
|
||||||
|
builder.Property(x => x.Mode).HasConversion<int>();
|
||||||
|
builder.Property(x => x.FineRuleJson).HasColumnType("text");
|
||||||
builder.Property(x => x.RowVersion)
|
builder.Property(x => x.RowVersion)
|
||||||
.IsConcurrencyToken();
|
.IsConcurrencyToken();
|
||||||
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
||||||
@@ -1040,6 +1091,34 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name });
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConfigureStoreStaffTemplate(EntityTypeBuilder<StoreStaffTemplate> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("store_staff_templates");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.MorningStartTime).IsRequired();
|
||||||
|
builder.Property(x => x.MorningEndTime).IsRequired();
|
||||||
|
builder.Property(x => x.EveningStartTime).IsRequired();
|
||||||
|
builder.Property(x => x.EveningEndTime).IsRequired();
|
||||||
|
builder.Property(x => x.FullStartTime).IsRequired();
|
||||||
|
builder.Property(x => x.FullEndTime).IsRequired();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureStoreStaffWeeklySchedule(EntityTypeBuilder<StoreStaffWeeklySchedule> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("store_staff_weekly_schedules");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.StaffId).IsRequired();
|
||||||
|
builder.Property(x => x.DayOfWeek).IsRequired();
|
||||||
|
builder.Property(x => x.ShiftType).HasConversion<int>();
|
||||||
|
builder.Property(x => x.StartTime);
|
||||||
|
builder.Property(x => x.EndTime);
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.StaffId, x.DayOfWeek }).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.DayOfWeek });
|
||||||
|
}
|
||||||
|
|
||||||
private static void ConfigureProductAttributeGroup(EntityTypeBuilder<ProductAttributeGroup> builder)
|
private static void ConfigureProductAttributeGroup(EntityTypeBuilder<ProductAttributeGroup> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("product_attribute_groups");
|
builder.ToTable("product_attribute_groups");
|
||||||
|
|||||||
7751
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260217063434_AddStoreExtendedSettings.Designer.cs
generated
Normal file
7751
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260217063434_AddStoreExtendedSettings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,295 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddStoreExtendedSettings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FineRuleJson",
|
||||||
|
table: "store_pickup_settings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true,
|
||||||
|
comment: "精细规则 JSON。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Mode",
|
||||||
|
table: "store_pickup_settings",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
comment: "自提配置模式。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "CutleryFeeAmount",
|
||||||
|
table: "store_fees",
|
||||||
|
type: "numeric(10,2)",
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m,
|
||||||
|
comment: "餐具费金额。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "CutleryFeeEnabled",
|
||||||
|
table: "store_fees",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: "是否启用餐具费。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "RushFeeAmount",
|
||||||
|
table: "store_fees",
|
||||||
|
type: "numeric(10,2)",
|
||||||
|
precision: 10,
|
||||||
|
scale: 2,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m,
|
||||||
|
comment: "加急费金额。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "RushFeeEnabled",
|
||||||
|
table: "store_fees",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false,
|
||||||
|
comment: "是否启用加急费。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Color",
|
||||||
|
table: "store_delivery_zones",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: true,
|
||||||
|
comment: "区域颜色。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Priority",
|
||||||
|
table: "store_delivery_zones",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 100,
|
||||||
|
comment: "优先级(数值越小越优先)。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "store_delivery_settings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
Mode = table.Column<int>(type: "integer", nullable: false, comment: "配送模式。"),
|
||||||
|
EtaAdjustmentMinutes = table.Column<int>(type: "integer", nullable: false, comment: "配送时效加成(分钟)。"),
|
||||||
|
FreeDeliveryThreshold = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: true, comment: "免配送费门槛。"),
|
||||||
|
HourlyCapacityLimit = table.Column<int>(type: "integer", nullable: false, comment: "每小时配送上限。"),
|
||||||
|
MaxDeliveryDistance = table.Column<decimal>(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false, comment: "最大配送距离(公里)。"),
|
||||||
|
RadiusTiersJson = table.Column<string>(type: "text", nullable: true, comment: "半径梯度配置 JSON。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_store_delivery_settings", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "门店配送设置聚合。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "store_dinein_settings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true, comment: "是否启用堂食。"),
|
||||||
|
DefaultDiningMinutes = table.Column<int>(type: "integer", nullable: false, defaultValue: 90, comment: "默认用餐时长(分钟)。"),
|
||||||
|
OvertimeReminderMinutes = table.Column<int>(type: "integer", nullable: false, defaultValue: 10, comment: "超时提醒阈值(分钟)。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_store_dinein_settings", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "门店堂食基础设置。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "store_staff_templates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
MorningStartTime = table.Column<TimeSpan>(type: "interval", nullable: false, comment: "早班开始时间。"),
|
||||||
|
MorningEndTime = table.Column<TimeSpan>(type: "interval", nullable: false, comment: "早班结束时间。"),
|
||||||
|
EveningStartTime = table.Column<TimeSpan>(type: "interval", nullable: false, comment: "晚班开始时间。"),
|
||||||
|
EveningEndTime = table.Column<TimeSpan>(type: "interval", nullable: false, comment: "晚班结束时间。"),
|
||||||
|
FullStartTime = table.Column<TimeSpan>(type: "interval", nullable: false, comment: "全天班开始时间。"),
|
||||||
|
FullEndTime = table.Column<TimeSpan>(type: "interval", nullable: false, comment: "全天班结束时间。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_store_staff_templates", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "门店员工班次模板。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "store_staff_weekly_schedules",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
StaffId = table.Column<long>(type: "bigint", nullable: false, comment: "员工 ID。"),
|
||||||
|
DayOfWeek = table.Column<int>(type: "integer", nullable: false, comment: "星期(0=周一,6=周日)。"),
|
||||||
|
ShiftType = table.Column<int>(type: "integer", nullable: false, comment: "班次类型。"),
|
||||||
|
StartTime = table.Column<TimeSpan>(type: "interval", nullable: true, comment: "开始时间(休息时为空)。"),
|
||||||
|
EndTime = table.Column<TimeSpan>(type: "interval", nullable: true, comment: "结束时间(休息时为空)。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_store_staff_weekly_schedules", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "门店员工每周排班。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "tenant_visibility_role_rules",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
QuotaVisibleRoleCodes = table.Column<string[]>(type: "text[]", nullable: false, comment: "配额可见角色编码集合。"),
|
||||||
|
BillingVisibleRoleCodes = table.Column<string[]>(type: "text[]", nullable: false, comment: "账单可见角色编码集合。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: false, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_tenant_visibility_role_rules", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "租户账单/配额可见角色规则。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_store_delivery_settings_TenantId_StoreId",
|
||||||
|
table: "store_delivery_settings",
|
||||||
|
columns: new[] { "TenantId", "StoreId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_store_dinein_settings_TenantId_StoreId",
|
||||||
|
table: "store_dinein_settings",
|
||||||
|
columns: new[] { "TenantId", "StoreId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_store_staff_templates_TenantId_StoreId",
|
||||||
|
table: "store_staff_templates",
|
||||||
|
columns: new[] { "TenantId", "StoreId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_store_staff_weekly_schedules_TenantId_StoreId_DayOfWeek",
|
||||||
|
table: "store_staff_weekly_schedules",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "DayOfWeek" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_store_staff_weekly_schedules_TenantId_StoreId_StaffId_DayOf~",
|
||||||
|
table: "store_staff_weekly_schedules",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "StaffId", "DayOfWeek" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_tenant_visibility_role_rules_TenantId",
|
||||||
|
table: "tenant_visibility_role_rules",
|
||||||
|
column: "TenantId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "store_delivery_settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "store_dinein_settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "store_staff_templates");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "store_staff_weekly_schedules");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "tenant_visibility_role_rules");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FineRuleJson",
|
||||||
|
table: "store_pickup_settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Mode",
|
||||||
|
table: "store_pickup_settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CutleryFeeAmount",
|
||||||
|
table: "store_fees");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CutleryFeeEnabled",
|
||||||
|
table: "store_fees");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RushFeeAmount",
|
||||||
|
table: "store_fees");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RushFeeEnabled",
|
||||||
|
table: "store_fees");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Color",
|
||||||
|
table: "store_delivery_zones");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Priority",
|
||||||
|
table: "store_delivery_zones");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -654,7 +654,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Property<long?>("StoreId")
|
b.Property<long?>("StoreId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("所属门店(可空为平台)。");
|
.HasComment("所属门店(可空为系统会话)。");
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
b.Property<long>("TenantId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
@@ -1148,7 +1148,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Property<long?>("UserId")
|
b.Property<long?>("UserId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("用户 ID(如绑定平台账号)。");
|
.HasComment("用户 ID(如绑定登录账号)。");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
@@ -2517,13 +2517,6 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("character varying(512)")
|
.HasColumnType("character varying(512)")
|
||||||
.HasComment("审核备注或驳回原因。");
|
.HasComment("审核备注或驳回原因。");
|
||||||
|
|
||||||
b.Property<byte[]>("RowVersion")
|
|
||||||
.IsConcurrencyToken()
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasColumnType("bytea")
|
|
||||||
.HasComment("并发控制版本。");
|
|
||||||
|
|
||||||
b.Property<string>("ServicePhone")
|
b.Property<string>("ServicePhone")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasComment("客服电话。");
|
.HasComment("客服电话。");
|
||||||
@@ -2552,6 +2545,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<uint>("xmin")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasColumnType("xid")
|
||||||
|
.HasColumnName("xmin");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ClaimedBy");
|
b.HasIndex("ClaimedBy");
|
||||||
@@ -3791,7 +3790,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
b.Property<string>("TradeNo")
|
b.Property<string>("TradeNo")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)")
|
.HasColumnType("character varying(64)")
|
||||||
.HasComment("平台交易号。");
|
.HasComment("系统交易号。");
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
@@ -5228,6 +5227,84 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliverySetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<int>("EtaAdjustmentMinutes")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("配送时效加成(分钟)。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FreeDeliveryThreshold")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("numeric(10,2)")
|
||||||
|
.HasComment("免配送费门槛。");
|
||||||
|
|
||||||
|
b.Property<int>("HourlyCapacityLimit")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("每小时配送上限。");
|
||||||
|
|
||||||
|
b.Property<decimal>("MaxDeliveryDistance")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("numeric(10,2)")
|
||||||
|
.HasComment("最大配送距离(公里)。");
|
||||||
|
|
||||||
|
b.Property<int>("Mode")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("配送模式。");
|
||||||
|
|
||||||
|
b.Property<string>("RadiusTiersJson")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("半径梯度配置 JSON。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("store_delivery_settings", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("门店配送设置聚合。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -5237,6 +5314,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Color")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("区域颜色。");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasComment("创建时间(UTC)。");
|
.HasComment("创建时间(UTC)。");
|
||||||
@@ -5272,6 +5354,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasComment("GeoJSON 表示的多边形范围。");
|
.HasComment("GeoJSON 表示的多边形范围。");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(100)
|
||||||
|
.HasComment("优先级(数值越小越优先)。");
|
||||||
|
|
||||||
b.Property<int>("SortOrder")
|
b.Property<int>("SortOrder")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasComment("排序值。");
|
.HasComment("排序值。");
|
||||||
@@ -5308,6 +5396,76 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDineInSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<int>("DefaultDiningMinutes")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(90)
|
||||||
|
.HasComment("默认用餐时长(分钟)。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true)
|
||||||
|
.HasComment("是否启用堂食。");
|
||||||
|
|
||||||
|
b.Property<int>("OvertimeReminderMinutes")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(10)
|
||||||
|
.HasComment("超时提醒阈值(分钟)。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("store_dinein_settings", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("门店堂食基础设置。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -5407,6 +5565,17 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<decimal>("CutleryFeeAmount")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("numeric(10,2)")
|
||||||
|
.HasComment("餐具费金额。");
|
||||||
|
|
||||||
|
b.Property<bool>("CutleryFeeEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasComment("是否启用餐具费。");
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
b.Property<DateTime?>("DeletedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasComment("软删除时间(UTC),未删除时为 null。");
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
@@ -5430,18 +5599,29 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("numeric(10,2)")
|
.HasColumnType("numeric(10,2)")
|
||||||
.HasComment("起送费(元)。");
|
.HasComment("起送费(元)。");
|
||||||
|
|
||||||
b.Property<int>("PackagingFeeMode")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasComment("打包费模式。");
|
|
||||||
|
|
||||||
b.Property<int>("OrderPackagingFeeMode")
|
b.Property<int>("OrderPackagingFeeMode")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasComment("订单打包费规则(按订单收费时生效)。");
|
.HasComment("订单打包费规则(按订单收费时生效)。");
|
||||||
|
|
||||||
|
b.Property<int>("PackagingFeeMode")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("打包费模式。");
|
||||||
|
|
||||||
b.Property<string>("PackagingFeeTiersJson")
|
b.Property<string>("PackagingFeeTiersJson")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasComment("阶梯打包费配置(JSON)。");
|
.HasComment("阶梯打包费配置(JSON)。");
|
||||||
|
|
||||||
|
b.Property<decimal>("RushFeeAmount")
|
||||||
|
.HasPrecision(10, 2)
|
||||||
|
.HasColumnType("numeric(10,2)")
|
||||||
|
.HasComment("加急费金额。");
|
||||||
|
|
||||||
|
b.Property<bool>("RushFeeEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false)
|
||||||
|
.HasComment("是否启用加急费。");
|
||||||
|
|
||||||
b.Property<long>("StoreId")
|
b.Property<long>("StoreId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("门店标识。");
|
.HasComment("门店标识。");
|
||||||
@@ -5600,10 +5780,18 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("FineRuleJson")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("精细规则 JSON。");
|
||||||
|
|
||||||
b.Property<int?>("MaxQuantityPerOrder")
|
b.Property<int?>("MaxQuantityPerOrder")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasComment("单笔自提最大份数。");
|
.HasComment("单笔自提最大份数。");
|
||||||
|
|
||||||
|
b.Property<int>("Mode")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("自提配置模式。");
|
||||||
|
|
||||||
b.Property<byte[]>("RowVersion")
|
b.Property<byte[]>("RowVersion")
|
||||||
.IsConcurrencyToken()
|
.IsConcurrencyToken()
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -5817,6 +6005,156 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreStaffTemplate", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("EveningEndTime")
|
||||||
|
.HasColumnType("interval")
|
||||||
|
.HasComment("晚班结束时间。");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("EveningStartTime")
|
||||||
|
.HasColumnType("interval")
|
||||||
|
.HasComment("晚班开始时间。");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("FullEndTime")
|
||||||
|
.HasColumnType("interval")
|
||||||
|
.HasComment("全天班结束时间。");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("FullStartTime")
|
||||||
|
.HasColumnType("interval")
|
||||||
|
.HasComment("全天班开始时间。");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("MorningEndTime")
|
||||||
|
.HasColumnType("interval")
|
||||||
|
.HasComment("早班结束时间。");
|
||||||
|
|
||||||
|
b.Property<TimeSpan>("MorningStartTime")
|
||||||
|
.HasColumnType("interval")
|
||||||
|
.HasComment("早班开始时间。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("store_staff_templates", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("门店员工班次模板。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreStaffWeeklySchedule", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<int>("DayOfWeek")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("星期(0=周一,6=周日)。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<TimeSpan?>("EndTime")
|
||||||
|
.HasColumnType("interval")
|
||||||
|
.HasComment("结束时间(休息时为空)。");
|
||||||
|
|
||||||
|
b.Property<int>("ShiftType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("班次类型。");
|
||||||
|
|
||||||
|
b.Property<long>("StaffId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("员工 ID。");
|
||||||
|
|
||||||
|
b.Property<TimeSpan?>("StartTime")
|
||||||
|
.HasColumnType("interval")
|
||||||
|
.HasComment("开始时间(休息时为空)。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "DayOfWeek");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "StaffId", "DayOfWeek")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("store_staff_weekly_schedules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("门店员工每周排班。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -6038,7 +6376,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.ToTable("quota_packages", null, t =>
|
b.ToTable("quota_packages", null, t =>
|
||||||
{
|
{
|
||||||
t.HasComment("配额包定义(平台提供的可购买配额包)。");
|
t.HasComment("配额包定义(系统提供的可购买配额包)。");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6191,7 +6529,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.ToTable("tenants", null, t =>
|
b.ToTable("tenants", null, t =>
|
||||||
{
|
{
|
||||||
t.HasComment("平台租户信息,描述租户的生命周期与基础资料。");
|
t.HasComment("租户信息,描述租户的生命周期与基础资料。");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6255,7 +6593,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Property<long?>("PublisherUserId")
|
b.Property<long?>("PublisherUserId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("发布者用户 ID(平台或租户后台账号)。");
|
.HasComment("发布者用户 ID(系统或租户后台账号)。");
|
||||||
|
|
||||||
b.Property<DateTime?>("RevokedAt")
|
b.Property<DateTime?>("RevokedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
@@ -6629,7 +6967,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Property<bool>("IsActive")
|
b.Property<bool>("IsActive")
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasComment("是否仍启用(平台控制)。");
|
.HasComment("是否仍启用(系统控制)。");
|
||||||
|
|
||||||
b.Property<bool>("IsAllowNewTenantPurchase")
|
b.Property<bool>("IsAllowNewTenantPurchase")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -6720,7 +7058,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.ToTable("tenant_packages", null, t =>
|
b.ToTable("tenant_packages", null, t =>
|
||||||
{
|
{
|
||||||
t.HasComment("平台提供的租户套餐定义。");
|
t.HasComment("系统提供的租户套餐定义。");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7051,75 +7389,6 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantReviewClaim", b =>
|
|
||||||
{
|
|
||||||
b.Property<long>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasComment("实体唯一标识。");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
|
||||||
|
|
||||||
b.Property<DateTime>("ClaimedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasComment("领取时间(UTC)。");
|
|
||||||
|
|
||||||
b.Property<long>("ClaimedBy")
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasComment("领取人用户 ID。");
|
|
||||||
|
|
||||||
b.Property<string>("ClaimedByName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)")
|
|
||||||
.HasComment("领取人名称(展示用快照)。");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasComment("创建时间(UTC)。");
|
|
||||||
|
|
||||||
b.Property<long?>("CreatedBy")
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasComment("软删除时间(UTC),未删除时为 null。");
|
|
||||||
|
|
||||||
b.Property<long?>("DeletedBy")
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("ReleasedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasComment("释放时间(UTC),未释放时为 null。");
|
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasComment("被领取的租户 ID。");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("UpdatedAt")
|
|
||||||
.HasColumnType("timestamp with time zone")
|
|
||||||
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
|
||||||
|
|
||||||
b.Property<long?>("UpdatedBy")
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ClaimedBy");
|
|
||||||
|
|
||||||
b.HasIndex("TenantId")
|
|
||||||
.IsUnique()
|
|
||||||
.HasFilter("\"ReleasedAt\" IS NULL AND \"DeletedAt\" IS NULL");
|
|
||||||
|
|
||||||
b.ToTable("tenant_review_claims", null, t =>
|
|
||||||
{
|
|
||||||
t.HasComment("租户入驻审核领取记录(防止多管理员并发审核)。");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -7407,6 +7676,64 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantVisibilityRoleRule", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("BillingVisibleRoleCodes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasComment("账单可见角色编码集合。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string[]>("QuotaVisibleRoleCodes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text[]")
|
||||||
|
.HasComment("配额可见角色编码集合。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("tenant_visibility_role_rules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("租户账单/配额可见角色规则。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null)
|
b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null)
|
||||||
|
|||||||
Reference in New Issue
Block a user