diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreDeliveryContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreDeliveryContracts.cs new file mode 100644 index 0000000..aa45951 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreDeliveryContracts.cs @@ -0,0 +1,147 @@ +namespace TakeoutSaaS.TenantApi.Contracts.Store; + +/// +/// 半径梯度。 +/// +public sealed class RadiusTierDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// MinDistance。 + /// + public decimal MinDistance { get; set; } + /// + /// MaxDistance。 + /// + public decimal MaxDistance { get; set; } + /// + /// DeliveryFee。 + /// + public decimal DeliveryFee { get; set; } + /// + /// EtaMinutes。 + /// + public int EtaMinutes { get; set; } + /// + /// MinOrderAmount。 + /// + public decimal MinOrderAmount { get; set; } + /// + /// Color。 + /// + public string Color { get; set; } = "#1677ff"; +} + +/// +/// 多边形区域。 +/// +public sealed class PolygonZoneDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// Name。 + /// + public string Name { get; set; } = string.Empty; + /// + /// Color。 + /// + public string Color { get; set; } = "#1677ff"; + /// + /// DeliveryFee。 + /// + public decimal DeliveryFee { get; set; } + /// + /// EtaMinutes。 + /// + public int EtaMinutes { get; set; } + /// + /// MinOrderAmount。 + /// + public decimal MinOrderAmount { get; set; } + /// + /// Priority。 + /// + public int Priority { get; set; } +} + +/// +/// 通用配送设置。 +/// +public sealed class DeliveryGeneralSettingsDto +{ + /// + /// EtaAdjustmentMinutes。 + /// + public int EtaAdjustmentMinutes { get; set; } + /// + /// FreeDeliveryThreshold。 + /// + public decimal? FreeDeliveryThreshold { get; set; } + /// + /// HourlyCapacityLimit。 + /// + public int HourlyCapacityLimit { get; set; } + /// + /// MaxDeliveryDistance。 + /// + public decimal MaxDeliveryDistance { get; set; } +} + +/// +/// 门店配送设置聚合。 +/// +public sealed class StoreDeliverySettingsDto +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Mode。 + /// + public string Mode { get; set; } = "radius"; + /// + /// RadiusTiers。 + /// + public List RadiusTiers { get; set; } = []; + /// + /// PolygonZones。 + /// + public List PolygonZones { get; set; } = []; + /// + /// GeneralSettings。 + /// + public DeliveryGeneralSettingsDto GeneralSettings { get; set; } = new(); +} + +/// +/// 复制配送设置请求。 +/// +public sealed class CopyStoreDeliverySettingsRequest +{ + /// + /// SourceStoreId。 + /// + public string SourceStoreId { get; set; } = string.Empty; + /// + /// TargetStoreIds。 + /// + public List TargetStoreIds { get; set; } = []; +} + +/// +/// 复制结果。 +/// +public sealed class CopyStoreDeliverySettingsResult +{ + /// + /// CopiedCount。 + /// + public int CopiedCount { get; set; } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreDineInContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreDineInContracts.cs new file mode 100644 index 0000000..210108d --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreDineInContracts.cs @@ -0,0 +1,240 @@ +namespace TakeoutSaaS.TenantApi.Contracts.Store; + +/// +/// 堂食基础设置。 +/// +public sealed class DineInBasicSettingsDto +{ + /// + /// Enabled。 + /// + public bool Enabled { get; set; } + /// + /// DefaultDiningMinutes。 + /// + public int DefaultDiningMinutes { get; set; } + /// + /// OvertimeReminderMinutes。 + /// + public int OvertimeReminderMinutes { get; set; } +} + +/// +/// 堂食区域。 +/// +public sealed class DineInAreaDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// Name。 + /// + public string Name { get; set; } = string.Empty; + /// + /// Description。 + /// + public string Description { get; set; } = string.Empty; + /// + /// Sort。 + /// + public int Sort { get; set; } +} + +/// +/// 堂食桌位。 +/// +public sealed class DineInTableDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// AreaId。 + /// + public string AreaId { get; set; } = string.Empty; + /// + /// Code。 + /// + public string Code { get; set; } = string.Empty; + /// + /// Seats。 + /// + public int Seats { get; set; } + /// + /// Status。 + /// + public string Status { get; set; } = "free"; + /// + /// Tags。 + /// + public List Tags { get; set; } = []; +} + +/// +/// 堂食设置聚合。 +/// +public sealed class StoreDineInSettingsDto +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// BasicSettings。 + /// + public DineInBasicSettingsDto BasicSettings { get; set; } = new(); + /// + /// Areas。 + /// + public List Areas { get; set; } = []; + /// + /// Tables。 + /// + public List Tables { get; set; } = []; +} + +/// +/// 保存基础设置请求。 +/// +public sealed class SaveStoreDineInBasicSettingsRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// BasicSettings。 + /// + public DineInBasicSettingsDto BasicSettings { get; set; } = new(); +} + +/// +/// 保存区域请求。 +/// +public sealed class SaveDineInAreaRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Area。 + /// + public DineInAreaDto Area { get; set; } = new(); +} + +/// +/// 删除区域请求。 +/// +public sealed class DeleteDineInAreaRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// AreaId。 + /// + public string AreaId { get; set; } = string.Empty; +} + +/// +/// 保存桌位请求。 +/// +public sealed class SaveDineInTableRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Table。 + /// + public DineInTableDto Table { get; set; } = new(); +} + +/// +/// 删除桌位请求。 +/// +public sealed class DeleteDineInTableRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// TableId。 + /// + public string TableId { get; set; } = string.Empty; +} + +/// +/// 批量生成桌位请求。 +/// +public sealed class BatchCreateDineInTablesRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// AreaId。 + /// + public string AreaId { get; set; } = string.Empty; + /// + /// CodePrefix。 + /// + public string CodePrefix { get; set; } = string.Empty; + /// + /// StartNumber。 + /// + public int StartNumber { get; set; } + /// + /// Count。 + /// + public int Count { get; set; } + /// + /// Seats。 + /// + public int Seats { get; set; } +} + +/// +/// 批量生成桌位结果。 +/// +public sealed class BatchCreateDineInTablesResultDto +{ + /// + /// CreatedTables。 + /// + public List CreatedTables { get; set; } = []; +} + +/// +/// 复制堂食设置请求。 +/// +public sealed class CopyStoreDineInSettingsRequest +{ + /// + /// SourceStoreId。 + /// + public string SourceStoreId { get; set; } = string.Empty; + /// + /// TargetStoreIds。 + /// + public List TargetStoreIds { get; set; } = []; +} + +/// +/// 复制结果。 +/// +public sealed class CopyStoreDineInSettingsResult +{ + /// + /// CopiedCount。 + /// + public int CopiedCount { get; set; } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreFeesContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreFeesContracts.cs new file mode 100644 index 0000000..7c94586 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreFeesContracts.cs @@ -0,0 +1,127 @@ +namespace TakeoutSaaS.TenantApi.Contracts.Store; + +/// +/// 阶梯包装费。 +/// +public sealed class PackagingFeeTierDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// MinAmount。 + /// + public decimal MinAmount { get; set; } + /// + /// MaxAmount。 + /// + public decimal? MaxAmount { get; set; } + /// + /// Fee。 + /// + public decimal Fee { get; set; } + /// + /// Sort。 + /// + public int Sort { get; set; } +} + +/// +/// 附加费用项。 +/// +public sealed class AdditionalFeeItemDto +{ + /// + /// Enabled。 + /// + public bool Enabled { get; set; } + /// + /// Amount。 + /// + public decimal Amount { get; set; } +} + +/// +/// 其他费用。 +/// +public sealed class StoreOtherFeesDto +{ + /// + /// Cutlery。 + /// + public AdditionalFeeItemDto Cutlery { get; set; } = new(); + /// + /// Rush。 + /// + public AdditionalFeeItemDto Rush { get; set; } = new(); +} + +/// +/// 门店费用设置。 +/// +public sealed class StoreFeesSettingsDto +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// MinimumOrderAmount。 + /// + public decimal MinimumOrderAmount { get; set; } + /// + /// BaseDeliveryFee。 + /// + public decimal BaseDeliveryFee { get; set; } + /// + /// FreeDeliveryThreshold。 + /// + public decimal? FreeDeliveryThreshold { get; set; } + /// + /// PackagingFeeMode。 + /// + public string PackagingFeeMode { get; set; } = "order"; + /// + /// OrderPackagingFeeMode。 + /// + public string OrderPackagingFeeMode { get; set; } = "fixed"; + /// + /// FixedPackagingFee。 + /// + public decimal FixedPackagingFee { get; set; } + /// + /// PackagingFeeTiers。 + /// + public List PackagingFeeTiers { get; set; } = []; + /// + /// OtherFees。 + /// + public StoreOtherFeesDto OtherFees { get; set; } = new(); +} + +/// +/// 复制费用请求。 +/// +public sealed class CopyStoreFeesSettingsRequest +{ + /// + /// SourceStoreId。 + /// + public string SourceStoreId { get; set; } = string.Empty; + /// + /// TargetStoreIds。 + /// + public List TargetStoreIds { get; set; } = []; +} + +/// +/// 复制结果。 +/// +public sealed class CopyStoreFeesSettingsResult +{ + /// + /// CopiedCount。 + /// + public int CopiedCount { get; set; } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreHoursContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreHoursContracts.cs new file mode 100644 index 0000000..4d5aafb --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreHoursContracts.cs @@ -0,0 +1,229 @@ +namespace TakeoutSaaS.TenantApi.Contracts.Store; + +/// +/// 时段类型。 +/// +public enum StoreHourSlotType +{ + /// + /// 营业时段。 + /// + Business = 1, + + /// + /// 配送时段。 + /// + Delivery = 2, + + /// + /// 自提时段。 + /// + Pickup = 3 +} + +/// +/// 特殊日期类型。 +/// +public enum StoreHolidayType +{ + /// + /// 休息日。 + /// + Closed = 1, + + /// + /// 特殊营业日。 + /// + Special = 2 +} + +/// +/// 时段 DTO。 +/// +public sealed class StoreHourTimeSlotDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// Type。 + /// + public int Type { get; set; } + /// + /// StartTime。 + /// + public string StartTime { get; set; } = string.Empty; + /// + /// EndTime。 + /// + public string EndTime { get; set; } = string.Empty; + /// + /// Capacity。 + /// + public int? Capacity { get; set; } + /// + /// Remark。 + /// + public string? Remark { get; set; } +} + +/// +/// 每日营业时间 DTO。 +/// +public sealed class StoreHourDayHoursDto +{ + /// + /// DayOfWeek。 + /// + public int DayOfWeek { get; set; } + /// + /// IsOpen。 + /// + public bool IsOpen { get; set; } + /// + /// Slots。 + /// + public List Slots { get; set; } = []; +} + +/// +/// 特殊日期 DTO。 +/// +public sealed class StoreHourHolidayDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// StartDate。 + /// + public string StartDate { get; set; } = string.Empty; + /// + /// EndDate。 + /// + public string EndDate { get; set; } = string.Empty; + /// + /// Type。 + /// + public int Type { get; set; } + /// + /// StartTime。 + /// + public string? StartTime { get; set; } + /// + /// EndTime。 + /// + public string? EndTime { get; set; } + /// + /// Reason。 + /// + public string Reason { get; set; } = string.Empty; + /// + /// Remark。 + /// + public string? Remark { get; set; } +} + +/// +/// 门店营业时间聚合。 +/// +public sealed class StoreHoursDto +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// WeeklyHours。 + /// + public List WeeklyHours { get; set; } = []; + /// + /// Holidays。 + /// + public List Holidays { get; set; } = []; +} + +/// +/// 保存每周时段请求。 +/// +public sealed class SaveWeeklyHoursRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// WeeklyHours。 + /// + public List WeeklyHours { get; set; } = []; +} + +/// +/// 保存特殊日期请求。 +/// +public sealed class SaveHolidayRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Holiday。 + /// + public StoreHourHolidayDto Holiday { get; set; } = new(); +} + +/// +/// 删除特殊日期请求。 +/// +public sealed class DeleteHolidayRequest +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; +} + +/// +/// 复制营业时间请求。 +/// +public sealed class CopyStoreHoursRequest +{ + /// + /// SourceStoreId。 + /// + public string SourceStoreId { get; set; } = string.Empty; + /// + /// TargetStoreIds。 + /// + public List TargetStoreIds { get; set; } = []; + /// + /// IncludeWeeklyHours。 + /// + public bool? IncludeWeeklyHours { get; set; } + /// + /// IncludeHolidays。 + /// + public bool? IncludeHolidays { get; set; } +} + +/// +/// 复制结果。 +/// +public sealed class CopyStoreHoursResult +{ + /// + /// CopiedCount。 + /// + public int CopiedCount { get; set; } + /// + /// IncludeWeeklyHours。 + /// + public bool IncludeWeeklyHours { get; set; } + /// + /// IncludeHolidays。 + /// + public bool IncludeHolidays { get; set; } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StorePickupContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StorePickupContracts.cs new file mode 100644 index 0000000..50e7247 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StorePickupContracts.cs @@ -0,0 +1,250 @@ +namespace TakeoutSaaS.TenantApi.Contracts.Store; + +/// +/// 自提基础设置。 +/// +public sealed class PickupBasicSettingsDto +{ + /// + /// AllowSameDayPickup。 + /// + public bool AllowSameDayPickup { get; set; } + /// + /// BookingDays。 + /// + public int BookingDays { get; set; } + /// + /// MaxItemsPerOrder。 + /// + public int? MaxItemsPerOrder { get; set; } +} + +/// +/// 自提大时段。 +/// +public sealed class PickupSlotDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// Name。 + /// + public string Name { get; set; } = string.Empty; + /// + /// StartTime。 + /// + public string StartTime { get; set; } = string.Empty; + /// + /// EndTime。 + /// + public string EndTime { get; set; } = string.Empty; + /// + /// CutoffMinutes。 + /// + public int CutoffMinutes { get; set; } + /// + /// Capacity。 + /// + public int Capacity { get; set; } + /// + /// ReservedCount。 + /// + public int ReservedCount { get; set; } + /// + /// DayOfWeeks。 + /// + public List DayOfWeeks { get; set; } = []; + /// + /// Enabled。 + /// + public bool Enabled { get; set; } +} + +/// +/// 精细规则。 +/// +public sealed class PickupFineRuleDto +{ + /// + /// IntervalMinutes。 + /// + public int IntervalMinutes { get; set; } + /// + /// SlotCapacity。 + /// + public int SlotCapacity { get; set; } + /// + /// DayStartTime。 + /// + public string DayStartTime { get; set; } = string.Empty; + /// + /// DayEndTime。 + /// + public string DayEndTime { get; set; } = string.Empty; + /// + /// MinAdvanceHours。 + /// + public int MinAdvanceHours { get; set; } + /// + /// DayOfWeeks。 + /// + public List DayOfWeeks { get; set; } = []; +} + +/// +/// 预览时段。 +/// +public sealed class PickupPreviewSlotDto +{ + /// + /// Time。 + /// + public string Time { get; set; } = string.Empty; + /// + /// Status。 + /// + public string Status { get; set; } = "available"; + /// + /// RemainingCount。 + /// + public int RemainingCount { get; set; } +} + +/// +/// 预览日期。 +/// +public sealed class PickupPreviewDayDto +{ + /// + /// Date。 + /// + public string Date { get; set; } = string.Empty; + /// + /// Label。 + /// + public string Label { get; set; } = string.Empty; + /// + /// SubLabel。 + /// + public string SubLabel { get; set; } = string.Empty; + /// + /// Slots。 + /// + public List Slots { get; set; } = []; +} + +/// +/// 门店自提设置。 +/// +public sealed class StorePickupSettingsDto +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Mode。 + /// + public string Mode { get; set; } = "big"; + /// + /// BasicSettings。 + /// + public PickupBasicSettingsDto BasicSettings { get; set; } = new(); + /// + /// BigSlots。 + /// + public List BigSlots { get; set; } = []; + /// + /// FineRule。 + /// + public PickupFineRuleDto FineRule { get; set; } = new(); + /// + /// PreviewDays。 + /// + public List PreviewDays { get; set; } = []; +} + +/// +/// 保存基础设置请求。 +/// +public sealed class SavePickupBasicSettingsRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Mode。 + /// + public string? Mode { get; set; } + /// + /// BasicSettings。 + /// + public PickupBasicSettingsDto BasicSettings { get; set; } = new(); +} + +/// +/// 保存大时段请求。 +/// +public sealed class SavePickupSlotsRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Mode。 + /// + public string? Mode { get; set; } + /// + /// Slots。 + /// + public List Slots { get; set; } = []; +} + +/// +/// 保存精细规则请求。 +/// +public sealed class SavePickupFineRuleRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Mode。 + /// + public string? Mode { get; set; } + /// + /// FineRule。 + /// + public PickupFineRuleDto FineRule { get; set; } = new(); +} + +/// +/// 复制自提设置请求。 +/// +public sealed class CopyStorePickupSettingsRequest +{ + /// + /// SourceStoreId。 + /// + public string SourceStoreId { get; set; } = string.Empty; + /// + /// TargetStoreIds。 + /// + public List TargetStoreIds { get; set; } = []; +} + +/// +/// 复制结果。 +/// +public sealed class CopyStorePickupSettingsResult +{ + /// + /// CopiedCount。 + /// + public int CopiedCount { get; set; } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreStaffContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreStaffContracts.cs new file mode 100644 index 0000000..c2a5dde --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Store/StoreStaffContracts.cs @@ -0,0 +1,299 @@ +namespace TakeoutSaaS.TenantApi.Contracts.Store; + +/// +/// 分页结构。 +/// +public sealed class PaginatedResultDto +{ + /// + /// Items。 + /// + public List Items { get; set; } = []; + /// + /// Total。 + /// + public int Total { get; set; } + /// + /// Page。 + /// + public int Page { get; set; } + /// + /// PageSize。 + /// + public int PageSize { get; set; } +} + +/// +/// 员工档案。 +/// +public sealed class StoreStaffItemDto +{ + /// + /// Id。 + /// + public string Id { get; set; } = string.Empty; + /// + /// Name。 + /// + public string Name { get; set; } = string.Empty; + /// + /// Phone。 + /// + public string Phone { get; set; } = string.Empty; + /// + /// Email。 + /// + public string Email { get; set; } = string.Empty; + /// + /// RoleType。 + /// + public string RoleType { get; set; } = "cashier"; + /// + /// Status。 + /// + public string Status { get; set; } = "active"; + /// + /// Permissions。 + /// + public List Permissions { get; set; } = []; + /// + /// AvatarColor。 + /// + public string AvatarColor { get; set; } = "#1677ff"; + /// + /// HiredAt。 + /// + public string HiredAt { get; set; } = string.Empty; +} + +/// +/// 班次时间段模板。 +/// +public sealed class ShiftTemplateItemDto +{ + /// + /// StartTime。 + /// + public string StartTime { get; set; } = string.Empty; + /// + /// EndTime。 + /// + public string EndTime { get; set; } = string.Empty; +} + +/// +/// 门店班次模板。 +/// +public sealed class StoreShiftTemplatesDto +{ + /// + /// Morning。 + /// + public ShiftTemplateItemDto Morning { get; set; } = new(); + /// + /// Evening。 + /// + public ShiftTemplateItemDto Evening { get; set; } = new(); + /// + /// Full。 + /// + public ShiftTemplateItemDto Full { get; set; } = new(); +} + +/// +/// 员工单日排班。 +/// +public sealed class StaffDayShiftDto +{ + /// + /// DayOfWeek。 + /// + public int DayOfWeek { get; set; } + /// + /// ShiftType。 + /// + public string ShiftType { get; set; } = "off"; + /// + /// StartTime。 + /// + public string StartTime { get; set; } = string.Empty; + /// + /// EndTime。 + /// + public string EndTime { get; set; } = string.Empty; +} + +/// +/// 员工排班。 +/// +public sealed class StaffScheduleDto +{ + /// + /// StaffId。 + /// + public string StaffId { get; set; } = string.Empty; + /// + /// Shifts。 + /// + public List Shifts { get; set; } = []; +} + +/// +/// 门店排班聚合。 +/// +public sealed class StoreStaffScheduleDto +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// WeekStartDate。 + /// + public string WeekStartDate { get; set; } = string.Empty; + /// + /// Templates。 + /// + public StoreShiftTemplatesDto Templates { get; set; } = new(); + /// + /// Schedules。 + /// + public List Schedules { get; set; } = []; +} + +/// +/// 保存员工请求。 +/// +public sealed class SaveStoreStaffRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Id。 + /// + public string? Id { get; set; } + /// + /// Name。 + /// + public string Name { get; set; } = string.Empty; + /// + /// Phone。 + /// + public string Phone { get; set; } = string.Empty; + /// + /// Email。 + /// + public string Email { get; set; } = string.Empty; + /// + /// RoleType。 + /// + public string RoleType { get; set; } = "cashier"; + /// + /// Status。 + /// + public string Status { get; set; } = "active"; + /// + /// Permissions。 + /// + public List Permissions { get; set; } = []; +} + +/// +/// 删除员工请求。 +/// +public sealed class DeleteStoreStaffRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// StaffId。 + /// + public string StaffId { get; set; } = string.Empty; +} + +/// +/// 保存班次模板请求。 +/// +public sealed class SaveStoreStaffTemplatesRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Templates。 + /// + public StoreShiftTemplatesDto Templates { get; set; } = new(); +} + +/// +/// 保存个人排班请求。 +/// +public sealed class SaveStoreStaffPersonalScheduleRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// StaffId。 + /// + public string StaffId { get; set; } = string.Empty; + /// + /// Shifts。 + /// + public List Shifts { get; set; } = []; +} + +/// +/// 保存周排班请求。 +/// +public sealed class SaveStoreStaffWeeklyScheduleRequest +{ + /// + /// StoreId。 + /// + public string StoreId { get; set; } = string.Empty; + /// + /// Schedules。 + /// + public List Schedules { get; set; } = []; +} + +/// +/// 复制排班请求。 +/// +public sealed class CopyStoreStaffScheduleRequest +{ + /// + /// SourceStoreId。 + /// + public string SourceStoreId { get; set; } = string.Empty; + /// + /// TargetStoreIds。 + /// + public List TargetStoreIds { get; set; } = []; + /// + /// CopyScope。 + /// + public string CopyScope { get; set; } = string.Empty; +} + +/// +/// 复制排班结果。 +/// +public sealed class CopyStoreStaffScheduleResult +{ + /// + /// CopiedCount。 + /// + public int CopiedCount { get; set; } + /// + /// CopyScope。 + /// + public string CopyScope { get; set; } = "template_and_schedule"; +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreApiHelpers.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreApiHelpers.cs new file mode 100644 index 0000000..2c7978f --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreApiHelpers.cs @@ -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 ParseSnowflakeList(IEnumerable? 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? 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 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 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> FilterAccessibleStoreIdsAsync( + TakeoutAppDbContext dbContext, + long tenantId, + long merchantId, + IEnumerable 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); + } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreDeliveryController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreDeliveryController.cs new file mode 100644 index 0000000..4f0d09f --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreDeliveryController.cs @@ -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; + +/// +/// 门店配送设置模块。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/store")] +public sealed class StoreDeliveryController( + TakeoutAppDbContext dbContext, + StoreContextService storeContextService) : BaseApiController +{ + /// + /// 获取门店配送设置。 + /// + [HttpGet("delivery")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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 + } + }); + } + + /// + /// 保存门店配送设置。 + /// + [HttpPost("delivery/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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(); + + 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.Ok(null); + } + + /// + /// 复制门店配送设置。 + /// + [HttpPost("delivery/copy")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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.Ok(new CopyStoreDeliverySettingsResult + { + CopiedCount = accessibleTargetIds.Count + }); + } + + private static List ParseRadiusTiers(string? raw) + { + if (!string.IsNullOrWhiteSpace(raw)) + { + try + { + var parsed = JsonSerializer.Deserialize>(raw, StoreApiHelpers.JsonOptions); + if (parsed is not null && parsed.Count > 0) + { + return NormalizeRadiusTiers(parsed); + } + } + catch + { + // 忽略配置反序列化异常并回落默认值 + } + } + + return NormalizeRadiusTiers(new List + { + 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 NormalizeRadiusTiers(IEnumerable? 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 + }; + } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreDineInController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreDineInController.cs new file mode 100644 index 0000000..2dd74da --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreDineInController.cs @@ -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; + +/// +/// 门店堂食管理模块。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/store")] +public sealed class StoreDineInController( + TakeoutAppDbContext dbContext, + StoreContextService storeContextService) : BaseApiController +{ + /// + /// 获取门店堂食设置。 + /// + [HttpGet("dinein")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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() + }); + } + + /// + /// 保存门店堂食基础设置。 + /// + [HttpPost("dinein/basic/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(null); + } + + /// + /// 保存堂食区域。 + /// + [HttpPost("dinein/area/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(MapArea(area)); + } + + /// + /// 删除堂食区域。 + /// + [HttpPost("dinein/area/delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(null); + } + + dbContext.StoreTableAreas.Remove(area); + await dbContext.SaveChangesAsync(cancellationToken); + return ApiResponse.Ok(null); + } + + /// + /// 保存堂食桌位。 + /// + [HttpPost("dinein/table/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(MapTable(table)); + } + + /// + /// 删除堂食桌位。 + /// + [HttpPost("dinein/table/delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(null); + } + + dbContext.StoreTables.Remove(table); + await dbContext.SaveChangesAsync(cancellationToken); + return ApiResponse.Ok(null); + } + + /// + /// 批量生成堂食桌位。 + /// + [HttpPost("dinein/table/batch-create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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(existingCodes, StringComparer.OrdinalIgnoreCase); + + var created = new List(); + 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.Ok(new BatchCreateDineInTablesResultDto + { + CreatedTables = created.Select(MapTable).ToList() + }); + } + + /// + /// 复制堂食设置。 + /// + [HttpPost("dinein/copy")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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(); + 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.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() + }; + } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreFeesController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreFeesController.cs new file mode 100644 index 0000000..5501c83 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreFeesController.cs @@ -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; + +/// +/// 门店费用设置模块。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/store")] +public sealed class StoreFeesController( + IMediator mediator, + TakeoutAppDbContext dbContext, + StoreContextService storeContextService) : BaseApiController +{ + /// + /// 获取门店费用设置。 + /// + [HttpGet("fees")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(response); + } + + /// + /// 保存门店费用设置。 + /// + [HttpPost("fees/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(MapFeeSettings(parsedStoreId, result)); + } + + /// + /// 复制费用设置。 + /// + [HttpPost("fees/copy")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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.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"; + } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreHoursController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreHoursController.cs new file mode 100644 index 0000000..18591b2 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreHoursController.cs @@ -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; + +/// +/// 门店营业时间模块。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/store")] +public sealed class StoreHoursController( + TakeoutAppDbContext dbContext, + StoreContextService storeContextService) : BaseApiController +{ + /// + /// 获取门店营业时间。 + /// + [HttpGet("hours")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(result); + } + + /// + /// 保存每周营业时间。 + /// + [HttpPost("hours/weekly")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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(); + 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.Ok(null); + } + + /// + /// 保存特殊日期。 + /// + [HttpPost("hours/holiday")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(result); + } + + /// + /// 删除特殊日期。 + /// + [HttpPost("hours/holiday/delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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.Ok(null); + } + + /// + /// 复制营业时间到其他门店。 + /// + [HttpPost("hours/copy")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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.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 + } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/StorePickupController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/StorePickupController.cs new file mode 100644 index 0000000..8ffccc9 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/StorePickupController.cs @@ -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; + +/// +/// 门店自提设置模块。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/store")] +public sealed class StorePickupController( + TakeoutAppDbContext dbContext, + StoreContextService storeContextService) : BaseApiController +{ + /// + /// 获取门店自提设置。 + /// + [HttpGet("pickup")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(response); + } + + /// + /// 保存自提基础设置。 + /// + [HttpPost("pickup/basic/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(null); + } + + /// + /// 保存自提大时段。 + /// + [HttpPost("pickup/slots/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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(); + 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.Ok(null); + } + + /// + /// 保存自提精细规则。 + /// + [HttpPost("pickup/fine-rule/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(null); + } + + /// + /// 复制自提设置。 + /// + [HttpPost("pickup/copy")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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.Ok(new CopyStorePickupSettingsResult + { + CopiedCount = accessibleTargetIds.Count + }); + } + + private async Task 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(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 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(); + 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 BuildPreviewSlots( + DateTime date, + int startMinutes, + int endMinutes, + int intervalMinutes, + int slotCapacity, + int minAdvanceHours) + { + var now = DateTime.Now; + var results = new List(); + 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 => "周日", + _ => "周一" + }; + } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreStaffController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreStaffController.cs new file mode 100644 index 0000000..2320a35 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/StoreStaffController.cs @@ -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; + +/// +/// 门店员工与排班模块。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/store")] +public sealed class StoreStaffController( + TakeoutAppDbContext dbContext, + StoreContextService storeContextService) : BaseApiController +{ + /// + /// 获取员工分页列表。 + /// + [HttpGet("staff")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> 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 + { + Items = staffs.Select(MapStaff).ToList(), + Total = total, + Page = normalizedPage, + PageSize = normalizedPageSize + }; + + return ApiResponse>.Ok(result); + } + + /// + /// 保存员工。 + /// + [HttpPost("staff/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(MapStaff(entity)); + } + + /// + /// 删除员工。 + /// + [HttpPost("staff/delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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.Ok(null); + } + + /// + /// 获取门店排班配置。 + /// + [HttpGet("staff/schedule")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(new StoreStaffScheduleDto + { + StoreId = parsedStoreId.ToString(), + WeekStartDate = StoreApiHelpers.ResolveWeekStartDate(weekStartDate), + Templates = template, + Schedules = schedules + }); + } + + /// + /// 保存班次模板。 + /// + [HttpPost("staff/template/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(normalizedTemplate); + } + + /// + /// 保存单员工排班。 + /// + [HttpPost("staff/schedule/personal/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.Ok(new StaffScheduleDto + { + StaffId = parsedStaffId.ToString(), + Shifts = shifts + }); + } + + /// + /// 保存门店周排班。 + /// + [HttpPost("staff/schedule/weekly/save")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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>(); + 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(); + foreach (var staff in staffs) + { + List 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.Ok(new StoreStaffScheduleDto + { + StoreId = parsedStoreId.ToString(), + WeekStartDate = StoreApiHelpers.ResolveWeekStartDate(null), + Templates = template, + Schedules = finalSchedules + }); + } + + /// + /// 复制班次模板与排班。 + /// + [HttpPost("staff/copy")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> 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.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.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(); + 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.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 ParsePermissions(string? rawJson, StaffRoleType roleType) + { + if (!string.IsNullOrWhiteSpace(rawJson)) + { + try + { + var parsed = JsonSerializer.Deserialize>(rawJson, StoreApiHelpers.JsonOptions); + if (parsed is not null) + { + return NormalizePermissions(parsed, roleType); + } + } + catch + { + // 忽略权限反序列化异常并回落默认值 + } + } + + return roleType is StaffRoleType.Admin or StaffRoleType.Operator ? ["全部权限"] : []; + } + + private static List NormalizePermissions(IEnumerable? 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 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 NormalizeRowsToShifts( + List 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(); + + 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 NormalizeShifts( + IEnumerable? source, + List 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(); + 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 ToWeeklyEntities( + long storeId, + long staffId, + IEnumerable 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 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(); + 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 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" + } + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreDeliveryZoneCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreDeliveryZoneCommand.cs index af1af08..8d5e041 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreDeliveryZoneCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/CreateStoreDeliveryZoneCommand.cs @@ -38,6 +38,16 @@ public sealed record CreateStoreDeliveryZoneCommand : IRequest public int? EstimatedMinutes { get; init; } + /// + /// 区域颜色。 + /// + public string? Color { get; init; } + + /// + /// 优先级。 + /// + public int Priority { get; init; } = 100; + /// /// 排序。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreDeliveryZoneCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreDeliveryZoneCommand.cs index e21ded5..734ff93 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreDeliveryZoneCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreDeliveryZoneCommand.cs @@ -43,6 +43,16 @@ public sealed record UpdateStoreDeliveryZoneCommand : IRequest public int? EstimatedMinutes { get; init; } + /// + /// 区域颜色。 + /// + public string? Color { get; init; } + + /// + /// 优先级。 + /// + public int Priority { get; init; } = 100; + /// /// 排序。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreFeeCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreFeeCommand.cs index 8a64005..69c7dab 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreFeeCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpdateStoreFeeCommand.cs @@ -48,4 +48,24 @@ public sealed record UpdateStoreFeeCommand : IRequest /// 免配送费门槛。 /// public decimal? FreeDeliveryThreshold { get; init; } + + /// + /// 餐具费是否启用。 + /// + public bool CutleryFeeEnabled { get; init; } + + /// + /// 餐具费金额。 + /// + public decimal CutleryFeeAmount { get; init; } + + /// + /// 加急费是否启用。 + /// + public bool RushFeeEnabled { get; init; } + + /// + /// 加急费金额。 + /// + public decimal RushFeeAmount { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpsertStorePickupSettingCommand.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpsertStorePickupSettingCommand.cs index 4822b8f..c104a66 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpsertStorePickupSettingCommand.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Commands/UpsertStorePickupSettingCommand.cs @@ -1,5 +1,6 @@ using MediatR; using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Domain.Stores.Enums; namespace TakeoutSaaS.Application.App.Stores.Commands; @@ -32,4 +33,14 @@ public sealed record UpsertStorePickupSettingCommand : IRequest public int? MaxQuantityPerOrder { get; init; } + + /// + /// 自提模式。 + /// + public StorePickupMode Mode { get; init; } = StorePickupMode.Big; + + /// + /// 精细规则 JSON。 + /// + public string? FineRuleJson { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDeliveryZoneDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDeliveryZoneDto.cs index 588aa43..6728ba0 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDeliveryZoneDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreDeliveryZoneDto.cs @@ -51,6 +51,16 @@ public sealed record StoreDeliveryZoneDto /// public int? EstimatedMinutes { get; init; } + /// + /// 区域颜色。 + /// + public string? Color { get; init; } + + /// + /// 优先级。 + /// + public int Priority { get; init; } + /// /// 排序。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreFeeDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreFeeDto.cs index 8a38810..1de2c70 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreFeeDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StoreFeeDto.cs @@ -31,6 +31,26 @@ public sealed record StoreFeeDto /// public decimal DeliveryFee { get; init; } + /// + /// 餐具费是否启用。 + /// + public bool CutleryFeeEnabled { get; init; } + + /// + /// 餐具费金额。 + /// + public decimal CutleryFeeAmount { get; init; } + + /// + /// 加急费是否启用。 + /// + public bool RushFeeEnabled { get; init; } + + /// + /// 加急费金额。 + /// + public decimal RushFeeAmount { get; init; } + /// /// 打包费模式。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSettingDto.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSettingDto.cs index 5b071cd..098c69e 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSettingDto.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Dto/StorePickupSettingDto.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Shared.Abstractions.Serialization; namespace TakeoutSaaS.Application.App.Stores.Dto; @@ -39,4 +40,14 @@ public sealed record StorePickupSettingDto /// 单笔最大自提份数。 /// public int? MaxQuantityPerOrder { get; init; } + + /// + /// 自提模式。 + /// + public StorePickupMode Mode { get; init; } + + /// + /// 精细规则 JSON。 + /// + public string? FineRuleJson { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreDeliveryZoneCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreDeliveryZoneCommandHandler.cs index db6a1b7..bbef95f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreDeliveryZoneCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreDeliveryZoneCommandHandler.cs @@ -51,6 +51,8 @@ public sealed class CreateStoreDeliveryZoneCommandHandler( MinimumOrderAmount = request.MinimumOrderAmount, DeliveryFee = request.DeliveryFee, EstimatedMinutes = request.EstimatedMinutes, + Color = request.Color?.Trim(), + Priority = request.Priority, SortOrder = request.SortOrder }; diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreFeeQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreFeeQueryHandler.cs index e2906dc..823466a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreFeeQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreFeeQueryHandler.cs @@ -40,7 +40,11 @@ public sealed class GetStoreFeeQueryHandler( BaseDeliveryFee = 0m, PackagingFeeMode = Domain.Stores.Enums.PackagingFeeMode.Fixed, OrderPackagingFeeMode = Domain.Stores.Enums.OrderPackagingFeeMode.Fixed, - FixedPackagingFee = 0m + FixedPackagingFee = 0m, + CutleryFeeEnabled = false, + CutleryFeeAmount = 0m, + RushFeeEnabled = false, + RushFeeAmount = 0m }; return StoreMapping.ToDto(fallback); } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStorePickupSettingQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStorePickupSettingQueryHandler.cs index f7899e3..fb0cd68 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStorePickupSettingQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStorePickupSettingQueryHandler.cs @@ -31,7 +31,9 @@ public sealed class GetStorePickupSettingQueryHandler( AllowToday = setting.AllowToday, AllowDaysAhead = setting.AllowDaysAhead, DefaultCutoffMinutes = setting.DefaultCutoffMinutes, - MaxQuantityPerOrder = setting.MaxQuantityPerOrder + MaxQuantityPerOrder = setting.MaxQuantityPerOrder, + Mode = setting.Mode, + FineRuleJson = setting.FineRuleJson }; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreDeliveryZoneCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreDeliveryZoneCommandHandler.cs index b8443e5..b11fe10 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreDeliveryZoneCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreDeliveryZoneCommandHandler.cs @@ -52,6 +52,8 @@ public sealed class UpdateStoreDeliveryZoneCommandHandler( existing.MinimumOrderAmount = request.MinimumOrderAmount; existing.DeliveryFee = request.DeliveryFee; existing.EstimatedMinutes = request.EstimatedMinutes; + existing.Color = request.Color?.Trim(); + existing.Priority = request.Priority; existing.SortOrder = request.SortOrder; // 5. (空行后) 持久化 diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreFeeCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreFeeCommandHandler.cs index 7665786..1d3d369 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreFeeCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreFeeCommandHandler.cs @@ -71,6 +71,10 @@ public sealed class UpdateStoreFeeCommandHandler( fee.PackagingFeeTiersJson = null; } fee.FreeDeliveryThreshold = request.FreeDeliveryThreshold; + fee.CutleryFeeEnabled = request.CutleryFeeEnabled; + fee.CutleryFeeAmount = request.CutleryFeeAmount; + fee.RushFeeEnabled = request.RushFeeEnabled; + fee.RushFeeAmount = request.RushFeeAmount; // 4. (空行后) 保存并返回 if (isNew) diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpsertStorePickupSettingCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpsertStorePickupSettingCommandHandler.cs index abb8866..640d18c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpsertStorePickupSettingCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpsertStorePickupSettingCommandHandler.cs @@ -47,6 +47,8 @@ public sealed class UpsertStorePickupSettingCommandHandler( setting.AllowDaysAhead = request.AllowDaysAhead; setting.DefaultCutoffMinutes = request.DefaultCutoffMinutes; setting.MaxQuantityPerOrder = request.MaxQuantityPerOrder; + setting.Mode = request.Mode; + setting.FineRuleJson = request.FineRuleJson; await storeRepository.UpdatePickupSettingAsync(setting, cancellationToken); await storeRepository.SaveChangesAsync(cancellationToken); logger.LogInformation("更新门店 {StoreId} 自提配置", request.StoreId); @@ -57,7 +59,9 @@ public sealed class UpsertStorePickupSettingCommandHandler( AllowToday = setting.AllowToday, AllowDaysAhead = setting.AllowDaysAhead, DefaultCutoffMinutes = setting.DefaultCutoffMinutes, - MaxQuantityPerOrder = setting.MaxQuantityPerOrder + MaxQuantityPerOrder = setting.MaxQuantityPerOrder, + Mode = setting.Mode, + FineRuleJson = setting.FineRuleJson }; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs b/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs index 3dc8357..e7e4939 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/StoreMapping.cs @@ -64,6 +64,10 @@ public static class StoreMapping StoreId = fee.StoreId, MinimumOrderAmount = fee.MinimumOrderAmount, DeliveryFee = fee.BaseDeliveryFee, + CutleryFeeEnabled = fee.CutleryFeeEnabled, + CutleryFeeAmount = fee.CutleryFeeAmount, + RushFeeEnabled = fee.RushFeeEnabled, + RushFeeAmount = fee.RushFeeAmount, PackagingFeeMode = fee.PackagingFeeMode, OrderPackagingFeeMode = fee.OrderPackagingFeeMode, FixedPackagingFee = fee.FixedPackagingFee, @@ -137,6 +141,8 @@ public static class StoreMapping MinimumOrderAmount = zone.MinimumOrderAmount, DeliveryFee = zone.DeliveryFee, EstimatedMinutes = zone.EstimatedMinutes, + Color = zone.Color, + Priority = zone.Priority, SortOrder = zone.SortOrder, CreatedAt = zone.CreatedAt }; diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreDeliveryZoneCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreDeliveryZoneCommandValidator.cs index b8878e3..7a44086 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreDeliveryZoneCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/CreateStoreDeliveryZoneCommandValidator.cs @@ -19,6 +19,8 @@ public sealed class CreateStoreDeliveryZoneCommandValidator : AbstractValidator< 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.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); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreDeliveryZoneCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreDeliveryZoneCommandValidator.cs index 5de6927..0e8f0c1 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreDeliveryZoneCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreDeliveryZoneCommandValidator.cs @@ -20,6 +20,8 @@ public sealed class UpdateStoreDeliveryZoneCommandValidator : AbstractValidator< 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.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); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreFeeCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreFeeCommandValidator.cs index 2b145bd..0b38a71 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreFeeCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpdateStoreFeeCommandValidator.cs @@ -32,6 +32,14 @@ public sealed class UpdateStoreFeeCommandValidator : AbstractValidator !fee.HasValue || fee.Value >= 0) .WithMessage("固定打包费不能为负数"); + RuleFor(x => x.CutleryFeeAmount) + .GreaterThanOrEqualTo(0) + .LessThanOrEqualTo(99.99m); + + RuleFor(x => x.RushFeeAmount) + .GreaterThanOrEqualTo(0) + .LessThanOrEqualTo(99.99m); + RuleFor(x => x) .Custom((command, context) => { diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpsertStorePickupSettingCommandValidator.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpsertStorePickupSettingCommandValidator.cs index 2cc3d04..6acf351 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpsertStorePickupSettingCommandValidator.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Validators/UpsertStorePickupSettingCommandValidator.cs @@ -17,5 +17,6 @@ public sealed class UpsertStorePickupSettingCommandValidator : AbstractValidator RuleFor(x => x.AllowDaysAhead).GreaterThanOrEqualTo(0); RuleFor(x => x.DefaultCutoffMinutes).GreaterThanOrEqualTo(0); RuleFor(x => x.MaxQuantityPerOrder).GreaterThan(0).When(x => x.MaxQuantityPerOrder.HasValue); + RuleFor(x => x.FineRuleJson).MaximumLength(20000); } } diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliverySetting.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliverySetting.cs new file mode 100644 index 0000000..9b97519 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliverySetting.cs @@ -0,0 +1,45 @@ +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店配送设置聚合。 +/// +public sealed class StoreDeliverySetting : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 配送模式。 + /// + public StoreDeliveryMode Mode { get; set; } = StoreDeliveryMode.Polygon; + + /// + /// 配送时效加成(分钟)。 + /// + public int EtaAdjustmentMinutes { get; set; } + + /// + /// 免配送费门槛。 + /// + public decimal? FreeDeliveryThreshold { get; set; } + + /// + /// 每小时配送上限。 + /// + public int HourlyCapacityLimit { get; set; } = 100; + + /// + /// 最大配送距离(公里)。 + /// + public decimal MaxDeliveryDistance { get; set; } = 5m; + + /// + /// 半径梯度配置 JSON。 + /// + public string? RadiusTiersJson { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs index 9e72e51..d5e9c34 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDeliveryZone.cs @@ -37,6 +37,16 @@ public sealed class StoreDeliveryZone : MultiTenantEntityBase /// public int? EstimatedMinutes { get; set; } + /// + /// 区域颜色。 + /// + public string? Color { get; set; } + + /// + /// 优先级(数值越小越优先)。 + /// + public int Priority { get; set; } = 100; + /// /// 排序值。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDineInSetting.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDineInSetting.cs new file mode 100644 index 0000000..61d0e66 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreDineInSetting.cs @@ -0,0 +1,29 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店堂食基础设置。 +/// +public sealed class StoreDineInSetting : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 是否启用堂食。 + /// + public bool Enabled { get; set; } = true; + + /// + /// 默认用餐时长(分钟)。 + /// + public int DefaultDiningMinutes { get; set; } = 90; + + /// + /// 超时提醒阈值(分钟)。 + /// + public int OvertimeReminderMinutes { get; set; } = 10; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreFee.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreFee.cs index 4856e28..0dfe9c8 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreFee.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreFee.cs @@ -47,4 +47,24 @@ public sealed class StoreFee : MultiTenantEntityBase /// 免配送费门槛。 /// public decimal? FreeDeliveryThreshold { get; set; } + + /// + /// 是否启用餐具费。 + /// + public bool CutleryFeeEnabled { get; set; } + + /// + /// 餐具费金额。 + /// + public decimal CutleryFeeAmount { get; set; } + + /// + /// 是否启用加急费。 + /// + public bool RushFeeEnabled { get; set; } + + /// + /// 加急费金额。 + /// + public decimal RushFeeAmount { get; set; } } diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSetting.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSetting.cs index d47984c..d505677 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSetting.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StorePickupSetting.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Stores.Entities; @@ -33,6 +34,16 @@ public sealed class StorePickupSetting : MultiTenantEntityBase /// public int? MaxQuantityPerOrder { get; set; } + /// + /// 自提配置模式。 + /// + public StorePickupMode Mode { get; set; } = StorePickupMode.Big; + + /// + /// 精细规则 JSON。 + /// + public string? FineRuleJson { get; set; } + /// /// 并发控制字段。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreStaffTemplate.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreStaffTemplate.cs new file mode 100644 index 0000000..70c1df2 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreStaffTemplate.cs @@ -0,0 +1,44 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店员工班次模板。 +/// +public sealed class StoreStaffTemplate : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 早班开始时间。 + /// + public TimeSpan MorningStartTime { get; set; } = new(9, 0, 0); + + /// + /// 早班结束时间。 + /// + public TimeSpan MorningEndTime { get; set; } = new(14, 0, 0); + + /// + /// 晚班开始时间。 + /// + public TimeSpan EveningStartTime { get; set; } = new(14, 0, 0); + + /// + /// 晚班结束时间。 + /// + public TimeSpan EveningEndTime { get; set; } = new(21, 0, 0); + + /// + /// 全天班开始时间。 + /// + public TimeSpan FullStartTime { get; set; } = new(9, 0, 0); + + /// + /// 全天班结束时间。 + /// + public TimeSpan FullEndTime { get; set; } = new(21, 0, 0); +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreStaffWeeklySchedule.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreStaffWeeklySchedule.cs new file mode 100644 index 0000000..c93e988 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Entities/StoreStaffWeeklySchedule.cs @@ -0,0 +1,40 @@ +using TakeoutSaaS.Domain.Stores.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Stores.Entities; + +/// +/// 门店员工每周排班。 +/// +public sealed class StoreStaffWeeklySchedule : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 员工 ID。 + /// + public long StaffId { get; set; } + + /// + /// 星期(0=周一,6=周日)。 + /// + public int DayOfWeek { get; set; } + + /// + /// 班次类型。 + /// + public StoreStaffShiftType ShiftType { get; set; } = StoreStaffShiftType.Off; + + /// + /// 开始时间(休息时为空)。 + /// + public TimeSpan? StartTime { get; set; } + + /// + /// 结束时间(休息时为空)。 + /// + public TimeSpan? EndTime { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreDeliveryMode.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreDeliveryMode.cs new file mode 100644 index 0000000..031fc42 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreDeliveryMode.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Stores.Enums; + +/// +/// 门店配送配置模式。 +/// +public enum StoreDeliveryMode +{ + /// + /// 多边形配送范围。 + /// + Polygon = 0, + + /// + /// 半径梯度配送。 + /// + Radius = 1 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StorePickupMode.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StorePickupMode.cs new file mode 100644 index 0000000..bc6958d --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StorePickupMode.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Stores.Enums; + +/// +/// 门店自提模式。 +/// +public enum StorePickupMode +{ + /// + /// 大时段模式。 + /// + Big = 0, + + /// + /// 精细规则模式。 + /// + Fine = 1 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreStaffShiftType.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreStaffShiftType.cs new file mode 100644 index 0000000..78f1c23 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Enums/StoreStaffShiftType.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Stores.Enums; + +/// +/// 员工班次类型。 +/// +public enum StoreStaffShiftType +{ + /// + /// 早班。 + /// + Morning = 0, + + /// + /// 晚班。 + /// + Evening = 1, + + /// + /// 全天班。 + /// + Full = 2, + + /// + /// 休息。 + /// + Off = 3 +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index f1c8126..9f9fca7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -148,6 +148,14 @@ public sealed class TakeoutAppDbContext( /// public DbSet StoreDeliveryZones => Set(); /// + /// 门店配送设置。 + /// + public DbSet StoreDeliverySettings => Set(); + /// + /// 门店堂食设置。 + /// + public DbSet StoreDineInSettings => Set(); + /// /// 门店桌台区域。 /// public DbSet StoreTableAreas => Set(); @@ -168,6 +176,14 @@ public sealed class TakeoutAppDbContext( /// public DbSet StorePickupSlots => Set(); /// + /// 门店班次模板。 + /// + public DbSet StoreStaffTemplates => Set(); + /// + /// 门店每周排班。 + /// + public DbSet StoreStaffWeeklySchedules => Set(); + /// /// 商品分类。 /// public DbSet ProductCategories => Set(); @@ -411,11 +427,15 @@ public sealed class TakeoutAppDbContext( ConfigureStoreBusinessHour(modelBuilder.Entity()); ConfigureStoreHoliday(modelBuilder.Entity()); ConfigureStoreDeliveryZone(modelBuilder.Entity()); + ConfigureStoreDeliverySetting(modelBuilder.Entity()); + ConfigureStoreDineInSetting(modelBuilder.Entity()); ConfigureStoreTableArea(modelBuilder.Entity()); ConfigureStoreTable(modelBuilder.Entity()); ConfigureStoreEmployeeShift(modelBuilder.Entity()); ConfigureStorePickupSetting(modelBuilder.Entity()); ConfigureStorePickupSlot(modelBuilder.Entity()); + ConfigureStoreStaffTemplate(modelBuilder.Entity()); + ConfigureStoreStaffWeeklySchedule(modelBuilder.Entity()); ConfigureProductCategory(modelBuilder.Entity()); ConfigureProduct(modelBuilder.Entity()); ConfigureProductAttributeGroup(modelBuilder.Entity()); @@ -603,6 +623,10 @@ public sealed class TakeoutAppDbContext( builder.Property(x => x.FixedPackagingFee).HasPrecision(10, 2); builder.Property(x => x.PackagingFeeTiersJson).HasColumnType("text"); 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 => x.TenantId); } @@ -981,9 +1005,34 @@ public sealed class TakeoutAppDbContext( builder.Property(x => x.PolygonGeoJson).HasColumnType("text").IsRequired(); builder.Property(x => x.MinimumOrderAmount).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 }); } + private static void ConfigureStoreDeliverySetting(EntityTypeBuilder builder) + { + builder.ToTable("store_delivery_settings"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.Mode).HasConversion(); + 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 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 builder) { builder.ToTable("store_table_areas"); @@ -1022,6 +1071,8 @@ public sealed class TakeoutAppDbContext( builder.HasKey(x => x.Id); builder.Property(x => x.StoreId).IsRequired(); builder.Property(x => x.DefaultCutoffMinutes).HasDefaultValue(30); + builder.Property(x => x.Mode).HasConversion(); + builder.Property(x => x.FineRuleJson).HasColumnType("text"); builder.Property(x => x.RowVersion) .IsConcurrencyToken(); 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 }); } + private static void ConfigureStoreStaffTemplate(EntityTypeBuilder 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 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(); + 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 builder) { builder.ToTable("product_attribute_groups"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260217063434_AddStoreExtendedSettings.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260217063434_AddStoreExtendedSettings.Designer.cs new file mode 100644 index 0000000..9686ba1 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260217063434_AddStoreExtendedSettings.Designer.cs @@ -0,0 +1,7751 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.App.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations +{ + [DbContext(typeof(TakeoutAppDbContext))] + [Migration("20260217063434_AddStoreExtendedSettings")] + partial class AddStoreExtendedSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricAlertRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionJson") + .IsRequired() + .HasColumnType("text") + .HasComment("触发条件 JSON。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("MetricDefinitionId") + .HasColumnType("bigint") + .HasComment("关联指标。"); + + b.Property("NotificationChannels") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("通知渠道。"); + + b.Property("Severity") + .HasColumnType("integer") + .HasComment("告警级别。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MetricDefinitionId", "Severity"); + + b.ToTable("metric_alert_rules", null, t => + { + t.HasComment("指标告警规则。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("指标编码。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DefaultAggregation") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("默认聚合方式。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("说明。"); + + b.Property("DimensionsJson") + .HasColumnType("text") + .HasComment("维度描述 JSON。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("指标名称。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("metric_definitions", null, t => + { + t.HasComment("指标定义,描述可观测的数据点。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DimensionKey") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("维度键(JSON)。"); + + b.Property("MetricDefinitionId") + .HasColumnType("bigint") + .HasComment("指标定义 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("numeric(18,4)") + .HasComment("数值。"); + + b.Property("WindowEnd") + .HasColumnType("timestamp with time zone") + .HasComment("统计时间窗口结束。"); + + b.Property("WindowStart") + .HasColumnType("timestamp with time zone") + .HasComment("统计时间窗口开始。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd") + .IsUnique(); + + b.ToTable("metric_snapshots", null, t => + { + t.HasComment("指标快照,用于大盘展示。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("券码或序列号。"); + + b.Property("CouponTemplateId") + .HasColumnType("bigint") + .HasComment("模板标识。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("到期时间。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("发放时间。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("订单 ID(已使用时记录)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasComment("使用时间。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("归属用户。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("coupons", null, t => + { + t.HasComment("用户领取的券。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowStack") + .HasColumnType("boolean") + .HasComment("是否允许叠加其他优惠。"); + + b.Property("ChannelsJson") + .HasColumnType("text") + .HasComment("发放渠道(JSON)。"); + + b.Property("ClaimedQuantity") + .HasColumnType("integer") + .HasComment("已领取数量。"); + + b.Property("CouponType") + .HasColumnType("integer") + .HasComment("券类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("DiscountCap") + .HasColumnType("numeric") + .HasComment("折扣上限(针对折扣券)。"); + + b.Property("MinimumSpend") + .HasColumnType("numeric") + .HasComment("最低消费门槛。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("模板名称。"); + + b.Property("ProductScopeJson") + .HasColumnType("text") + .HasComment("适用品类或商品范围(JSON)。"); + + b.Property("RelativeValidDays") + .HasColumnType("integer") + .HasComment("有效天数(相对发放时间)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreScopeJson") + .HasColumnType("text") + .HasComment("适用门店 ID 集合(JSON)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TotalQuantity") + .HasColumnType("integer") + .HasComment("总发放数量上限。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasComment("可用开始时间。"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone") + .HasComment("可用结束时间。"); + + b.Property("Value") + .HasColumnType("numeric") + .HasComment("面值或折扣额度。"); + + b.HasKey("Id"); + + b.ToTable("coupon_templates", null, t => + { + t.HasComment("优惠券模板。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AudienceDescription") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("目标人群描述。"); + + b.Property("BannerUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("营销素材(如 banner)。"); + + b.Property("Budget") + .HasColumnType("numeric") + .HasComment("预算金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("活动名称。"); + + b.Property("PromotionType") + .HasColumnType("integer") + .HasComment("活动类型。"); + + b.Property("RulesJson") + .IsRequired() + .HasColumnType("text") + .HasComment("活动规则 JSON。"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("活动状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("promotion_campaigns", null, t => + { + t.HasComment("营销活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatSessionId") + .HasColumnType("bigint") + .HasComment("会话标识。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("消息内容。"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("消息类型(文字/图片/语音等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRead") + .HasColumnType("boolean") + .HasComment("是否已读。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("读取时间。"); + + b.Property("SenderType") + .HasColumnType("integer") + .HasComment("发送方类型。"); + + b.Property("SenderUserId") + .HasColumnType("bigint") + .HasComment("发送方用户 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ChatSessionId", "CreatedAt"); + + b.ToTable("chat_messages", null, t => + { + t.HasComment("会话消息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentUserId") + .HasColumnType("bigint") + .HasComment("当前客服员工 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("bigint") + .HasComment("顾客用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("IsBotActive") + .HasColumnType("boolean") + .HasComment("是否机器人接待中。"); + + b.Property("SessionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会话编号。"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会话状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("所属门店(可空为系统会话)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionCode") + .IsUnique(); + + b.ToTable("chat_sessions", null, t => + { + t.HasComment("客服会话。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedAgentId") + .HasColumnType("bigint") + .HasComment("指派的客服。"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone") + .HasComment("关闭时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("bigint") + .HasComment("客户用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("工单详情。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单(如有)。"); + + b.Property("Priority") + .HasColumnType("integer") + .HasComment("优先级。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("工单主题。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TicketNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("工单编号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TicketNo") + .IsUnique(); + + b.ToTable("support_tickets", null, t => + { + t.HasComment("客服工单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachmentsJson") + .HasColumnType("text") + .HasComment("附件 JSON。"); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .HasComment("评论人 ID。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("评论内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsInternal") + .HasColumnType("boolean") + .HasComment("是否内部备注。"); + + b.Property("SupportTicketId") + .HasColumnType("bigint") + .HasComment("工单标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SupportTicketId"); + + b.ToTable("ticket_comments", null, t => + { + t.HasComment("工单评论/流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryOrderId") + .HasColumnType("bigint") + .HasComment("配送单标识。"); + + b.Property("EventType") + .HasColumnType("integer") + .HasComment("事件类型。"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("事件描述。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("原始数据 JSON。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DeliveryOrderId", "EventType"); + + b.ToTable("delivery_events", null, t => + { + t.HasComment("配送状态事件流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CourierName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("骑手姓名。"); + + b.Property("CourierPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("骑手电话。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveredAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配送费。"); + + b.Property("DispatchedAt") + .HasColumnType("timestamp with time zone") + .HasComment("下发时间。"); + + b.Property("FailureReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("异常原因。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("获取或设置关联订单 ID。"); + + b.Property("PickedUpAt") + .HasColumnType("timestamp with time zone") + .HasComment("取餐时间。"); + + b.Property("Provider") + .HasColumnType("integer") + .HasComment("配送服务商。"); + + b.Property("ProviderOrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("第三方配送单号。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId") + .IsUnique(); + + b.ToTable("delivery_orders", null, t => + { + t.HasComment("配送单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") + .HasComment("推广人标识。"); + + b.Property("BuyerUserId") + .HasColumnType("bigint") + .HasComment("用户 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EstimatedCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("预计佣金。"); + + b.Property("OrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("订单金额。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单。"); + + b.Property("SettledAt") + .HasColumnType("timestamp with time zone") + .HasComment("结算完成时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "OrderId") + .IsUnique(); + + b.ToTable("affiliate_orders", null, t => + { + t.HasComment("分销订单记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelType") + .HasColumnType("integer") + .HasComment("渠道类型。"); + + b.Property("CommissionRate") + .HasColumnType("numeric") + .HasComment("分成比例(0-1)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称或渠道名称。"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("审核备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID(如绑定登录账号)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DisplayName"); + + b.ToTable("affiliate_partners", null, t => + { + t.HasComment("分销/推广合作伙伴。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") + .HasComment("合作伙伴标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("结算金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("打款时间。"); + + b.Property("Period") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("结算周期描述。"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "Period") + .IsUnique(); + + b.ToTable("affiliate_payouts", null, t => + { + t.HasComment("佣金结算记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowMakeupCount") + .HasColumnType("integer") + .HasComment("支持补签次数。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("活动描述。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("结束日期。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("活动名称。"); + + b.Property("RewardsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("连签奖励 JSON。"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasComment("开始日期。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("checkin_campaigns", null, t => + { + t.HasComment("签到活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CheckInCampaignId") + .HasColumnType("bigint") + .HasComment("活动标识。"); + + b.Property("CheckInDate") + .HasColumnType("timestamp with time zone") + .HasComment("签到日期(本地)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsMakeup") + .HasColumnType("boolean") + .HasComment("是否补签。"); + + b.Property("RewardJson") + .IsRequired() + .HasColumnType("text") + .HasComment("获得奖励 JSON。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CheckInCampaignId", "UserId", "CheckInDate") + .IsUnique(); + + b.ToTable("checkin_records", null, t => + { + t.HasComment("用户签到记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .HasComment("评论人。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("评论内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasComment("状态。"); + + b.Property("ParentId") + .HasColumnType("bigint") + .HasComment("父级评论 ID。"); + + b.Property("PostId") + .HasColumnType("bigint") + .HasComment("动态标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "CreatedAt"); + + b.ToTable("community_comments", null, t => + { + t.HasComment("社区评论。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .HasComment("作者用户 ID。"); + + b.Property("CommentCount") + .HasColumnType("integer") + .HasComment("评论数。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("内容。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("LikeCount") + .HasColumnType("integer") + .HasComment("点赞数。"); + + b.Property("MediaJson") + .HasColumnType("text") + .HasComment("媒体资源 JSON。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AuthorUserId", "CreatedAt"); + + b.ToTable("community_posts", null, t => + { + t.HasComment("社区动态。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityReaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PostId") + .HasColumnType("bigint") + .HasComment("动态 ID。"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone") + .HasComment("时间戳。"); + + b.Property("ReactionType") + .HasColumnType("integer") + .HasComment("反应类型。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "UserId") + .IsUnique(); + + b.ToTable("community_reactions", null, t => + { + t.HasComment("社区互动反馈。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentCount") + .HasColumnType("integer") + .HasComment("当前已参与人数。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("GroupOrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("拼单编号。"); + + b.Property("GroupPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("拼团价格。"); + + b.Property("LeaderUserId") + .HasColumnType("bigint") + .HasComment("团长用户 ID。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("关联商品或套餐。"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("拼团状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("SucceededAt") + .HasColumnType("timestamp with time zone") + .HasComment("成团时间。"); + + b.Property("TargetCount") + .HasColumnType("integer") + .HasComment("成团需要的人数。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderNo") + .IsUnique(); + + b.ToTable("group_orders", null, t => + { + t.HasComment("拼单活动。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("GroupOrderId") + .HasColumnType("bigint") + .HasComment("拼单活动标识。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("参与时间。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("对应订单标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("参与状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderId", "UserId") + .IsUnique(); + + b.ToTable("group_participants", null, t => + { + t.HasComment("拼单参与者。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryAdjustment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdjustmentType") + .HasColumnType("integer") + .HasComment("调整类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("InventoryItemId") + .HasColumnType("bigint") + .HasComment("对应的库存记录标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("调整数量,正数增加,负数减少。"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("原因说明。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "InventoryItemId", "OccurredAt"); + + b.ToTable("inventory_adjustments", null, t => + { + t.HasComment("库存调整记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BatchNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("批次编号。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU 标识。"); + + b.Property("ProductionDate") + .HasColumnType("timestamp with time zone") + .HasComment("生产日期。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("入库数量。"); + + b.Property("RemainingQuantity") + .HasColumnType("integer") + .HasComment("剩余数量。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber") + .IsUnique(); + + b.ToTable("inventory_batches", null, t => + { + t.HasComment("SKU 批次信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BatchConsumeStrategy") + .HasColumnType("integer") + .HasComment("批次扣减策略。"); + + b.Property("BatchNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("批次编号,可为空表示混批。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("IsPresale") + .HasColumnType("boolean") + .HasComment("是否预售商品。"); + + b.Property("IsSoldOut") + .HasColumnType("boolean") + .HasComment("是否标记售罄。"); + + b.Property("Location") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("储位或仓位信息。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("单品限购(覆盖商品级 MaxQuantityPerOrder)。"); + + b.Property("PresaleCapacity") + .HasColumnType("integer") + .HasComment("预售名额(上限)。"); + + b.Property("PresaleEndTime") + .HasColumnType("timestamp with time zone") + .HasComment("预售结束时间(UTC)。"); + + b.Property("PresaleLocked") + .HasColumnType("integer") + .HasComment("当前预售已锁定数量。"); + + b.Property("PresaleStartTime") + .HasColumnType("timestamp with time zone") + .HasComment("预售开始时间(UTC)。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU 标识。"); + + b.Property("QuantityOnHand") + .HasColumnType("integer") + .HasComment("可用库存。"); + + b.Property("QuantityReserved") + .HasColumnType("integer") + .HasComment("已锁定库存(订单占用)。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("SafetyStock") + .HasColumnType("integer") + .HasComment("安全库存阈值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "BatchNumber"); + + b.ToTable("inventory_items", null, t => + { + t.HasComment("SKU 在门店的库存信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryLockRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(UTC)。"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("幂等键。"); + + b.Property("IsPresale") + .HasColumnType("boolean") + .HasComment("是否预售锁定。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU ID。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("锁定数量。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("锁定状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "Status"); + + b.ToTable("inventory_lock_records", null, t => + { + t.HasComment("库存锁定记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceAfterChange") + .HasColumnType("integer") + .HasComment("变动后余额。"); + + b.Property("ChangeAmount") + .HasColumnType("integer") + .HasComment("变动数量,可为负值。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(如适用)。"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Reason") + .HasColumnType("integer") + .HasComment("变动原因。"); + + b.Property("SourceId") + .HasColumnType("bigint") + .HasComment("来源 ID(订单、活动等)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MemberId", "OccurredAt"); + + b.ToTable("member_point_ledgers", null, t => + { + t.HasComment("积分变动流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像。"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone") + .HasComment("生日。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("GrowthValue") + .HasColumnType("integer") + .HasComment("成长值/经验值。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("注册时间。"); + + b.Property("MemberTierId") + .HasColumnType("bigint") + .HasComment("当前会员等级 ID。"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号。"); + + b.Property("Nickname") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称。"); + + b.Property("PointsBalance") + .HasColumnType("integer") + .HasComment("会员积分余额。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会员状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Mobile") + .IsUnique(); + + b.ToTable("member_profiles", null, t => + { + t.HasComment("会员档案。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BenefitsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("等级权益(JSON)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("等级名称。"); + + b.Property("RequiredGrowth") + .HasColumnType("integer") + .HasComment("所需成长值。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name") + .IsUnique(); + + b.ToTable("member_tiers", null, t => + { + t.HasComment("会员等级定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("详细地址。"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核通过时间。"); + + b.Property("ApprovedBy") + .HasColumnType("bigint") + .HasComment("审核通过人。"); + + b.Property("BrandAlias") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("品牌简称或别名。"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("品牌名称(对外展示)。"); + + b.Property("BusinessLicenseImageUrl") + .HasColumnType("text") + .HasComment("营业执照扫描件地址。"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("营业执照号。"); + + b.Property("Category") + .HasColumnType("text") + .HasComment("品牌所属品类,如火锅、咖啡等。"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在城市。"); + + b.Property("ClaimExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("领取过期时间。"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone") + .HasComment("领取时间。"); + + b.Property("ClaimedBy") + .HasColumnType("bigint") + .HasComment("当前领取人。"); + + b.Property("ClaimedByName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("当前领取人姓名。"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("联系邮箱。"); + + b.Property("ContactPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在区县。"); + + b.Property("FrozenAt") + .HasColumnType("timestamp with time zone") + .HasComment("冻结时间。"); + + b.Property("FrozenReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("冻结原因。"); + + b.Property("IsFrozen") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否冻结业务。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("入驻时间。"); + + b.Property("LastReviewedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次审核时间。"); + + b.Property("LastReviewedBy") + .HasColumnType("bigint") + .HasComment("最近一次审核人。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度信息。"); + + b.Property("LegalPerson") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("法人或负责人姓名。"); + + b.Property("LogoUrl") + .HasColumnType("text") + .HasComment("品牌 Logo。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度信息。"); + + b.Property("OperatingMode") + .HasColumnType("integer") + .HasComment("经营模式(同一主体/不同主体)。"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在省份。"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("审核备注或驳回原因。"); + + b.Property("ServicePhone") + .HasColumnType("text") + .HasComment("客服电话。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("入驻状态。"); + + b.Property("SupportEmail") + .HasColumnType("text") + .HasComment("客服邮箱。"); + + b.Property("TaxNumber") + .HasColumnType("text") + .HasComment("税号/统一社会信用代码。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClaimedBy"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Status"); + + b.ToTable("merchants", null, t => + { + t.HasComment("商户主体信息,承载入驻和资质审核结果。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DisplayOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("显示顺序,越小越靠前。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否可用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("类目名称。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name") + .IsUnique(); + + b.ToTable("merchant_categories", null, t => + { + t.HasComment("商户可选类目。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContractNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("合同编号。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("合同结束时间。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("合同文件存储地址。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户标识。"); + + b.Property("SignedAt") + .HasColumnType("timestamp with time zone") + .HasComment("签署时间。"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasComment("合同开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("合同状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TerminatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("终止时间。"); + + b.Property("TerminationReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("终止原因。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "ContractNumber") + .IsUnique(); + + b.ToTable("merchant_contracts", null, t => + { + t.HasComment("商户合同记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DocumentNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("证照编号。"); + + b.Property("DocumentType") + .HasColumnType("integer") + .HasComment("证照类型。"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("到期日期。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("证照文件链接。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("签发日期。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户标识。"); + + b.Property("Remarks") + .HasColumnType("text") + .HasComment("审核备注或驳回原因。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("审核状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "DocumentType"); + + b.ToTable("merchant_documents", null, t => + { + t.HasComment("商户提交的资质或证照材料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("邮箱地址。"); + + b.Property("IdentityUserId") + .HasColumnType("bigint") + .HasComment("登录账号 ID(指向统一身份体系)。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户标识。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("员工姓名。"); + + b.Property("PermissionsJson") + .HasColumnType("text") + .HasComment("自定义权限(JSON)。"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号。"); + + b.Property("RoleType") + .HasColumnType("integer") + .HasComment("员工角色类型。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("员工状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("可选的关联门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "Phone"); + + b.ToTable("merchant_staff", null, t => + { + t.HasComment("商户员工账号,支持门店维度分配。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Landmark") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("打车/导航落点描述。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("名称。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("关联门店 ID,可空表示独立 POI。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("map_locations", null, t => + { + t.HasComment("地图 POI 信息,用于门店定位和推荐。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("来源通道(小程序、H5 等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("请求时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TargetApp") + .HasColumnType("integer") + .HasComment("跳转的地图应用。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId", "RequestedAt"); + + b.ToTable("navigation_requests", null, t => + { + t.HasComment("用户发起的导航请求日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributesJson") + .HasColumnType("text") + .HasComment("扩展 JSON(规格、加料选项等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("商品或 SKU 标识。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称快照。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU 标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("自定义备注(口味要求)。"); + + b.Property("ShoppingCartId") + .HasColumnType("bigint") + .HasComment("所属购物车标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("单价快照。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ShoppingCartId"); + + b.ToTable("cart_items", null, t => + { + t.HasComment("购物车条目。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItemAddon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CartItemId") + .HasColumnType("bigint") + .HasComment("所属购物车条目。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("OptionId") + .HasColumnType("bigint") + .HasComment("选项 ID(可对应 ProductAddonOption)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("cart_item_addons", null, t => + { + t.HasComment("购物车条目的加料/附加项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(UTC)。"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会话 Token。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会话状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.Property("ValidationResultJson") + .IsRequired() + .HasColumnType("text") + .HasComment("校验结果明细 JSON。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionToken") + .IsUnique(); + + b.ToTable("checkout_sessions", null, t => + { + t.HasComment("结账会话,记录校验上下文。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.ShoppingCart", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryPreference") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("履约方式(堂食/自提/配送)缓存。"); + + b.Property("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次修改时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("购物车状态,包含正常/锁定。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TableContext") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("桌码或场景标识(扫码点餐)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId") + .IsUnique(); + + b.ToTable("shopping_carts", null, t => + { + t.HasComment("用户购物车,按租户/门店隔离。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("取消原因。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("下单渠道。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("顾客姓名。"); + + b.Property("CustomerPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("顾客手机号。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryType") + .HasColumnType("integer") + .HasComment("履约类型。"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("优惠金额。"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("ItemsAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("商品总额。"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("订单号。"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付时间。"); + + b.Property("PayableAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("应付金额。"); + + b.Property("PaymentStatus") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("QueueNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("排队号(如有)。"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("ReservationId") + .HasColumnType("bigint") + .HasComment("预约 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店。"); + + b.Property("TableNo") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("就餐桌号。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("orders", null, t => + { + t.HasComment("交易订单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributesJson") + .HasColumnType("text") + .HasComment("自定义属性 JSON。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("折扣金额。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("订单 ID。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("SkuName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("SKU/规格描述。"); + + b.Property("SubTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("小计。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("单位。"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("单价。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("order_items", null, t => + { + t.HasComment("订单明细。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderStatusHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注信息。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人标识(可为空表示系统)。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("订单标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("变更后的状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId", "OccurredAt"); + + b.ToTable("order_status_histories", null, t => + { + t.HasComment("订单状态流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("申请金额。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单标识。"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核完成时间。"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("申请原因。"); + + b.Property("RefundNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("退款单号。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("用户提交时间。"); + + b.Property("ReviewNotes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("审核备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("退款状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "RefundNo") + .IsUnique(); + + b.ToTable("refund_requests", null, t => + { + t.HasComment("售后/退款申请。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("支付金额。"); + + b.Property("ChannelTransactionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("第三方渠道单号。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Method") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付完成时间。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("原始回调内容。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("错误/备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TradeNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("系统交易号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("payment_records", null, t => + { + t.HasComment("支付流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRefundRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("退款金额。"); + + b.Property("ChannelRefundId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("渠道退款流水号。"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单标识。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("渠道返回的原始数据 JSON。"); + + b.Property("PaymentRecordId") + .HasColumnType("bigint") + .HasComment("原支付记录标识。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("退款请求时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("退款状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PaymentRecordId"); + + b.ToTable("payment_refund_records", null, t => + { + t.HasComment("支付渠道退款流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint") + .HasComment("所属分类。"); + + b.Property("CoverImage") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("主图。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasColumnType("text") + .HasComment("商品描述。"); + + b.Property("EnableDelivery") + .HasColumnType("boolean") + .HasComment("支持配送。"); + + b.Property("EnableDineIn") + .HasColumnType("boolean") + .HasComment("支持堂食。"); + + b.Property("EnablePickup") + .HasColumnType("boolean") + .HasComment("支持自提。"); + + b.Property("GalleryImages") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("Gallery 图片逗号分隔。"); + + b.Property("IsFeatured") + .HasColumnType("boolean") + .HasComment("是否热门推荐。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("最大每单限购。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("现价。"); + + b.Property("SpuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("商品编码。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("商品状态。"); + + b.Property("StockQuantity") + .HasColumnType("integer") + .HasComment("库存数量(可选)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("所属门店。"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("副标题/卖点。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("售卖单位(份/杯等)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SpuCode") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("products", null, t => + { + t.HasComment("商品(SPU)信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("MaxSelect") + .HasColumnType("integer") + .HasComment("最大选择数量。"); + + b.Property("MinSelect") + .HasColumnType("integer") + .HasComment("最小选择数量。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "Name"); + + b.ToTable("product_addon_groups", null, t => + { + t.HasComment("加料/做法分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddonGroupId") + .HasColumnType("bigint") + .HasComment("所属加料分组。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认选项。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.ToTable("product_addon_options", null, t => + { + t.HasComment("加料选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称,例如“辣度”“份量”。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品标识。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型(单选/多选)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("显示排序。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("关联门店,可为空表示所有门店共享。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name"); + + b.ToTable("product_attribute_groups", null, t => + { + t.HasComment("商品规格/属性分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributeGroupId") + .HasColumnType("bigint") + .HasComment("所属规格组。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认选中。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AttributeGroupId", "Name") + .IsUnique(); + + b.ToTable("product_attribute_options", null, t => + { + t.HasComment("商品规格选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("分类描述。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分类名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("所属门店。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("product_categories", null, t => + { + t.HasComment("商品分类。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Caption") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述或标题。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("MediaType") + .HasColumnType("integer") + .HasComment("媒体类型。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("商品标识。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("媒资链接。"); + + b.HasKey("Id"); + + b.ToTable("product_media_assets", null, t => + { + t.HasComment("商品媒资素材。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductPricingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("条件描述(JSON),如会员等级、渠道等。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone") + .HasComment("生效结束时间。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("特殊价格。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品。"); + + b.Property("RuleType") + .HasColumnType("integer") + .HasComment("策略类型。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasComment("生效开始时间。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("WeekdaysJson") + .HasColumnType("text") + .HasComment("生效星期(JSON 数组)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "RuleType"); + + b.ToTable("product_pricing_rules", null, t => + { + t.HasComment("商品价格策略,支持会员价/时段价等。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSku", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributesJson") + .IsRequired() + .HasColumnType("text") + .HasComment("规格属性 JSON(记录选项 ID)。"); + + b.Property("Barcode") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("条形码。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("售价。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品标识。"); + + b.Property("SkuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("SKU 编码。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StockQuantity") + .HasColumnType("integer") + .HasComment("可售库存。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Weight") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)") + .HasComment("重量(千克)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SkuCode") + .IsUnique(); + + b.ToTable("product_skus", null, t => + { + t.HasComment("商品 SKU,记录具体规格组合价格。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CalledAt") + .HasColumnType("timestamp with time zone") + .HasComment("叫号时间。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EstimatedWaitMinutes") + .HasColumnType("integer") + .HasComment("预计等待分钟。"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasComment("过号时间。"); + + b.Property("PartySize") + .HasColumnType("integer") + .HasComment("就餐人数。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("获取或设置所属门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TicketNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("排队编号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId"); + + b.HasIndex("TenantId", "StoreId", "TicketNumber") + .IsUnique(); + + b.ToTable("queue_tickets", null, t => + { + t.HasComment("排队叫号。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CheckInCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("核销码/到店码。"); + + b.Property("CheckedInAt") + .HasColumnType("timestamp with time zone") + .HasComment("实际签到时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("客户姓名。"); + + b.Property("CustomerPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PeopleCount") + .HasColumnType("integer") + .HasComment("用餐人数。"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("ReservationNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("预约号。"); + + b.Property("ReservationTime") + .HasColumnType("timestamp with time zone") + .HasComment("预约时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店。"); + + b.Property("TablePreference") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("桌型/标签。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ReservationNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("reservations", null, t => + { + t.HasComment("预约/预订记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核通过时间。"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("详细地址。"); + + b.Property("Announcement") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("门店公告。"); + + b.Property("AuditStatus") + .HasColumnType("integer") + .HasComment("审核状态。"); + + b.Property("BusinessHours") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("门店营业时段描述(备用字符串)。"); + + b.Property("BusinessLicenseImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("门店营业执照图片地址(主体不一致模式使用)。"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("门店营业执照号(主体不一致模式使用)。"); + + b.Property("BusinessStatus") + .HasColumnType("integer") + .HasComment("经营状态。"); + + b.Property("CategoryId") + .HasColumnType("bigint") + .HasComment("行业类目 ID。"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在城市。"); + + b.Property("ClosureReason") + .HasColumnType("integer") + .HasComment("歇业原因。"); + + b.Property("ClosureReasonText") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("歇业原因补充说明。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("门店编码,便于扫码及外部对接。"); + + b.Property("Country") + .HasColumnType("text") + .HasComment("所在国家或地区。"); + + b.Property("CoverImageUrl") + .HasColumnType("text") + .HasComment("门店海报。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryRadiusKm") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)") + .HasComment("默认配送半径(公里)。"); + + b.Property("Description") + .HasColumnType("text") + .HasComment("门店描述或公告。"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区县信息。"); + + b.Property("ForceCloseReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("强制关闭原因。"); + + b.Property("ForceClosedAt") + .HasColumnType("timestamp with time zone") + .HasComment("强制关闭时间。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度。"); + + b.Property("LegalRepresentative") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("门店法人(主体不一致模式使用)。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("高德/腾讯地图经度。"); + + b.Property("ManagerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("门店负责人姓名。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户标识。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("门店名称。"); + + b.Property("OwnershipType") + .HasColumnType("integer") + .HasComment("主体类型。"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在省份。"); + + b.Property("RegisteredAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("门店注册地址(主体不一致模式使用)。"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("审核驳回原因。"); + + b.Property("SignboardImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("门头招牌图 URL。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("门店当前运营状态。"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone") + .HasComment("提交审核时间。"); + + b.Property("SupportsDelivery") + .HasColumnType("boolean") + .HasComment("是否支持配送。"); + + b.Property("SupportsDineIn") + .HasColumnType("boolean") + .HasComment("是否支持堂食。"); + + b.Property("SupportsPickup") + .HasColumnType("boolean") + .HasComment("是否支持自提。"); + + b.Property("SupportsQueueing") + .HasColumnType("boolean") + .HasComment("支持排队叫号。"); + + b.Property("SupportsReservation") + .HasColumnType("boolean") + .HasComment("支持预约。"); + + b.Property("Tags") + .HasColumnType("text") + .HasComment("门店标签(逗号分隔)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("Longitude", "Latitude") + .HasFilter("\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL"); + + b.HasIndex("MerchantId", "BusinessLicenseNumber") + .IsUnique() + .HasFilter("\"BusinessLicenseNumber\" IS NOT NULL AND \"Status\" <> 3"); + + b.HasIndex("TenantId", "AuditStatus"); + + b.HasIndex("TenantId", "BusinessStatus"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "MerchantId"); + + b.HasIndex("TenantId", "OwnershipType"); + + b.ToTable("stores", null, t => + { + t.HasComment("门店信息,承载营业配置与能力。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreAuditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .HasComment("操作类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("NewStatus") + .HasColumnType("integer") + .HasComment("操作后状态。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人 ID。"); + + b.Property("OperatorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("操作人名称。"); + + b.Property("PreviousStatus") + .HasColumnType("integer") + .HasComment("操作前状态。"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("驳回理由文本。"); + + b.Property("RejectionReasonId") + .HasColumnType("bigint") + .HasComment("驳回理由 ID。"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasComment("备注。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("store_audit_records", null, t => + { + t.HasComment("门店审核记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapacityLimit") + .HasColumnType("integer") + .HasComment("最大接待容量或单量限制。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DayOfWeek") + .HasColumnType("integer") + .HasComment("星期几,0 表示周日。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(本地时间)。"); + + b.Property("HourType") + .HasColumnType("integer") + .HasComment("时段类型(正常营业、休息、预约等)。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间(本地时间)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "DayOfWeek"); + + b.ToTable("store_business_hours", null, t => + { + t.HasComment("门店营业时段配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliverySetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EtaAdjustmentMinutes") + .HasColumnType("integer") + .HasComment("配送时效加成(分钟)。"); + + b.Property("FreeDeliveryThreshold") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("免配送费门槛。"); + + b.Property("HourlyCapacityLimit") + .HasColumnType("integer") + .HasComment("每小时配送上限。"); + + b.Property("MaxDeliveryDistance") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("最大配送距离(公里)。"); + + b.Property("Mode") + .HasColumnType("integer") + .HasComment("配送模式。"); + + b.Property("RadiusTiersJson") + .HasColumnType("text") + .HasComment("半径梯度配置 JSON。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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 => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Color") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("区域颜色。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配送费。"); + + b.Property("EstimatedMinutes") + .HasColumnType("integer") + .HasComment("预计送达分钟。"); + + b.Property("MinimumOrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("起送价。"); + + b.Property("PolygonGeoJson") + .IsRequired() + .HasColumnType("text") + .HasComment("GeoJSON 表示的多边形范围。"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("优先级(数值越小越优先)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("ZoneName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ZoneName"); + + b.ToTable("store_delivery_zones", null, t => + { + t.HasComment("门店配送范围配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDineInSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DefaultDiningMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(90) + .HasComment("默认用餐时长(分钟)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用堂食。"); + + b.Property("OvertimeReminderMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10) + .HasComment("超时提醒阈值(分钟)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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 => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("RoleType") + .HasColumnType("integer") + .HasComment("排班角色。"); + + b.Property("ShiftDate") + .HasColumnType("timestamp with time zone") + .HasComment("班次日期。"); + + b.Property("StaffId") + .HasColumnType("bigint") + .HasComment("员工标识。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ShiftDate", "StaffId") + .IsUnique(); + + b.ToTable("store_employee_shifts", null, t => + { + t.HasComment("门店员工排班记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreFee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BaseDeliveryFee") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("基础配送费(元)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CutleryFeeAmount") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("餐具费金额。"); + + b.Property("CutleryFeeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否启用餐具费。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("FixedPackagingFee") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("固定打包费(总计模式有效)。"); + + b.Property("FreeDeliveryThreshold") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("免配送费门槛。"); + + b.Property("MinimumOrderAmount") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("起送费(元)。"); + + b.Property("OrderPackagingFeeMode") + .HasColumnType("integer") + .HasComment("订单打包费规则(按订单收费时生效)。"); + + b.Property("PackagingFeeMode") + .HasColumnType("integer") + .HasComment("打包费模式。"); + + b.Property("PackagingFeeTiersJson") + .HasColumnType("text") + .HasComment("阶梯打包费配置(JSON)。"); + + b.Property("RushFeeAmount") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("加急费金额。"); + + b.Property("RushFeeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否启用加急费。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "StoreId") + .IsUnique(); + + b.ToTable("store_fees", null, t => + { + t.HasComment("门店费用配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasComment("开始日期(原 Date 字段)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("结束日期(可选,用于日期范围,如春节 1.28~2.4)。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(IsAllDay=false 时使用)。"); + + b.Property("IsAllDay") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否全天生效。true=全天;false=仅 StartTime~EndTime 时段。"); + + b.Property("IsClosed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否闭店(兼容旧数据,新逻辑请用 OverrideType)。"); + + b.Property("OverrideType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("覆盖类型(闭店/临时营业/调整时间)。"); + + b.Property("Reason") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasComment("说明内容。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间(IsAllDay=false 时使用)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Date"); + + b.ToTable("store_holidays", null, t => + { + t.HasComment("门店临时时段配置(节假日/歇业/调整营业时间)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StorePickupSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowDaysAhead") + .HasColumnType("integer") + .HasComment("可预约天数(含当天)。"); + + b.Property("AllowToday") + .HasColumnType("boolean") + .HasComment("是否允许当天自提。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DefaultCutoffMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasComment("默认截单分钟(开始前多少分钟截止)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("FineRuleJson") + .HasColumnType("text") + .HasComment("精细规则 JSON。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("单笔自提最大份数。"); + + b.Property("Mode") + .HasColumnType("integer") + .HasComment("自提配置模式。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId") + .IsUnique(); + + b.ToTable("store_pickup_settings", null, t => + { + t.HasComment("门店自提配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StorePickupSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasComment("容量(份数)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CutoffMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasComment("截单分钟(开始前多少分钟截止)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("当天结束时间(UTC)。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("档期名称。"); + + b.Property("ReservedCount") + .HasColumnType("integer") + .HasComment("已占用数量。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("当天开始时间(UTC)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Weekdays") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("适用星期(逗号分隔 1-7)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name"); + + b.ToTable("store_pickup_slots", null, t => + { + t.HasComment("门店自提档期。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreQualification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DocumentNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("证照编号。"); + + b.Property("ExpiresAt") + .HasColumnType("date") + .HasComment("到期日期。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("证照文件 URL。"); + + b.Property("IssuedAt") + .HasColumnType("date") + .HasComment("签发日期。"); + + b.Property("QualificationType") + .HasColumnType("integer") + .HasComment("资质类型。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt") + .HasFilter("\"ExpiresAt\" IS NOT NULL"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("store_qualifications", null, t => + { + t.HasComment("门店资质证照。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreStaffTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EveningEndTime") + .HasColumnType("interval") + .HasComment("晚班结束时间。"); + + b.Property("EveningStartTime") + .HasColumnType("interval") + .HasComment("晚班开始时间。"); + + b.Property("FullEndTime") + .HasColumnType("interval") + .HasComment("全天班结束时间。"); + + b.Property("FullStartTime") + .HasColumnType("interval") + .HasComment("全天班开始时间。"); + + b.Property("MorningEndTime") + .HasColumnType("interval") + .HasComment("早班结束时间。"); + + b.Property("MorningStartTime") + .HasColumnType("interval") + .HasComment("早班开始时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DayOfWeek") + .HasColumnType("integer") + .HasComment("星期(0=周一,6=周日)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(休息时为空)。"); + + b.Property("ShiftType") + .HasColumnType("integer") + .HasComment("班次类型。"); + + b.Property("StaffId") + .HasColumnType("bigint") + .HasComment("员工 ID。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间(休息时为空)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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 => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AreaId") + .HasColumnType("bigint") + .HasComment("所在区域 ID。"); + + b.Property("Capacity") + .HasColumnType("integer") + .HasComment("可容纳人数。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("QrCodeUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("桌码二维码地址。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前桌台状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TableCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("桌码。"); + + b.Property("Tags") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("桌台标签(堂食、快餐等)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "TableCode") + .IsUnique(); + + b.ToTable("store_tables", null, t => + { + t.HasComment("桌台信息与二维码绑定。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("区域描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.ToTable("store_table_areas", null, t => + { + t.HasComment("门店桌台区域配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.QuotaPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否上架。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("配额包名称。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("价格。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型。"); + + b.Property("QuotaValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配额数值。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("排序。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("QuotaType", "IsActive", "SortOrder"); + + b.ToTable("quota_packages", null, t => + { + t.HasComment("配额包定义(系统提供的可购买配额包)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("text") + .HasComment("详细地址信息。"); + + b.Property("City") + .HasColumnType("text") + .HasComment("所在城市。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("租户短编码,作为跨系统引用的唯一标识。"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("主联系人邮箱。"); + + b.Property("ContactName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("主联系人姓名。"); + + b.Property("ContactPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("主联系人电话。"); + + b.Property("Country") + .HasColumnType("text") + .HasComment("所在国家/地区。"); + + b.Property("CoverImageUrl") + .HasColumnType("text") + .HasComment("品牌海报或封面图。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("服务生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("服务到期时间(UTC)。"); + + b.Property("Industry") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所属行业,如餐饮、零售等。"); + + b.Property("LegalEntityName") + .HasColumnType("text") + .HasComment("法人或公司主体名称。"); + + b.Property("LogoUrl") + .HasColumnType("text") + .HasComment("LOGO 图片地址。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("租户全称或品牌名称。"); + + b.Property("OperatingMode") + .HasColumnType("integer") + .HasComment("经营模式(同一主体/不同主体)。"); + + b.Property("PrimaryOwnerUserId") + .HasColumnType("bigint") + .HasComment("系统内对应的租户所有者账号 ID。"); + + b.Property("Province") + .HasColumnType("text") + .HasComment("所在省份或州。"); + + b.Property("Remarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息,用于运营记录特殊说明。"); + + b.Property("ShortName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("对外展示的简称。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("租户当前状态,涵盖审核、启用、停用等场景。"); + + b.Property("SuspendedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次暂停服务时间。"); + + b.Property("SuspensionReason") + .HasColumnType("text") + .HasComment("暂停或终止的原因说明。"); + + b.Property("Tags") + .HasColumnType("text") + .HasComment("业务标签集合(逗号分隔)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Website") + .HasColumnType("text") + .HasComment("官网或主要宣传链接。"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ContactPhone") + .IsUnique(); + + b.ToTable("tenants", null, t => + { + t.HasComment("租户信息,描述租户的生命周期与基础资料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AnnouncementType") + .HasColumnType("integer") + .HasComment("公告类型。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("公告正文(可为 Markdown/HTML,前端自行渲染)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("失效时间(UTC),为空表示长期有效。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否启用(已弃用,迁移期保留)。"); + + b.Property("Priority") + .HasColumnType("integer") + .HasComment("展示优先级,数值越大越靠前。"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasComment("实际发布时间(UTC)。"); + + b.Property("PublisherScope") + .HasColumnType("integer") + .HasComment("发布者范围。"); + + b.Property("PublisherUserId") + .HasColumnType("bigint") + .HasComment("发布者用户 ID(系统或租户后台账号)。"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasComment("撤销时间(UTC)。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("ScheduledPublishAt") + .HasColumnType("timestamp with time zone") + .HasComment("预定发布时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("公告状态。"); + + b.Property("TargetParameters") + .HasColumnType("text") + .HasComment("目标受众参数(JSON)。"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("目标受众类型。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("公告标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("Status", "EffectiveFrom") + .HasFilter("\"TenantId\" = 0"); + + b.HasIndex("TenantId", "AnnouncementType", "IsActive"); + + b.HasIndex("TenantId", "EffectiveFrom", "EffectiveTo"); + + b.HasIndex("TenantId", "Status", "EffectiveFrom"); + + b.ToTable("tenant_announcements", null, t => + { + t.HasComment("租户公告。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncementRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AnnouncementId") + .HasColumnType("bigint") + .HasComment("公告 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("已读时间。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("已读用户 ID(后台账号),为空表示租户级已读。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AnnouncementId", "UserId") + .IsUnique(); + + b.ToTable("tenant_announcement_reads", null, t => + { + t.HasComment("租户公告已读记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountDue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("应付金额(原始金额)。"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("BillingType") + .HasColumnType("integer") + .HasComment("账单类型(订阅账单/配额包账单/手动账单/续费账单)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasDefaultValue("CNY") + .HasComment("货币类型(默认 CNY)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("折扣金额。"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone") + .HasComment("到期日。"); + + b.Property("LineItemsJson") + .HasColumnType("text") + .HasComment("账单明细 JSON,记录各项费用。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息(如:人工备注、取消原因等)。"); + + b.Property("OverdueNotifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("逾期通知时间。"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone") + .HasComment("账单周期结束时间。"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone") + .HasComment("账单周期开始时间。"); + + b.Property("ReminderSentAt") + .HasColumnType("timestamp with time zone") + .HasComment("提醒发送时间(续费提醒、逾期提醒等)。"); + + b.Property("StatementNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("账单编号,供对账查询。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前付款状态。"); + + b.Property("SubscriptionId") + .HasColumnType("bigint") + .HasComment("关联的订阅 ID(仅当 BillingType 为 Subscription 或 Renewal 时有值)。"); + + b.Property("TaxAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("税费金额。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("idx_billing_created_at"); + + b.HasIndex("Status", "DueDate") + .HasDatabaseName("idx_billing_status_duedate") + .HasFilter("\"Status\" IN (0, 2)"); + + b.HasIndex("TenantId", "StatementNo") + .IsUnique(); + + b.HasIndex("TenantId", "Status", "DueDate") + .HasDatabaseName("idx_billing_tenant_status_duedate"); + + b.ToTable("tenant_billing_statements", null, t => + { + t.HasComment("租户账单,用于呈现周期性收费。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("发布通道(站内、邮件、短信等)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("通知正文。"); + + b.Property("MetadataJson") + .HasColumnType("text") + .HasComment("附加元数据 JSON。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("租户是否已阅读。"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasComment("推送时间。"); + + b.Property("Severity") + .HasColumnType("integer") + .HasComment("通知重要级别。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("通知标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Channel", "SentAt"); + + b.ToTable("tenant_notifications", null, t => + { + t.HasComment("面向租户的站内通知或消息推送。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPackage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("套餐描述,包含适用场景、权益等。"); + + b.Property("FeaturePoliciesJson") + .HasColumnType("text") + .HasComment("权益明细 JSON,记录自定义特性开关。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否仍启用(系统控制)。"); + + b.Property("IsAllowNewTenantPurchase") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否允许新租户购买/选择(仅影响新购)。"); + + b.Property("IsPublicVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否对外可见(展示页/套餐列表可见性)。"); + + b.Property("IsRecommended") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否推荐展示(运营推荐标识)。"); + + b.Property("MaxAccountCount") + .HasColumnType("integer") + .HasComment("允许创建的最大账号数。"); + + b.Property("MaxDeliveryOrders") + .HasColumnType("integer") + .HasComment("每月可调用的配送单数量上限。"); + + b.Property("MaxSmsCredits") + .HasColumnType("integer") + .HasComment("每月短信额度上限。"); + + b.Property("MaxStorageGb") + .HasColumnType("integer") + .HasComment("存储容量上限(GB)。"); + + b.Property("MaxStoreCount") + .HasColumnType("integer") + .HasComment("允许的最大门店数。"); + + b.Property("MonthlyPrice") + .HasColumnType("numeric") + .HasComment("月付价格,单位:人民币元。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("套餐名称,展示给租户的简称。"); + + b.Property("PackageType") + .HasColumnType("integer") + .HasComment("套餐分类(试用、标准、旗舰等)。"); + + b.Property("PublishStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("发布状态:0=草稿,1=已发布。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("展示排序,数值越小越靠前。"); + + b.PrimitiveCollection("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasComment("套餐标签(用于展示与对比页)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("YearlyPrice") + .HasColumnType("numeric") + .HasComment("年付价格,单位:人民币元。"); + + b.HasKey("Id"); + + b.HasIndex("IsActive", "SortOrder"); + + b.HasIndex("PublishStatus", "IsActive", "IsPublicVisible", "IsAllowNewTenantPurchase", "SortOrder"); + + b.ToTable("tenant_packages", null, t => + { + t.HasComment("系统提供的租户套餐定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("支付金额。"); + + b.Property("BillingStatementId") + .HasColumnType("bigint") + .HasComment("关联的账单 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Method") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付时间。"); + + b.Property("ProofUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("支付凭证 URL。"); + + b.Property("RefundReason") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("退款原因。"); + + b.Property("RefundedAt") + .HasColumnType("timestamp with time zone") + .HasComment("退款时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TransactionNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("交易号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核时间。"); + + b.Property("VerifiedBy") + .HasColumnType("bigint") + .HasComment("审核人 ID(管理员)。"); + + b.HasKey("Id"); + + b.HasIndex("TransactionNo") + .HasDatabaseName("idx_payment_transaction_no") + .HasFilter("\"TransactionNo\" IS NOT NULL"); + + b.HasIndex("BillingStatementId", "PaidAt") + .HasDatabaseName("idx_payment_billing_paidat"); + + b.HasIndex("TenantId", "BillingStatementId"); + + b.ToTable("tenant_payments", null, t => + { + t.HasComment("租户支付记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaPackagePurchase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(可选)。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("购买价格。"); + + b.Property("PurchasedAt") + .HasColumnType("timestamp with time zone") + .HasComment("购买时间。"); + + b.Property("QuotaPackageId") + .HasColumnType("bigint") + .HasComment("配额包 ID。"); + + b.Property("QuotaValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("购买时的配额值。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "QuotaPackageId", "PurchasedAt"); + + b.ToTable("tenant_quota_package_purchases", null, t => + { + t.HasComment("租户配额包购买记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("LastResetAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次重置时间。"); + + b.Property("LimitValue") + .HasColumnType("numeric") + .HasComment("当前配额上限。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型,例如门店数、短信条数等。"); + + b.Property("ResetCycle") + .HasColumnType("text") + .HasComment("配额刷新周期描述(如月、年)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsedValue") + .HasColumnType("numeric") + .HasComment("已消耗的数量。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "QuotaType") + .IsUnique(); + + b.ToTable("tenant_quota_usages", null, t => + { + t.HasComment("租户配额使用情况快照。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsageHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChangeAmount") + .HasColumnType("numeric") + .HasComment("变更量(可选)。"); + + b.Property("ChangeReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("变更原因(可选)。"); + + b.Property("ChangeType") + .HasColumnType("integer") + .HasComment("变更类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("LimitValue") + .HasColumnType("numeric") + .HasComment("限额值(记录时刻的快照)。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型。"); + + b.Property("RecordedAt") + .HasColumnType("timestamp with time zone") + .HasComment("记录时间(UTC)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsedValue") + .HasColumnType("numeric") + .HasComment("已使用值(记录时刻的快照)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "RecordedAt"); + + b.HasIndex("TenantId", "QuotaType", "RecordedAt"); + + b.ToTable("tenant_quota_usage_histories", null, t => + { + t.HasComment("租户配额使用历史记录(用于追踪配额上下限与使用量的时间序列变化)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoRenew") + .HasColumnType("boolean") + .HasComment("是否开启自动续费。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("订阅生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("订阅到期时间(UTC)。"); + + b.Property("NextBillingDate") + .HasColumnType("timestamp with time zone") + .HasComment("下一个计费时间,配合自动续费使用。"); + + b.Property("Notes") + .HasColumnType("text") + .HasComment("运营备注信息。"); + + b.Property("ScheduledPackageId") + .HasColumnType("bigint") + .HasComment("若已排期升降配,对应的新套餐 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("订阅当前状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TenantPackageId") + .HasColumnType("bigint") + .HasComment("当前订阅关联的套餐标识。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TenantPackageId"); + + b.ToTable("tenant_subscriptions", null, t => + { + t.HasComment("租户套餐订阅记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscriptionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric") + .HasComment("相关费用。"); + + b.Property("ChangeType") + .HasColumnType("integer") + .HasComment("变更类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("Currency") + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasComment("币种。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("生效时间。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("到期时间。"); + + b.Property("FromPackageId") + .HasColumnType("bigint") + .HasComment("原套餐 ID。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("租户标识。"); + + b.Property("TenantSubscriptionId") + .HasColumnType("bigint") + .HasComment("对应的订阅 ID。"); + + b.Property("ToPackageId") + .HasColumnType("bigint") + .HasComment("新套餐 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TenantSubscriptionId"); + + b.ToTable("tenant_subscription_histories", null, t => + { + t.HasComment("租户套餐订阅变更记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantVerificationProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdditionalDataJson") + .HasColumnType("text") + .HasComment("附加资料(JSON)。"); + + b.Property("BankAccountName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("开户名。"); + + b.Property("BankAccountNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("银行账号。"); + + b.Property("BankName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("银行名称。"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("营业执照编号。"); + + b.Property("BusinessLicenseUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("营业执照文件地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("LegalPersonIdBackUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("法人身份证反面。"); + + b.Property("LegalPersonIdFrontUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("法人身份证正面。"); + + b.Property("LegalPersonIdNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("法人身份证号。"); + + b.Property("LegalPersonName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("法人姓名。"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("审核备注。"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核时间。"); + + b.Property("ReviewedBy") + .HasColumnType("bigint") + .HasComment("审核人 ID。"); + + b.Property("ReviewedByName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("审核人姓名。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("实名状态。"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone") + .HasComment("提交时间。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("对应的租户标识。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId") + .IsUnique(); + + b.ToTable("tenant_verification_profiles", null, t => + { + t.HasComment("租户实名认证资料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantVisibilityRoleRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("BillingVisibleRoleCodes") + .IsRequired() + .HasColumnType("text[]") + .HasComment("账单可见角色编码集合。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.PrimitiveCollection("QuotaVisibleRoleCodes") + .IsRequired() + .HasColumnType("text[]") + .HasComment("配额可见角色编码集合。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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 => + { + b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260217063434_AddStoreExtendedSettings.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260217063434_AddStoreExtendedSettings.cs new file mode 100644 index 0000000..6944b54 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260217063434_AddStoreExtendedSettings.cs @@ -0,0 +1,295 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations +{ + /// + public partial class AddStoreExtendedSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FineRuleJson", + table: "store_pickup_settings", + type: "text", + nullable: true, + comment: "精细规则 JSON。"); + + migrationBuilder.AddColumn( + name: "Mode", + table: "store_pickup_settings", + type: "integer", + nullable: false, + defaultValue: 0, + comment: "自提配置模式。"); + + migrationBuilder.AddColumn( + name: "CutleryFeeAmount", + table: "store_fees", + type: "numeric(10,2)", + precision: 10, + scale: 2, + nullable: false, + defaultValue: 0m, + comment: "餐具费金额。"); + + migrationBuilder.AddColumn( + name: "CutleryFeeEnabled", + table: "store_fees", + type: "boolean", + nullable: false, + defaultValue: false, + comment: "是否启用餐具费。"); + + migrationBuilder.AddColumn( + name: "RushFeeAmount", + table: "store_fees", + type: "numeric(10,2)", + precision: 10, + scale: 2, + nullable: false, + defaultValue: 0m, + comment: "加急费金额。"); + + migrationBuilder.AddColumn( + name: "RushFeeEnabled", + table: "store_fees", + type: "boolean", + nullable: false, + defaultValue: false, + comment: "是否启用加急费。"); + + migrationBuilder.AddColumn( + name: "Color", + table: "store_delivery_zones", + type: "character varying(32)", + maxLength: 32, + nullable: true, + comment: "区域颜色。"); + + migrationBuilder.AddColumn( + 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(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店 ID。"), + Mode = table.Column(type: "integer", nullable: false, comment: "配送模式。"), + EtaAdjustmentMinutes = table.Column(type: "integer", nullable: false, comment: "配送时效加成(分钟)。"), + FreeDeliveryThreshold = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: true, comment: "免配送费门槛。"), + HourlyCapacityLimit = table.Column(type: "integer", nullable: false, comment: "每小时配送上限。"), + MaxDeliveryDistance = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false, comment: "最大配送距离(公里)。"), + RadiusTiersJson = table.Column(type: "text", nullable: true, comment: "半径梯度配置 JSON。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(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(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店 ID。"), + Enabled = table.Column(type: "boolean", nullable: false, defaultValue: true, comment: "是否启用堂食。"), + DefaultDiningMinutes = table.Column(type: "integer", nullable: false, defaultValue: 90, comment: "默认用餐时长(分钟)。"), + OvertimeReminderMinutes = table.Column(type: "integer", nullable: false, defaultValue: 10, comment: "超时提醒阈值(分钟)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(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(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店 ID。"), + MorningStartTime = table.Column(type: "interval", nullable: false, comment: "早班开始时间。"), + MorningEndTime = table.Column(type: "interval", nullable: false, comment: "早班结束时间。"), + EveningStartTime = table.Column(type: "interval", nullable: false, comment: "晚班开始时间。"), + EveningEndTime = table.Column(type: "interval", nullable: false, comment: "晚班结束时间。"), + FullStartTime = table.Column(type: "interval", nullable: false, comment: "全天班开始时间。"), + FullEndTime = table.Column(type: "interval", nullable: false, comment: "全天班结束时间。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(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(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店 ID。"), + StaffId = table.Column(type: "bigint", nullable: false, comment: "员工 ID。"), + DayOfWeek = table.Column(type: "integer", nullable: false, comment: "星期(0=周一,6=周日)。"), + ShiftType = table.Column(type: "integer", nullable: false, comment: "班次类型。"), + StartTime = table.Column(type: "interval", nullable: true, comment: "开始时间(休息时为空)。"), + EndTime = table.Column(type: "interval", nullable: true, comment: "结束时间(休息时为空)。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(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(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + QuotaVisibleRoleCodes = table.Column(type: "text[]", nullable: false, comment: "配额可见角色编码集合。"), + BillingVisibleRoleCodes = table.Column(type: "text[]", nullable: false, comment: "账单可见角色编码集合。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: false, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(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); + } + + /// + 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"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs index 9798c54..a8eee40 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs @@ -654,7 +654,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations b.Property("StoreId") .HasColumnType("bigint") - .HasComment("所属门店(可空为平台)。"); + .HasComment("所属门店(可空为系统会话)。"); b.Property("TenantId") .HasColumnType("bigint") @@ -1148,7 +1148,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations b.Property("UserId") .HasColumnType("bigint") - .HasComment("用户 ID(如绑定平台账号)。"); + .HasComment("用户 ID(如绑定登录账号)。"); b.HasKey("Id"); @@ -2517,13 +2517,6 @@ namespace TakeoutSaaS.Infrastructure.Migrations .HasColumnType("character varying(512)") .HasComment("审核备注或驳回原因。"); - b.Property("RowVersion") - .IsConcurrencyToken() - .IsRequired() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("bytea") - .HasComment("并发控制版本。"); - b.Property("ServicePhone") .HasColumnType("text") .HasComment("客服电话。"); @@ -2552,6 +2545,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations .HasColumnType("bigint") .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + b.Property("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.HasKey("Id"); b.HasIndex("ClaimedBy"); @@ -3791,7 +3790,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations b.Property("TradeNo") .HasMaxLength(64) .HasColumnType("character varying(64)") - .HasComment("平台交易号。"); + .HasComment("系统交易号。"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") @@ -5228,6 +5227,84 @@ namespace TakeoutSaaS.Infrastructure.Migrations }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliverySetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EtaAdjustmentMinutes") + .HasColumnType("integer") + .HasComment("配送时效加成(分钟)。"); + + b.Property("FreeDeliveryThreshold") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("免配送费门槛。"); + + b.Property("HourlyCapacityLimit") + .HasColumnType("integer") + .HasComment("每小时配送上限。"); + + b.Property("MaxDeliveryDistance") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("最大配送距离(公里)。"); + + b.Property("Mode") + .HasColumnType("integer") + .HasComment("配送模式。"); + + b.Property("RadiusTiersJson") + .HasColumnType("text") + .HasComment("半径梯度配置 JSON。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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 => { b.Property("Id") @@ -5237,6 +5314,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Color") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("区域颜色。"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone") .HasComment("创建时间(UTC)。"); @@ -5272,6 +5354,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations .HasColumnType("text") .HasComment("GeoJSON 表示的多边形范围。"); + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("优先级(数值越小越优先)。"); + b.Property("SortOrder") .HasColumnType("integer") .HasComment("排序值。"); @@ -5308,6 +5396,76 @@ namespace TakeoutSaaS.Infrastructure.Migrations }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDineInSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DefaultDiningMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(90) + .HasComment("默认用餐时长(分钟)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用堂食。"); + + b.Property("OvertimeReminderMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10) + .HasComment("超时提醒阈值(分钟)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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 => { b.Property("Id") @@ -5407,6 +5565,17 @@ namespace TakeoutSaaS.Infrastructure.Migrations .HasColumnType("bigint") .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + b.Property("CutleryFeeAmount") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("餐具费金额。"); + + b.Property("CutleryFeeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否启用餐具费。"); + b.Property("DeletedAt") .HasColumnType("timestamp with time zone") .HasComment("软删除时间(UTC),未删除时为 null。"); @@ -5430,18 +5599,29 @@ namespace TakeoutSaaS.Infrastructure.Migrations .HasColumnType("numeric(10,2)") .HasComment("起送费(元)。"); - b.Property("PackagingFeeMode") - .HasColumnType("integer") - .HasComment("打包费模式。"); - b.Property("OrderPackagingFeeMode") .HasColumnType("integer") .HasComment("订单打包费规则(按订单收费时生效)。"); + b.Property("PackagingFeeMode") + .HasColumnType("integer") + .HasComment("打包费模式。"); + b.Property("PackagingFeeTiersJson") .HasColumnType("text") .HasComment("阶梯打包费配置(JSON)。"); + b.Property("RushFeeAmount") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("加急费金额。"); + + b.Property("RushFeeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否启用加急费。"); + b.Property("StoreId") .HasColumnType("bigint") .HasComment("门店标识。"); @@ -5600,10 +5780,18 @@ namespace TakeoutSaaS.Infrastructure.Migrations .HasColumnType("bigint") .HasComment("删除人用户标识(软删除),未删除时为 null。"); + b.Property("FineRuleJson") + .HasColumnType("text") + .HasComment("精细规则 JSON。"); + b.Property("MaxQuantityPerOrder") .HasColumnType("integer") .HasComment("单笔自提最大份数。"); + b.Property("Mode") + .HasColumnType("integer") + .HasComment("自提配置模式。"); + b.Property("RowVersion") .IsConcurrencyToken() .IsRequired() @@ -5817,6 +6005,156 @@ namespace TakeoutSaaS.Infrastructure.Migrations }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreStaffTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EveningEndTime") + .HasColumnType("interval") + .HasComment("晚班结束时间。"); + + b.Property("EveningStartTime") + .HasColumnType("interval") + .HasComment("晚班开始时间。"); + + b.Property("FullEndTime") + .HasColumnType("interval") + .HasComment("全天班结束时间。"); + + b.Property("FullStartTime") + .HasColumnType("interval") + .HasComment("全天班开始时间。"); + + b.Property("MorningEndTime") + .HasColumnType("interval") + .HasComment("早班结束时间。"); + + b.Property("MorningStartTime") + .HasColumnType("interval") + .HasComment("早班开始时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DayOfWeek") + .HasColumnType("integer") + .HasComment("星期(0=周一,6=周日)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(休息时为空)。"); + + b.Property("ShiftType") + .HasColumnType("integer") + .HasComment("班次类型。"); + + b.Property("StaffId") + .HasColumnType("bigint") + .HasComment("员工 ID。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间(休息时为空)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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 => { b.Property("Id") @@ -6038,7 +6376,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations b.ToTable("quota_packages", null, t => { - t.HasComment("配额包定义(平台提供的可购买配额包)。"); + t.HasComment("配额包定义(系统提供的可购买配额包)。"); }); }); @@ -6191,7 +6529,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations b.ToTable("tenants", null, t => { - t.HasComment("平台租户信息,描述租户的生命周期与基础资料。"); + t.HasComment("租户信息,描述租户的生命周期与基础资料。"); }); }); @@ -6255,7 +6593,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations b.Property("PublisherUserId") .HasColumnType("bigint") - .HasComment("发布者用户 ID(平台或租户后台账号)。"); + .HasComment("发布者用户 ID(系统或租户后台账号)。"); b.Property("RevokedAt") .HasColumnType("timestamp with time zone") @@ -6629,7 +6967,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations b.Property("IsActive") .HasColumnType("boolean") - .HasComment("是否仍启用(平台控制)。"); + .HasComment("是否仍启用(系统控制)。"); b.Property("IsAllowNewTenantPurchase") .ValueGeneratedOnAdd() @@ -6720,7 +7058,7 @@ namespace TakeoutSaaS.Infrastructure.Migrations 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("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasComment("实体唯一标识。"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ClaimedAt") - .HasColumnType("timestamp with time zone") - .HasComment("领取时间(UTC)。"); - - b.Property("ClaimedBy") - .HasColumnType("bigint") - .HasComment("领取人用户 ID。"); - - b.Property("ClaimedByName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)") - .HasComment("领取人名称(展示用快照)。"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasComment("创建时间(UTC)。"); - - b.Property("CreatedBy") - .HasColumnType("bigint") - .HasComment("创建人用户标识,匿名或系统操作时为 null。"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasComment("软删除时间(UTC),未删除时为 null。"); - - b.Property("DeletedBy") - .HasColumnType("bigint") - .HasComment("删除人用户标识(软删除),未删除时为 null。"); - - b.Property("ReleasedAt") - .HasColumnType("timestamp with time zone") - .HasComment("释放时间(UTC),未释放时为 null。"); - - b.Property("TenantId") - .HasColumnType("bigint") - .HasComment("被领取的租户 ID。"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone") - .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); - - b.Property("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 => { b.Property("Id") @@ -7407,6 +7676,64 @@ namespace TakeoutSaaS.Infrastructure.Migrations }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantVisibilityRoleRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("BillingVisibleRoleCodes") + .IsRequired() + .HasColumnType("text[]") + .HasComment("账单可见角色编码集合。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.PrimitiveCollection("QuotaVisibleRoleCodes") + .IsRequired() + .HasColumnType("text[]") + .HasComment("配额可见角色编码集合。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("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 => { b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null)