From 3b3bdcee71eaadbf2d7c737842dd45b52c21d297 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Mon, 2 Mar 2026 21:43:09 +0800 Subject: [PATCH] feat: implement marketing punch card backend module --- TakeoutSaaS.Docs | 2 +- .../Contracts/Marketing/PunchCardContracts.cs | 809 ++ .../MarketingPunchCardController.cs | 402 + .../ChangePunchCardTemplateStatusCommand.cs | 25 + .../DeletePunchCardTemplateCommand.cs | 19 + .../Commands/SavePunchCardTemplateCommand.cs | 130 + .../WritePunchCardUsageRecordCommand.cs | 60 + .../PunchCard/Dto/PunchCardDetailDto.cs | 137 + .../PunchCard/Dto/PunchCardListItemDto.cs | 92 + .../PunchCard/Dto/PunchCardListResultDto.cs | 32 + .../PunchCard/Dto/PunchCardScopeDto.cs | 27 + .../PunchCard/Dto/PunchCardStatsDto.cs | 27 + .../Dto/PunchCardTemplateOptionDto.cs | 17 + .../PunchCard/Dto/PunchCardUsageRecordDto.cs | 77 + .../Dto/PunchCardUsageRecordExportDto.cs | 22 + .../Dto/PunchCardUsageRecordListResultDto.cs | 37 + .../PunchCard/Dto/PunchCardUsageStatsDto.cs | 22 + ...gePunchCardTemplateStatusCommandHandler.cs | 51 + .../DeletePunchCardTemplateCommandHandler.cs | 43 + ...portPunchCardUsageRecordCsvQueryHandler.cs | 128 + .../GetPunchCardTemplateDetailQueryHandler.cs | 47 + .../GetPunchCardTemplateListQueryHandler.cs | 67 + ...GetPunchCardUsageRecordListQueryHandler.cs | 104 + .../SavePunchCardTemplateCommandHandler.cs | 158 + ...WritePunchCardUsageRecordCommandHandler.cs | 160 + .../Coupons/PunchCard/PunchCardDtoFactory.cs | 210 + .../App/Coupons/PunchCard/PunchCardMapping.cs | 546 + .../ExportPunchCardUsageRecordCsvQuery.cs | 30 + .../GetPunchCardTemplateDetailQuery.cs | 20 + .../Queries/GetPunchCardTemplateListQuery.cs | 35 + .../GetPunchCardUsageRecordListQuery.cs | 40 + .../Coupons/Entities/PunchCardInstance.cs | 65 + .../Coupons/Entities/PunchCardTemplate.cs | 130 + .../Coupons/Entities/PunchCardUsageRecord.cs | 60 + .../Coupons/Enums/PunchCardExpireStrategy.cs | 17 + .../Coupons/Enums/PunchCardInstanceStatus.cs | 27 + .../Coupons/Enums/PunchCardScopeType.cs | 27 + .../Coupons/Enums/PunchCardStatus.cs | 17 + .../Coupons/Enums/PunchCardUsageMode.cs | 17 + .../Enums/PunchCardUsageRecordStatus.cs | 27 + .../Coupons/Enums/PunchCardValidityType.cs | 17 + .../Repositories/IPunchCardRepository.cs | 247 + .../AppServiceCollectionExtensions.cs | 1 + .../App/Persistence/TakeoutAppDbContext.cs | 86 + .../App/Repositories/EfPunchCardRepository.cs | 469 + ...60302125930_AddPunchCardModule.Designer.cs | 9547 +++++++++++++++++ .../20260302125930_AddPunchCardModule.cs | 177 + .../TakeoutAppDbContextModelSnapshot.cs | 357 + 48 files changed, 14863 insertions(+), 1 deletion(-) create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/PunchCardContracts.cs create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingPunchCardController.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/ChangePunchCardTemplateStatusCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/DeletePunchCardTemplateCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/SavePunchCardTemplateCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/WritePunchCardUsageRecordCommand.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardDetailDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardListItemDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardListResultDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardScopeDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardStatsDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardTemplateOptionDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordExportDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordListResultDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageStatsDto.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/ChangePunchCardTemplateStatusCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/DeletePunchCardTemplateCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/ExportPunchCardUsageRecordCsvQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardTemplateDetailQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardTemplateListQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardUsageRecordListQueryHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/SavePunchCardTemplateCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/WritePunchCardUsageRecordCommandHandler.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/PunchCardDtoFactory.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/PunchCardMapping.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/ExportPunchCardUsageRecordCsvQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardTemplateDetailQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardTemplateListQuery.cs create mode 100644 src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardUsageRecordListQuery.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardInstance.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardTemplate.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardUsageRecord.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardExpireStrategy.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardInstanceStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardScopeType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardUsageMode.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardUsageRecordStatus.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardValidityType.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Coupons/Repositories/IPunchCardRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPunchCardRepository.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.cs diff --git a/TakeoutSaaS.Docs b/TakeoutSaaS.Docs index 0941503..315fec7 160000 --- a/TakeoutSaaS.Docs +++ b/TakeoutSaaS.Docs @@ -1 +1 @@ -Subproject commit 094150312469f156198500db5bce4581ea026cfa +Subproject commit 315fec77b6a226f74a4d8bbd8bd264179135b9b6 diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/PunchCardContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/PunchCardContracts.cs new file mode 100644 index 0000000..5f73d44 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Marketing/PunchCardContracts.cs @@ -0,0 +1,809 @@ +namespace TakeoutSaaS.TenantApi.Contracts.Marketing; + +/// +/// 次卡列表查询请求。 +/// +public sealed class PunchCardListRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 名称关键字。 + /// + public string? Keyword { get; set; } + + /// + /// 状态筛选(enabled/disabled)。 + /// + public string? Status { get; set; } + + /// + /// 页码。 + /// + public int Page { get; set; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } = 4; +} + +/// +/// 次卡详情请求。 +/// +public sealed class PunchCardDetailRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 次卡 ID。 + /// + public string PunchCardId { get; set; } = string.Empty; +} + +/// +/// 保存次卡请求。 +/// +public sealed class SavePunchCardRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 次卡 ID(编辑时传)。 + /// + public string? Id { get; set; } + + /// + /// 次卡名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 封面图地址。 + /// + public string? CoverImageUrl { get; set; } + + /// + /// 售价。 + /// + public decimal SalePrice { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; set; } + + /// + /// 有效期类型(days/range)。 + /// + public string ValidityType { get; set; } = "days"; + + /// + /// 固定天数。 + /// + public int? ValidityDays { get; set; } + + /// + /// 固定开始日期(yyyy-MM-dd)。 + /// + public string? ValidFrom { get; set; } + + /// + /// 固定结束日期(yyyy-MM-dd)。 + /// + public string? ValidTo { get; set; } + + /// + /// 范围类型(all/category/tag/product)。 + /// + public string ScopeType { get; set; } = "all"; + + /// + /// 指定分类 ID。 + /// + public List ScopeCategoryIds { get; set; } = []; + + /// + /// 指定标签 ID。 + /// + public List ScopeTagIds { get; set; } = []; + + /// + /// 指定商品 ID。 + /// + public List ScopeProductIds { get; set; } = []; + + /// + /// 使用模式(free/cap)。 + /// + public string UsageMode { get; set; } = "free"; + + /// + /// 单次上限金额。 + /// + public decimal? UsageCapAmount { get; set; } + + /// + /// 每日限用次数。 + /// + public int? DailyLimit { get; set; } + + /// + /// 每单限用次数。 + /// + public int? PerOrderLimit { get; set; } + + /// + /// 每人限购。 + /// + public int? PerUserPurchaseLimit { get; set; } + + /// + /// 是否允许转赠。 + /// + public bool AllowTransfer { get; set; } + + /// + /// 过期策略(invalidate/refund)。 + /// + public string ExpireStrategy { get; set; } = "invalidate"; + + /// + /// 次卡描述。 + /// + public string? Description { get; set; } + + /// + /// 通知渠道(in_app/sms)。 + /// + public List NotifyChannels { get; set; } = []; +} + +/// +/// 次卡状态修改请求。 +/// +public sealed class ChangePunchCardStatusRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 次卡 ID。 + /// + public string PunchCardId { get; set; } = string.Empty; + + /// + /// 状态(enabled/disabled)。 + /// + public string Status { get; set; } = "disabled"; +} + +/// +/// 次卡删除请求。 +/// +public sealed class DeletePunchCardRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 次卡 ID。 + /// + public string PunchCardId { get; set; } = string.Empty; +} + +/// +/// 次卡使用记录查询请求。 +/// +public sealed class PunchCardUsageRecordListRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 次卡模板 ID。 + /// + public string? PunchCardId { get; set; } + + /// + /// 状态筛选(normal/used_up/expired)。 + /// + public string? Status { get; set; } + + /// + /// 关键字(会员/商品)。 + /// + public string? Keyword { get; set; } + + /// + /// 页码。 + /// + public int Page { get; set; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } = 10; +} + +/// +/// 次卡使用记录导出请求。 +/// +public sealed class ExportPunchCardUsageRecordRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 次卡模板 ID。 + /// + public string? PunchCardId { get; set; } + + /// + /// 状态筛选(normal/used_up/expired)。 + /// + public string? Status { get; set; } + + /// + /// 关键字(会员/商品)。 + /// + public string? Keyword { get; set; } +} + +/// +/// 写入次卡使用记录请求。 +/// +public sealed class WritePunchCardUsageRecordRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 次卡模板 ID。 + /// + public string PunchCardId { get; set; } = string.Empty; + + /// + /// 次卡实例 ID(可空)。 + /// + public string? PunchCardInstanceId { get; set; } + + /// + /// 次卡实例编号(可空)。 + /// + public string? PunchCardInstanceNo { get; set; } + + /// + /// 会员名称。 + /// + public string? MemberName { get; set; } + + /// + /// 会员手机号(脱敏)。 + /// + public string? MemberPhoneMasked { get; set; } + + /// + /// 兑换商品。 + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// 使用时间。 + /// + public DateTime? UsedAt { get; set; } + + /// + /// 本次使用次数。 + /// + public int UsedTimes { get; set; } = 1; + + /// + /// 超额补差金额。 + /// + public decimal? ExtraPayAmount { get; set; } +} + +/// +/// 次卡模板统计。 +/// +public sealed class PunchCardStatsResponse +{ + /// + /// 在售次卡数量。 + /// + public int OnSaleCount { get; set; } + + /// + /// 累计售出数量。 + /// + public int TotalSoldCount { get; set; } + + /// + /// 累计收入。 + /// + public decimal TotalRevenueAmount { get; set; } + + /// + /// 使用中数量。 + /// + public int ActiveInUseCount { get; set; } +} + +/// +/// 次卡列表项。 +/// +public sealed class PunchCardListItemResponse +{ + /// + /// 次卡 ID。 + /// + public string Id { get; set; } = string.Empty; + + /// + /// 次卡名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 封面图。 + /// + public string? CoverImageUrl { get; set; } + + /// + /// 售价。 + /// + public decimal SalePrice { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; set; } + + /// + /// 有效期展示。 + /// + public string ValiditySummary { get; set; } = string.Empty; + + /// + /// 适用范围类型。 + /// + public string ScopeType { get; set; } = "all"; + + /// + /// 使用模式。 + /// + public string UsageMode { get; set; } = "free"; + + /// + /// 单次上限金额。 + /// + public decimal? UsageCapAmount { get; set; } + + /// + /// 每日限用。 + /// + public int? DailyLimit { get; set; } + + /// + /// 状态(enabled/disabled)。 + /// + public string Status { get; set; } = "enabled"; + + /// + /// 是否弱化展示。 + /// + public bool IsDimmed { get; set; } + + /// + /// 已售数量。 + /// + public int SoldCount { get; set; } + + /// + /// 使用中数量。 + /// + public int ActiveCount { get; set; } + + /// + /// 累计收入。 + /// + public decimal RevenueAmount { get; set; } + + /// + /// 更新时间(yyyy-MM-dd HH:mm:ss)。 + /// + public string UpdatedAt { get; set; } = string.Empty; +} + +/// +/// 次卡列表结果。 +/// +public sealed class PunchCardListResultResponse +{ + /// + /// 列表。 + /// + public List Items { get; set; } = []; + + /// + /// 当前页。 + /// + public int Page { get; set; } + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } + + /// + /// 总条数。 + /// + public int TotalCount { get; set; } + + /// + /// 统计。 + /// + public PunchCardStatsResponse Stats { get; set; } = new(); +} + +/// +/// 次卡范围。 +/// +public sealed class PunchCardScopeResponse +{ + /// + /// 范围类型(all/category/tag/product)。 + /// + public string ScopeType { get; set; } = "all"; + + /// + /// 分类 ID。 + /// + public List CategoryIds { get; set; } = []; + + /// + /// 标签 ID。 + /// + public List TagIds { get; set; } = []; + + /// + /// 商品 ID。 + /// + public List ProductIds { get; set; } = []; +} + +/// +/// 次卡详情。 +/// +public sealed class PunchCardDetailResponse +{ + /// + /// 次卡 ID。 + /// + public string Id { get; set; } = string.Empty; + + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 次卡名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 封面图。 + /// + public string? CoverImageUrl { get; set; } + + /// + /// 售价。 + /// + public decimal SalePrice { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; set; } + + /// + /// 有效期类型(days/range)。 + /// + public string ValidityType { get; set; } = "days"; + + /// + /// 固定天数。 + /// + public int? ValidityDays { get; set; } + + /// + /// 固定开始日期(yyyy-MM-dd)。 + /// + public string? ValidFrom { get; set; } + + /// + /// 固定结束日期(yyyy-MM-dd)。 + /// + public string? ValidTo { get; set; } + + /// + /// 适用范围。 + /// + public PunchCardScopeResponse Scope { get; set; } = new(); + + /// + /// 使用模式(free/cap)。 + /// + public string UsageMode { get; set; } = "free"; + + /// + /// 单次上限金额。 + /// + public decimal? UsageCapAmount { get; set; } + + /// + /// 每日限用。 + /// + public int? DailyLimit { get; set; } + + /// + /// 每单限用。 + /// + public int? PerOrderLimit { get; set; } + + /// + /// 每人限购。 + /// + public int? PerUserPurchaseLimit { get; set; } + + /// + /// 是否允许转赠。 + /// + public bool AllowTransfer { get; set; } + + /// + /// 过期策略(invalidate/refund)。 + /// + public string ExpireStrategy { get; set; } = "invalidate"; + + /// + /// 描述。 + /// + public string? Description { get; set; } + + /// + /// 通知渠道。 + /// + public List NotifyChannels { get; set; } = []; + + /// + /// 状态(enabled/disabled)。 + /// + public string Status { get; set; } = "enabled"; + + /// + /// 已售数量。 + /// + public int SoldCount { get; set; } + + /// + /// 使用中数量。 + /// + public int ActiveCount { get; set; } + + /// + /// 累计收入。 + /// + public decimal RevenueAmount { get; set; } + + /// + /// 更新时间(yyyy-MM-dd HH:mm:ss)。 + /// + public string UpdatedAt { get; set; } = string.Empty; +} + +/// +/// 次卡下拉选项。 +/// +public sealed class PunchCardTemplateOptionResponse +{ + /// + /// 次卡 ID。 + /// + public string TemplateId { get; set; } = string.Empty; + + /// + /// 次卡名称。 + /// + public string Name { get; set; } = string.Empty; +} + +/// +/// 使用记录统计。 +/// +public sealed class PunchCardUsageStatsResponse +{ + /// + /// 今日使用次数。 + /// + public int TodayUsedCount { get; set; } + + /// + /// 本月使用次数。 + /// + public int MonthUsedCount { get; set; } + + /// + /// 7 天内即将过期数量。 + /// + public int ExpiringSoonCount { get; set; } +} + +/// +/// 次卡使用记录项。 +/// +public sealed class PunchCardUsageRecordResponse +{ + /// + /// 使用记录 ID。 + /// + public string Id { get; set; } = string.Empty; + + /// + /// 使用单号。 + /// + public string RecordNo { get; set; } = string.Empty; + + /// + /// 次卡模板 ID。 + /// + public string PunchCardId { get; set; } = string.Empty; + + /// + /// 次卡名称。 + /// + public string PunchCardName { get; set; } = string.Empty; + + /// + /// 次卡实例 ID。 + /// + public string PunchCardInstanceId { get; set; } = string.Empty; + + /// + /// 会员名称。 + /// + public string MemberName { get; set; } = string.Empty; + + /// + /// 会员手机号(脱敏)。 + /// + public string MemberPhoneMasked { get; set; } = string.Empty; + + /// + /// 兑换商品。 + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// 使用时间(yyyy-MM-dd HH:mm:ss)。 + /// + public string UsedAt { get; set; } = string.Empty; + + /// + /// 本次使用次数。 + /// + public int UsedTimes { get; set; } + + /// + /// 剩余次数。 + /// + public int RemainingTimesAfterUse { get; set; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; set; } + + /// + /// 状态(normal/almost_used_up/used_up/expired)。 + /// + public string DisplayStatus { get; set; } = "normal"; + + /// + /// 超额补差金额。 + /// + public decimal? ExtraPayAmount { get; set; } +} + +/// +/// 使用记录分页结果。 +/// +public sealed class PunchCardUsageRecordListResultResponse +{ + /// + /// 列表。 + /// + public List Items { get; set; } = []; + + /// + /// 页码。 + /// + public int Page { get; set; } + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } + + /// + /// 总条数。 + /// + public int TotalCount { get; set; } + + /// + /// 统计。 + /// + public PunchCardUsageStatsResponse Stats { get; set; } = new(); + + /// + /// 次卡筛选项。 + /// + public List TemplateOptions { get; set; } = []; +} + +/// +/// 使用记录导出回执。 +/// +public sealed class PunchCardUsageRecordExportResponse +{ + /// + /// 文件名。 + /// + public string FileName { get; set; } = string.Empty; + + /// + /// Base64 文件内容。 + /// + public string FileContentBase64 { get; set; } = string.Empty; + + /// + /// 导出总条数。 + /// + public int TotalCount { get; set; } +} diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingPunchCardController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingPunchCardController.cs new file mode 100644 index 0000000..6d241c9 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/MarketingPunchCardController.cs @@ -0,0 +1,402 @@ +using System.Globalization; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; +using TakeoutSaaS.Application.App.Stores.Services; +using TakeoutSaaS.Infrastructure.App.Persistence; +using TakeoutSaaS.Module.Authorization.Attributes; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Results; +using TakeoutSaaS.Shared.Web.Api; +using TakeoutSaaS.TenantApi.Contracts.Marketing; + +namespace TakeoutSaaS.TenantApi.Controllers; + +/// +/// 营销中心次卡管理。 +/// +[ApiVersion("1.0")] +[Authorize] +[Route("api/tenant/v{version:apiVersion}/marketing/punch-card")] +public sealed class MarketingPunchCardController( + IMediator mediator, + TakeoutAppDbContext dbContext, + StoreContextService storeContextService) + : BaseApiController +{ + private const string ViewPermission = "tenant:marketing:punch-card:view"; + private const string ManagePermission = "tenant:marketing:punch-card:manage"; + + /// + /// 获取次卡列表。 + /// + [HttpGet("list")] + [PermissionAuthorize(ViewPermission, ManagePermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> List( + [FromQuery] PunchCardListRequest request, + CancellationToken cancellationToken) + { + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + + var result = await mediator.Send(new GetPunchCardTemplateListQuery + { + StoreId = storeId, + Keyword = request.Keyword, + Status = request.Status, + Page = request.Page, + PageSize = request.PageSize + }, cancellationToken); + + return ApiResponse.Ok(new PunchCardListResultResponse + { + Items = result.Items.Select(MapListItem).ToList(), + Page = result.Page, + PageSize = result.PageSize, + TotalCount = result.TotalCount, + Stats = MapTemplateStats(result.Stats) + }); + } + + /// + /// 获取次卡详情。 + /// + [HttpGet("detail")] + [PermissionAuthorize(ViewPermission, ManagePermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Detail( + [FromQuery] PunchCardDetailRequest request, + CancellationToken cancellationToken) + { + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + + var result = await mediator.Send(new GetPunchCardTemplateDetailQuery + { + StoreId = storeId, + TemplateId = StoreApiHelpers.ParseRequiredSnowflake(request.PunchCardId, nameof(request.PunchCardId)) + }, cancellationToken); + + if (result is null) + { + return ApiResponse.Error(ErrorCodes.NotFound, "次卡不存在"); + } + + return ApiResponse.Ok(MapDetail(result)); + } + + /// + /// 保存次卡。 + /// + [HttpPost("save")] + [PermissionAuthorize(ManagePermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Save( + [FromBody] SavePunchCardRequest request, + CancellationToken cancellationToken) + { + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + + var result = await mediator.Send(new SavePunchCardTemplateCommand + { + StoreId = storeId, + TemplateId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id), + Name = request.Name, + CoverImageUrl = request.CoverImageUrl, + SalePrice = request.SalePrice, + OriginalPrice = request.OriginalPrice, + TotalTimes = request.TotalTimes, + ValidityType = request.ValidityType, + ValidityDays = request.ValidityDays, + ValidFrom = ParseDateOrNull(request.ValidFrom, nameof(request.ValidFrom)), + ValidTo = ParseDateOrNull(request.ValidTo, nameof(request.ValidTo)), + ScopeType = request.ScopeType, + ScopeCategoryIds = StoreApiHelpers.ParseSnowflakeList(request.ScopeCategoryIds), + ScopeTagIds = StoreApiHelpers.ParseSnowflakeList(request.ScopeTagIds), + ScopeProductIds = StoreApiHelpers.ParseSnowflakeList(request.ScopeProductIds), + UsageMode = request.UsageMode, + UsageCapAmount = request.UsageCapAmount, + DailyLimit = request.DailyLimit, + PerOrderLimit = request.PerOrderLimit, + PerUserPurchaseLimit = request.PerUserPurchaseLimit, + AllowTransfer = request.AllowTransfer, + ExpireStrategy = request.ExpireStrategy, + Description = request.Description, + NotifyChannels = request.NotifyChannels + }, cancellationToken); + + return ApiResponse.Ok(MapDetail(result)); + } + + /// + /// 修改次卡状态。 + /// + [HttpPost("status")] + [PermissionAuthorize(ManagePermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> ChangeStatus( + [FromBody] ChangePunchCardStatusRequest request, + CancellationToken cancellationToken) + { + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + + var result = await mediator.Send(new ChangePunchCardTemplateStatusCommand + { + StoreId = storeId, + TemplateId = StoreApiHelpers.ParseRequiredSnowflake(request.PunchCardId, nameof(request.PunchCardId)), + Status = request.Status + }, cancellationToken); + + return ApiResponse.Ok(MapDetail(result)); + } + + /// + /// 删除次卡。 + /// + [HttpPost("delete")] + [PermissionAuthorize(ManagePermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> Delete( + [FromBody] DeletePunchCardRequest request, + CancellationToken cancellationToken) + { + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + + await mediator.Send(new DeletePunchCardTemplateCommand + { + StoreId = storeId, + TemplateId = StoreApiHelpers.ParseRequiredSnowflake(request.PunchCardId, nameof(request.PunchCardId)) + }, cancellationToken); + + return ApiResponse.Ok(null); + } + + /// + /// 获取次卡使用记录。 + /// + [HttpGet("usage-record/list")] + [PermissionAuthorize(ViewPermission, ManagePermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> UsageRecordList( + [FromQuery] PunchCardUsageRecordListRequest request, + CancellationToken cancellationToken) + { + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + + var result = await mediator.Send(new GetPunchCardUsageRecordListQuery + { + StoreId = storeId, + TemplateId = StoreApiHelpers.ParseSnowflakeOrNull(request.PunchCardId), + Status = request.Status, + Keyword = request.Keyword, + Page = request.Page, + PageSize = request.PageSize + }, cancellationToken); + + return ApiResponse.Ok(new PunchCardUsageRecordListResultResponse + { + Items = result.Items.Select(MapUsageRecord).ToList(), + Page = result.Page, + PageSize = result.PageSize, + TotalCount = result.TotalCount, + Stats = new PunchCardUsageStatsResponse + { + TodayUsedCount = result.Stats.TodayUsedCount, + MonthUsedCount = result.Stats.MonthUsedCount, + ExpiringSoonCount = result.Stats.ExpiringSoonCount + }, + TemplateOptions = result.TemplateOptions.Select(item => new PunchCardTemplateOptionResponse + { + TemplateId = item.TemplateId.ToString(), + Name = item.Name + }).ToList() + }); + } + + /// + /// 导出次卡使用记录。 + /// + [HttpGet("usage-record/export")] + [PermissionAuthorize(ManagePermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> ExportUsageRecord( + [FromQuery] ExportPunchCardUsageRecordRequest request, + CancellationToken cancellationToken) + { + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + + var result = await mediator.Send(new ExportPunchCardUsageRecordCsvQuery + { + StoreId = storeId, + TemplateId = StoreApiHelpers.ParseSnowflakeOrNull(request.PunchCardId), + Status = request.Status, + Keyword = request.Keyword + }, cancellationToken); + + return ApiResponse.Ok(new PunchCardUsageRecordExportResponse + { + FileName = result.FileName, + FileContentBase64 = result.FileContentBase64, + TotalCount = result.TotalCount + }); + } + + /// + /// 写入次卡使用记录。 + /// + [HttpPost("usage-record/write")] + [PermissionAuthorize(ManagePermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> WriteUsageRecord( + [FromBody] WritePunchCardUsageRecordRequest request, + CancellationToken cancellationToken) + { + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + + var result = await mediator.Send(new WritePunchCardUsageRecordCommand + { + StoreId = storeId, + TemplateId = StoreApiHelpers.ParseRequiredSnowflake(request.PunchCardId, nameof(request.PunchCardId)), + InstanceId = StoreApiHelpers.ParseSnowflakeOrNull(request.PunchCardInstanceId), + InstanceNo = request.PunchCardInstanceNo, + MemberName = request.MemberName, + MemberPhoneMasked = request.MemberPhoneMasked, + ProductName = request.ProductName, + UsedAt = request.UsedAt, + UsedTimes = request.UsedTimes, + ExtraPayAmount = request.ExtraPayAmount + }, cancellationToken); + + return ApiResponse.Ok(MapUsageRecord(result)); + } + + private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken) + { + var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService); + await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken); + } + + private static DateTime? ParseDateOrNull(string? value, string fieldName) + { + return string.IsNullOrWhiteSpace(value) + ? null + : StoreApiHelpers.ParseDateOnly(value, fieldName); + } + + private static PunchCardListItemResponse MapListItem(PunchCardListItemDto source) + { + return new PunchCardListItemResponse + { + Id = source.Id.ToString(), + Name = source.Name, + CoverImageUrl = source.CoverImageUrl, + SalePrice = source.SalePrice, + OriginalPrice = source.OriginalPrice, + TotalTimes = source.TotalTimes, + ValiditySummary = source.ValiditySummary, + ScopeType = source.ScopeType, + UsageMode = source.UsageMode, + UsageCapAmount = source.UsageCapAmount, + DailyLimit = source.DailyLimit, + Status = source.Status, + IsDimmed = source.IsDimmed, + SoldCount = source.SoldCount, + ActiveCount = source.ActiveCount, + RevenueAmount = source.RevenueAmount, + UpdatedAt = ToDateTime(source.UpdatedAt) + }; + } + + private static PunchCardStatsResponse MapTemplateStats(PunchCardStatsDto source) + { + return new PunchCardStatsResponse + { + OnSaleCount = source.OnSaleCount, + TotalSoldCount = source.TotalSoldCount, + TotalRevenueAmount = source.TotalRevenueAmount, + ActiveInUseCount = source.ActiveInUseCount + }; + } + + private static PunchCardDetailResponse MapDetail(PunchCardDetailDto source) + { + return new PunchCardDetailResponse + { + Id = source.Id.ToString(), + StoreId = source.StoreId.ToString(), + Name = source.Name, + CoverImageUrl = source.CoverImageUrl, + SalePrice = source.SalePrice, + OriginalPrice = source.OriginalPrice, + TotalTimes = source.TotalTimes, + ValidityType = source.ValidityType, + ValidityDays = source.ValidityDays, + ValidFrom = ToDateOnly(source.ValidFrom), + ValidTo = ToDateOnly(source.ValidTo), + Scope = new PunchCardScopeResponse + { + ScopeType = source.Scope.ScopeType, + CategoryIds = source.Scope.CategoryIds.Select(item => item.ToString()).ToList(), + TagIds = source.Scope.TagIds.Select(item => item.ToString()).ToList(), + ProductIds = source.Scope.ProductIds.Select(item => item.ToString()).ToList() + }, + UsageMode = source.UsageMode, + UsageCapAmount = source.UsageCapAmount, + DailyLimit = source.DailyLimit, + PerOrderLimit = source.PerOrderLimit, + PerUserPurchaseLimit = source.PerUserPurchaseLimit, + AllowTransfer = source.AllowTransfer, + ExpireStrategy = source.ExpireStrategy, + Description = source.Description, + NotifyChannels = source.NotifyChannels.ToList(), + Status = source.Status, + SoldCount = source.SoldCount, + ActiveCount = source.ActiveCount, + RevenueAmount = source.RevenueAmount, + UpdatedAt = ToDateTime(source.UpdatedAt) + }; + } + + private static PunchCardUsageRecordResponse MapUsageRecord(PunchCardUsageRecordDto source) + { + return new PunchCardUsageRecordResponse + { + Id = source.Id.ToString(), + RecordNo = source.RecordNo, + PunchCardId = source.PunchCardTemplateId.ToString(), + PunchCardName = source.PunchCardName, + PunchCardInstanceId = source.PunchCardInstanceId.ToString(), + MemberName = source.MemberName, + MemberPhoneMasked = source.MemberPhoneMasked, + ProductName = source.ProductName, + UsedAt = ToDateTime(source.UsedAt), + UsedTimes = source.UsedTimes, + RemainingTimesAfterUse = source.RemainingTimesAfterUse, + TotalTimes = source.TotalTimes, + DisplayStatus = source.DisplayStatus, + ExtraPayAmount = source.ExtraPayAmount + }; + } + + private static string? ToDateOnly(DateTime? value) + { + return value.HasValue + ? value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + : null; + } + + private static string ToDateTime(DateTime value) + { + return value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/ChangePunchCardTemplateStatusCommand.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/ChangePunchCardTemplateStatusCommand.cs new file mode 100644 index 0000000..30cb49b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/ChangePunchCardTemplateStatusCommand.cs @@ -0,0 +1,25 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; + +/// +/// 修改次卡模板状态命令。 +/// +public sealed class ChangePunchCardTemplateStatusCommand : IRequest +{ + /// + /// 操作门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 次卡模板 ID。 + /// + public long TemplateId { get; init; } + + /// + /// 状态(enabled/disabled)。 + /// + public string Status { get; init; } = "disabled"; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/DeletePunchCardTemplateCommand.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/DeletePunchCardTemplateCommand.cs new file mode 100644 index 0000000..f146d71 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/DeletePunchCardTemplateCommand.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; + +/// +/// 删除次卡模板命令。 +/// +public sealed class DeletePunchCardTemplateCommand : IRequest +{ + /// + /// 操作门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 次卡模板 ID。 + /// + public long TemplateId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/SavePunchCardTemplateCommand.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/SavePunchCardTemplateCommand.cs new file mode 100644 index 0000000..93ace20 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/SavePunchCardTemplateCommand.cs @@ -0,0 +1,130 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; + +/// +/// 保存次卡模板命令。 +/// +public sealed class SavePunchCardTemplateCommand : IRequest +{ + /// + /// 操作门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 次卡模板 ID(编辑时传)。 + /// + public long? TemplateId { get; init; } + + /// + /// 次卡名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 封面图。 + /// + public string? CoverImageUrl { get; init; } + + /// + /// 售价。 + /// + public decimal SalePrice { get; init; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; init; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; init; } + + /// + /// 有效期类型(days/range)。 + /// + public string ValidityType { get; init; } = "days"; + + /// + /// 固定天数。 + /// + public int? ValidityDays { get; init; } + + /// + /// 固定开始日期。 + /// + public DateTime? ValidFrom { get; init; } + + /// + /// 固定结束日期。 + /// + public DateTime? ValidTo { get; init; } + + /// + /// 范围类型(all/category/tag/product)。 + /// + public string ScopeType { get; init; } = "all"; + + /// + /// 指定分类 ID。 + /// + public IReadOnlyCollection ScopeCategoryIds { get; init; } = []; + + /// + /// 指定标签 ID。 + /// + public IReadOnlyCollection ScopeTagIds { get; init; } = []; + + /// + /// 指定商品 ID。 + /// + public IReadOnlyCollection ScopeProductIds { get; init; } = []; + + /// + /// 使用模式(free/cap)。 + /// + public string UsageMode { get; init; } = "free"; + + /// + /// 单次上限金额。 + /// + public decimal? UsageCapAmount { get; init; } + + /// + /// 每日限用次数。 + /// + public int? DailyLimit { get; init; } + + /// + /// 每单限用次数。 + /// + public int? PerOrderLimit { get; init; } + + /// + /// 每人限购张数。 + /// + public int? PerUserPurchaseLimit { get; init; } + + /// + /// 是否允许转赠。 + /// + public bool AllowTransfer { get; init; } + + /// + /// 过期策略(invalidate/refund)。 + /// + public string ExpireStrategy { get; init; } = "invalidate"; + + /// + /// 次卡说明。 + /// + public string? Description { get; init; } + + /// + /// 通知渠道(in_app/sms)。 + /// + public IReadOnlyCollection NotifyChannels { get; init; } = []; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/WritePunchCardUsageRecordCommand.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/WritePunchCardUsageRecordCommand.cs new file mode 100644 index 0000000..92c2737 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Commands/WritePunchCardUsageRecordCommand.cs @@ -0,0 +1,60 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; + +/// +/// 写入次卡使用记录命令。 +/// +public sealed class WritePunchCardUsageRecordCommand : IRequest +{ + /// + /// 操作门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 次卡模板 ID。 + /// + public long TemplateId { get; init; } + + /// + /// 次卡实例 ID(可空)。 + /// + public long? InstanceId { get; init; } + + /// + /// 次卡实例编号(可空)。 + /// + public string? InstanceNo { get; init; } + + /// + /// 会员名称(当未指定实例时用于创建实例)。 + /// + public string? MemberName { get; init; } + + /// + /// 会员手机号(脱敏,当未指定实例时用于创建实例)。 + /// + public string? MemberPhoneMasked { get; init; } + + /// + /// 兑换商品名称。 + /// + public string ProductName { get; init; } = string.Empty; + + /// + /// 使用时间(可空,空则取当前 UTC)。 + /// + public DateTime? UsedAt { get; init; } + + /// + /// 本次使用次数。 + /// + public int UsedTimes { get; init; } = 1; + + /// + /// 超额补差金额。 + /// + public decimal? ExtraPayAmount { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardDetailDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardDetailDto.cs new file mode 100644 index 0000000..1382869 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardDetailDto.cs @@ -0,0 +1,137 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡详情。 +/// +public sealed class PunchCardDetailDto +{ + /// + /// 次卡 ID。 + /// + public long Id { get; init; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 次卡名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 封面图。 + /// + public string? CoverImageUrl { get; init; } + + /// + /// 售价。 + /// + public decimal SalePrice { get; init; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; init; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; init; } + + /// + /// 有效期类型(days/range)。 + /// + public string ValidityType { get; init; } = "days"; + + /// + /// 固定天数。 + /// + public int? ValidityDays { get; init; } + + /// + /// 固定开始日期(UTC)。 + /// + public DateTime? ValidFrom { get; init; } + + /// + /// 固定结束日期(UTC)。 + /// + public DateTime? ValidTo { get; init; } + + /// + /// 适用范围。 + /// + public PunchCardScopeDto Scope { get; init; } = new(); + + /// + /// 使用模式(free/cap)。 + /// + public string UsageMode { get; init; } = "free"; + + /// + /// 金额上限。 + /// + public decimal? UsageCapAmount { get; init; } + + /// + /// 每日限用。 + /// + public int? DailyLimit { get; init; } + + /// + /// 每单限用。 + /// + public int? PerOrderLimit { get; init; } + + /// + /// 每人限购。 + /// + public int? PerUserPurchaseLimit { get; init; } + + /// + /// 是否允许转赠。 + /// + public bool AllowTransfer { get; init; } + + /// + /// 过期策略(invalidate/refund)。 + /// + public string ExpireStrategy { get; init; } = "invalidate"; + + /// + /// 描述。 + /// + public string? Description { get; init; } + + /// + /// 通知渠道(in_app/sms)。 + /// + public IReadOnlyList NotifyChannels { get; init; } = []; + + /// + /// 状态(enabled/disabled)。 + /// + public string Status { get; init; } = "enabled"; + + /// + /// 已售数量。 + /// + public int SoldCount { get; init; } + + /// + /// 使用中数量。 + /// + public int ActiveCount { get; init; } + + /// + /// 累计收入。 + /// + public decimal RevenueAmount { get; init; } + + /// + /// 更新时间。 + /// + public DateTime UpdatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardListItemDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardListItemDto.cs new file mode 100644 index 0000000..1904b0e --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardListItemDto.cs @@ -0,0 +1,92 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡列表项。 +/// +public sealed class PunchCardListItemDto +{ + /// + /// 次卡 ID。 + /// + public long Id { get; init; } + + /// + /// 次卡名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 封面图。 + /// + public string? CoverImageUrl { get; init; } + + /// + /// 售价。 + /// + public decimal SalePrice { get; init; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; init; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; init; } + + /// + /// 有效期展示文案。 + /// + public string ValiditySummary { get; init; } = string.Empty; + + /// + /// 适用范围类型(all/category/tag/product)。 + /// + public string ScopeType { get; init; } = "all"; + + /// + /// 使用模式(free/cap)。 + /// + public string UsageMode { get; init; } = "free"; + + /// + /// 单次使用上限金额。 + /// + public decimal? UsageCapAmount { get; init; } + + /// + /// 每日限用次数。 + /// + public int? DailyLimit { get; init; } + + /// + /// 状态(enabled/disabled)。 + /// + public string Status { get; init; } = "enabled"; + + /// + /// 是否弱化展示。 + /// + public bool IsDimmed { get; init; } + + /// + /// 已售数量。 + /// + public int SoldCount { get; init; } + + /// + /// 使用中数量。 + /// + public int ActiveCount { get; init; } + + /// + /// 累计收入。 + /// + public decimal RevenueAmount { get; init; } + + /// + /// 更新时间。 + /// + public DateTime UpdatedAt { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardListResultDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardListResultDto.cs new file mode 100644 index 0000000..5bd9965 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardListResultDto.cs @@ -0,0 +1,32 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡模板列表结果。 +/// +public sealed class PunchCardListResultDto +{ + /// + /// 列表项。 + /// + public IReadOnlyList Items { get; init; } = []; + + /// + /// 当前页。 + /// + public int Page { get; init; } + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } + + /// + /// 总条数。 + /// + public int TotalCount { get; init; } + + /// + /// 统计数据。 + /// + public PunchCardStatsDto Stats { get; init; } = new(); +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardScopeDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardScopeDto.cs new file mode 100644 index 0000000..ff677fc --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardScopeDto.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡范围规则。 +/// +public sealed class PunchCardScopeDto +{ + /// + /// 范围类型(all/category/tag/product)。 + /// + public string ScopeType { get; init; } = "all"; + + /// + /// 指定分类 ID。 + /// + public IReadOnlyList CategoryIds { get; init; } = []; + + /// + /// 指定标签 ID。 + /// + public IReadOnlyList TagIds { get; init; } = []; + + /// + /// 指定商品 ID。 + /// + public IReadOnlyList ProductIds { get; init; } = []; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardStatsDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardStatsDto.cs new file mode 100644 index 0000000..aa54848 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardStatsDto.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡模板统计。 +/// +public sealed class PunchCardStatsDto +{ + /// + /// 在售次卡数量。 + /// + public int OnSaleCount { get; init; } + + /// + /// 累计售出数量。 + /// + public int TotalSoldCount { get; init; } + + /// + /// 累计收入。 + /// + public decimal TotalRevenueAmount { get; init; } + + /// + /// 使用中数量。 + /// + public int ActiveInUseCount { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardTemplateOptionDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardTemplateOptionDto.cs new file mode 100644 index 0000000..e7c97e2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardTemplateOptionDto.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡过滤选项。 +/// +public sealed class PunchCardTemplateOptionDto +{ + /// + /// 次卡模板 ID。 + /// + public long TemplateId { get; init; } + + /// + /// 次卡名称。 + /// + public string Name { get; init; } = string.Empty; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordDto.cs new file mode 100644 index 0000000..9864641 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordDto.cs @@ -0,0 +1,77 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡使用记录项。 +/// +public sealed class PunchCardUsageRecordDto +{ + /// + /// 使用记录 ID。 + /// + public long Id { get; init; } + + /// + /// 使用单号。 + /// + public string RecordNo { get; init; } = string.Empty; + + /// + /// 次卡模板 ID。 + /// + public long PunchCardTemplateId { get; init; } + + /// + /// 次卡名称。 + /// + public string PunchCardName { get; init; } = string.Empty; + + /// + /// 次卡实例 ID。 + /// + public long PunchCardInstanceId { get; init; } + + /// + /// 会员名称。 + /// + public string MemberName { get; init; } = string.Empty; + + /// + /// 会员手机号(脱敏)。 + /// + public string MemberPhoneMasked { get; init; } = string.Empty; + + /// + /// 兑换商品名称。 + /// + public string ProductName { get; init; } = string.Empty; + + /// + /// 使用时间。 + /// + public DateTime UsedAt { get; init; } + + /// + /// 本次使用次数。 + /// + public int UsedTimes { get; init; } + + /// + /// 使用后剩余次数。 + /// + public int RemainingTimesAfterUse { get; init; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; init; } + + /// + /// 状态(normal/almost_used_up/used_up/expired)。 + /// + public string DisplayStatus { get; init; } = "normal"; + + /// + /// 超额补差金额。 + /// + public decimal? ExtraPayAmount { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordExportDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordExportDto.cs new file mode 100644 index 0000000..b26fd7f --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordExportDto.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡使用记录导出结果。 +/// +public sealed class PunchCardUsageRecordExportDto +{ + /// + /// 文件名。 + /// + public string FileName { get; init; } = string.Empty; + + /// + /// 文件内容(Base64)。 + /// + public string FileContentBase64 { get; init; } = string.Empty; + + /// + /// 导出总条数。 + /// + public int TotalCount { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordListResultDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordListResultDto.cs new file mode 100644 index 0000000..5be5a25 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageRecordListResultDto.cs @@ -0,0 +1,37 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡使用记录分页结果。 +/// +public sealed class PunchCardUsageRecordListResultDto +{ + /// + /// 列表数据。 + /// + public IReadOnlyList Items { get; init; } = []; + + /// + /// 页码。 + /// + public int Page { get; init; } + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } + + /// + /// 总条数。 + /// + public int TotalCount { get; init; } + + /// + /// 统计数据。 + /// + public PunchCardUsageStatsDto Stats { get; init; } = new(); + + /// + /// 次卡筛选选项。 + /// + public IReadOnlyList TemplateOptions { get; init; } = []; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageStatsDto.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageStatsDto.cs new file mode 100644 index 0000000..5faa687 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Dto/PunchCardUsageStatsDto.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +/// +/// 次卡使用记录统计。 +/// +public sealed class PunchCardUsageStatsDto +{ + /// + /// 今日使用次数。 + /// + public int TodayUsedCount { get; init; } + + /// + /// 本月使用次数。 + /// + public int MonthUsedCount { get; init; } + + /// + /// 7 天内即将过期数量。 + /// + public int ExpiringSoonCount { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/ChangePunchCardTemplateStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/ChangePunchCardTemplateStatusCommandHandler.cs new file mode 100644 index 0000000..3968de3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/ChangePunchCardTemplateStatusCommandHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers; + +/// +/// 次卡状态变更处理器。 +/// +public sealed class ChangePunchCardTemplateStatusCommandHandler( + IPunchCardRepository repository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + ChangePunchCardTemplateStatusCommand request, + CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var normalizedStatus = PunchCardMapping.ParseTemplateStatus(request.Status); + + var entity = await repository.FindTemplateByIdAsync( + tenantId, + request.StoreId, + request.TemplateId, + cancellationToken) + ?? throw new BusinessException(ErrorCodes.NotFound, "次卡不存在"); + + entity.Status = normalizedStatus; + + await repository.UpdateTemplateAsync(entity, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); + + var aggregateMap = await repository.GetTemplateAggregateByTemplateIdsAsync( + tenantId, + request.StoreId, + [entity.Id], + cancellationToken); + + var aggregate = aggregateMap.TryGetValue(entity.Id, out var value) + ? value + : PunchCardDtoFactory.EmptyAggregate(entity.Id); + + return PunchCardDtoFactory.ToDetailDto(entity, aggregate); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/DeletePunchCardTemplateCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/DeletePunchCardTemplateCommandHandler.cs new file mode 100644 index 0000000..181b10d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/DeletePunchCardTemplateCommandHandler.cs @@ -0,0 +1,43 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers; + +/// +/// 删除次卡模板处理器。 +/// +public sealed class DeletePunchCardTemplateCommandHandler( + IPunchCardRepository repository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle(DeletePunchCardTemplateCommand request, CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var entity = await repository.FindTemplateByIdAsync( + tenantId, + request.StoreId, + request.TemplateId, + cancellationToken) + ?? throw new BusinessException(ErrorCodes.NotFound, "次卡不存在"); + + var aggregate = await repository.GetTemplateAggregateByTemplateIdsAsync( + tenantId, + request.StoreId, + [entity.Id], + cancellationToken); + + if (aggregate.TryGetValue(entity.Id, out var snapshot) && snapshot.SoldCount > 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "已售出的次卡不可删除"); + } + + await repository.DeleteTemplateAsync(entity, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/ExportPunchCardUsageRecordCsvQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/ExportPunchCardUsageRecordCsvQueryHandler.cs new file mode 100644 index 0000000..de0899d --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/ExportPunchCardUsageRecordCsvQueryHandler.cs @@ -0,0 +1,128 @@ +using System.Text; +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers; + +/// +/// 导出次卡使用记录处理器。 +/// +public sealed class ExportPunchCardUsageRecordCsvQueryHandler( + IPunchCardRepository repository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + ExportPunchCardUsageRecordCsvQuery request, + CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var normalizedStatus = PunchCardMapping.ParseUsageStatusFilter(request.Status); + + var records = await repository.ListUsageRecordsForExportAsync( + tenantId, + request.StoreId, + request.TemplateId, + request.Keyword, + normalizedStatus, + cancellationToken); + + if (records.Count == 0) + { + return new PunchCardUsageRecordExportDto + { + FileName = $"次卡使用记录_{DateTime.UtcNow:yyyyMMddHHmmss}.csv", + FileContentBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("\uFEFF使用单号,会员,手机号,次卡,兑换商品,使用时间,剩余次数,总次数,状态\n")), + TotalCount = 0 + }; + } + + var instanceIds = records.Select(item => item.PunchCardInstanceId).Distinct().ToList(); + var instances = await repository.GetInstancesByIdsAsync( + tenantId, + request.StoreId, + instanceIds, + cancellationToken); + + var instanceMap = instances.ToDictionary(item => item.Id, item => item); + + var templateIds = records.Select(item => item.PunchCardTemplateId) + .Concat(instances.Select(item => item.PunchCardTemplateId)) + .Distinct() + .ToList(); + + var templates = await repository.GetTemplatesByIdsAsync( + tenantId, + request.StoreId, + templateIds, + cancellationToken); + + var templateMap = templates.ToDictionary(item => item.Id, item => item); + + var csv = BuildCsv(records, instanceMap, templateMap); + var bytes = Encoding.UTF8.GetBytes($"\uFEFF{csv}"); + + return new PunchCardUsageRecordExportDto + { + FileName = $"次卡使用记录_{DateTime.UtcNow:yyyyMMddHHmmss}.csv", + FileContentBase64 = Convert.ToBase64String(bytes), + TotalCount = records.Count + }; + } + + private static string BuildCsv( + IReadOnlyCollection records, + IReadOnlyDictionary instanceMap, + IReadOnlyDictionary templateMap) + { + var lines = new List + { + "使用单号,会员,手机号,次卡,兑换商品,使用时间,剩余次数,总次数,状态" + }; + + var nowUtc = DateTime.UtcNow; + foreach (var record in records) + { + instanceMap.TryGetValue(record.PunchCardInstanceId, out var instance); + templateMap.TryGetValue(record.PunchCardTemplateId, out var template); + + var dto = PunchCardDtoFactory.ToUsageRecordDto(record, instance, template, nowUtc); + var statusText = ResolveStatusText(dto.DisplayStatus); + + lines.Add(string.Join(",", + Escape(dto.RecordNo), + Escape(dto.MemberName), + Escape(dto.MemberPhoneMasked), + Escape(dto.PunchCardName), + Escape(dto.ProductName), + Escape(dto.UsedAt.ToString("yyyy-MM-dd HH:mm:ss")), + dto.RemainingTimesAfterUse, + dto.TotalTimes, + Escape(statusText))); + } + + return string.Join('\n', lines); + } + + private static string ResolveStatusText(string value) + { + return value switch + { + "normal" => "正常使用", + "almost_used_up" => "即将用完", + "used_up" => "已用完", + "expired" => "已过期", + _ => "正常使用" + }; + } + + private static string Escape(string value) + { + var text = value.Replace("\"", "\"\""); + return $"\"{text}\""; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardTemplateDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardTemplateDetailQueryHandler.cs new file mode 100644 index 0000000..02da604 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardTemplateDetailQueryHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers; + +/// +/// 次卡模板详情查询处理器。 +/// +public sealed class GetPunchCardTemplateDetailQueryHandler( + IPunchCardRepository repository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + GetPunchCardTemplateDetailQuery request, + CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + + var template = await repository.FindTemplateByIdAsync( + tenantId, + request.StoreId, + request.TemplateId, + cancellationToken); + + if (template is null) + { + return null; + } + + var aggregate = await repository.GetTemplateAggregateByTemplateIdsAsync( + tenantId, + request.StoreId, + [request.TemplateId], + cancellationToken); + + var snapshot = aggregate.TryGetValue(template.Id, out var value) + ? value + : PunchCardDtoFactory.EmptyAggregate(template.Id); + + return PunchCardDtoFactory.ToDetailDto(template, snapshot); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardTemplateListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardTemplateListQueryHandler.cs new file mode 100644 index 0000000..6616b82 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardTemplateListQueryHandler.cs @@ -0,0 +1,67 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers; + +/// +/// 次卡模板列表查询处理器。 +/// +public sealed class GetPunchCardTemplateListQueryHandler( + IPunchCardRepository repository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + GetPunchCardTemplateListQuery request, + CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 200); + var status = PunchCardMapping.ParseTemplateStatusFilter(request.Status); + + var (items, totalCount) = await repository.SearchTemplatesAsync( + tenantId, + request.StoreId, + request.Keyword, + status, + page, + pageSize, + cancellationToken); + + var templateIds = items.Select(item => item.Id).ToList(); + var aggregates = await repository.GetTemplateAggregateByTemplateIdsAsync( + tenantId, + request.StoreId, + templateIds, + cancellationToken); + + var mappedItems = items + .Select(item => + { + var aggregate = aggregates.TryGetValue(item.Id, out var value) + ? value + : PunchCardDtoFactory.EmptyAggregate(item.Id); + return PunchCardDtoFactory.ToListItemDto(item, aggregate); + }) + .ToList(); + + var statsSnapshot = await repository.GetTemplateStatsAsync( + tenantId, + request.StoreId, + cancellationToken); + + return new PunchCardListResultDto + { + Items = mappedItems, + Page = page, + PageSize = pageSize, + TotalCount = totalCount, + Stats = PunchCardDtoFactory.ToStatsDto(statsSnapshot) + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardUsageRecordListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardUsageRecordListQueryHandler.cs new file mode 100644 index 0000000..98dae1b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/GetPunchCardUsageRecordListQueryHandler.cs @@ -0,0 +1,104 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers; + +/// +/// 次卡使用记录列表查询处理器。 +/// +public sealed class GetPunchCardUsageRecordListQueryHandler( + IPunchCardRepository repository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + GetPunchCardUsageRecordListQuery request, + CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var page = Math.Max(1, request.Page); + var pageSize = Math.Clamp(request.PageSize, 1, 500); + var normalizedStatus = PunchCardMapping.ParseUsageStatusFilter(request.Status); + + var (records, totalCount) = await repository.SearchUsageRecordsAsync( + tenantId, + request.StoreId, + request.TemplateId, + request.Keyword, + normalizedStatus, + page, + pageSize, + cancellationToken); + + var instanceIds = records.Select(item => item.PunchCardInstanceId).Distinct().ToList(); + var instances = await repository.GetInstancesByIdsAsync( + tenantId, + request.StoreId, + instanceIds, + cancellationToken); + + var instanceMap = instances.ToDictionary(item => item.Id, item => item); + + var templateIds = records.Select(item => item.PunchCardTemplateId) + .Concat(instances.Select(item => item.PunchCardTemplateId)) + .Distinct() + .ToList(); + + var templates = await repository.GetTemplatesByIdsAsync( + tenantId, + request.StoreId, + templateIds, + cancellationToken); + + var templateMap = templates.ToDictionary(item => item.Id, item => item); + + var nowUtc = DateTime.UtcNow; + var mappedRecords = records + .Select(record => + { + instanceMap.TryGetValue(record.PunchCardInstanceId, out var instance); + templateMap.TryGetValue(record.PunchCardTemplateId, out var template); + return PunchCardDtoFactory.ToUsageRecordDto(record, instance, template, nowUtc); + }) + .ToList(); + + var usageStats = await repository.GetUsageStatsAsync( + tenantId, + request.StoreId, + request.TemplateId, + nowUtc, + cancellationToken); + + var (templateRows, _) = await repository.SearchTemplatesAsync( + tenantId, + request.StoreId, + null, + null, + 1, + 500, + cancellationToken); + + var templateOptions = templateRows + .OrderBy(item => item.Name, StringComparer.Ordinal) + .Select(item => new PunchCardTemplateOptionDto + { + TemplateId = item.Id, + Name = item.Name + }) + .ToList(); + + return new PunchCardUsageRecordListResultDto + { + Items = mappedRecords, + Page = page, + PageSize = pageSize, + TotalCount = totalCount, + Stats = PunchCardDtoFactory.ToUsageStatsDto(usageStats), + TemplateOptions = templateOptions + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/SavePunchCardTemplateCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/SavePunchCardTemplateCommandHandler.cs new file mode 100644 index 0000000..24f5edd --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/SavePunchCardTemplateCommandHandler.cs @@ -0,0 +1,158 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers; + +/// +/// 次卡模板保存处理器。 +/// +public sealed class SavePunchCardTemplateCommandHandler( + IPunchCardRepository repository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + SavePunchCardTemplateCommand request, + CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + + var normalizedName = PunchCardMapping.NormalizeName(request.Name); + var normalizedCoverImageUrl = PunchCardMapping.NormalizeOptionalCoverUrl(request.CoverImageUrl); + var normalizedSalePrice = PunchCardMapping.NormalizeAmount(request.SalePrice, "salePrice", false); + var normalizedOriginalPrice = PunchCardMapping.NormalizeOptionalAmount(request.OriginalPrice, "originalPrice", true); + var normalizedTotalTimes = PunchCardMapping.NormalizeRequiredPositiveInt(request.TotalTimes, "totalTimes", 10_000); + + if (normalizedOriginalPrice.HasValue && normalizedOriginalPrice.Value < normalizedSalePrice) + { + throw new BusinessException(ErrorCodes.BadRequest, "originalPrice 不能小于 salePrice"); + } + + var validityType = PunchCardMapping.ParseValidityType(request.ValidityType); + var (normalizedValidityDays, normalizedValidFrom, normalizedValidTo) = PunchCardMapping.NormalizeValidity( + validityType, + request.ValidityDays, + request.ValidFrom, + request.ValidTo); + + var scopeType = PunchCardMapping.ParseScopeType(request.ScopeType); + var (normalizedCategoryIds, normalizedTagIds, normalizedProductIds) = PunchCardMapping.NormalizeScopeIds( + scopeType, + request.ScopeCategoryIds, + request.ScopeTagIds, + request.ScopeProductIds); + + var usageMode = PunchCardMapping.ParseUsageMode(request.UsageMode); + var normalizedUsageCapAmount = usageMode switch + { + PunchCardUsageMode.Free => null, + PunchCardUsageMode.Cap => PunchCardMapping.NormalizeOptionalAmount(request.UsageCapAmount, "usageCapAmount", false), + _ => null + }; + + if (usageMode == PunchCardUsageMode.Cap && !normalizedUsageCapAmount.HasValue) + { + throw new BusinessException(ErrorCodes.BadRequest, "usageCapAmount 不能为空"); + } + + var normalizedDailyLimit = PunchCardMapping.NormalizeOptionalLimit(request.DailyLimit, "dailyLimit", normalizedTotalTimes); + var normalizedPerOrderLimit = PunchCardMapping.NormalizeOptionalLimit(request.PerOrderLimit, "perOrderLimit", normalizedTotalTimes); + var normalizedPerUserPurchaseLimit = PunchCardMapping.NormalizeOptionalLimit(request.PerUserPurchaseLimit, "perUserPurchaseLimit", 1000); + + var expireStrategy = PunchCardMapping.ParseExpireStrategy(request.ExpireStrategy); + var normalizedDescription = PunchCardMapping.NormalizeOptionalDescription(request.Description); + var normalizedNotifyChannelsJson = PunchCardMapping.SerializeNotifyChannels(request.NotifyChannels); + + var normalizedCategoryIdsJson = PunchCardMapping.SerializeSnowflakeIds(normalizedCategoryIds); + var normalizedTagIdsJson = PunchCardMapping.SerializeSnowflakeIds(normalizedTagIds); + var normalizedProductIdsJson = PunchCardMapping.SerializeSnowflakeIds(normalizedProductIds); + + if (!request.TemplateId.HasValue) + { + var newEntity = PunchCardDtoFactory.CreateTemplateEntity( + request, + normalizedName, + normalizedCoverImageUrl, + normalizedSalePrice, + normalizedOriginalPrice, + normalizedTotalTimes, + validityType, + normalizedValidityDays, + normalizedValidFrom, + normalizedValidTo, + scopeType, + normalizedCategoryIdsJson, + normalizedTagIdsJson, + normalizedProductIdsJson, + usageMode, + normalizedUsageCapAmount, + normalizedDailyLimit, + normalizedPerOrderLimit, + normalizedPerUserPurchaseLimit, + expireStrategy, + normalizedDescription, + normalizedNotifyChannelsJson); + + await repository.AddTemplateAsync(newEntity, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); + + return PunchCardDtoFactory.ToDetailDto( + newEntity, + PunchCardDtoFactory.EmptyAggregate(newEntity.Id)); + } + + var entity = await repository.FindTemplateByIdAsync( + tenantId, + request.StoreId, + request.TemplateId.Value, + cancellationToken) + ?? throw new BusinessException(ErrorCodes.NotFound, "次卡不存在"); + + entity.Name = normalizedName; + entity.CoverImageUrl = string.IsNullOrWhiteSpace(normalizedCoverImageUrl) + ? null + : normalizedCoverImageUrl; + entity.SalePrice = normalizedSalePrice; + entity.OriginalPrice = normalizedOriginalPrice; + entity.TotalTimes = normalizedTotalTimes; + entity.ValidityType = validityType; + entity.ValidityDays = normalizedValidityDays; + entity.ValidFrom = normalizedValidFrom; + entity.ValidTo = normalizedValidTo; + entity.ScopeType = scopeType; + entity.ScopeCategoryIdsJson = normalizedCategoryIdsJson; + entity.ScopeTagIdsJson = normalizedTagIdsJson; + entity.ScopeProductIdsJson = normalizedProductIdsJson; + entity.UsageMode = usageMode; + entity.UsageCapAmount = normalizedUsageCapAmount; + entity.DailyLimit = normalizedDailyLimit; + entity.PerOrderLimit = normalizedPerOrderLimit; + entity.PerUserPurchaseLimit = normalizedPerUserPurchaseLimit; + entity.AllowTransfer = request.AllowTransfer; + entity.ExpireStrategy = expireStrategy; + entity.Description = normalizedDescription; + entity.NotifyChannelsJson = normalizedNotifyChannelsJson; + + await repository.UpdateTemplateAsync(entity, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); + + var aggregateMap = await repository.GetTemplateAggregateByTemplateIdsAsync( + tenantId, + request.StoreId, + [entity.Id], + cancellationToken); + + var aggregate = aggregateMap.TryGetValue(entity.Id, out var value) + ? value + : PunchCardDtoFactory.EmptyAggregate(entity.Id); + + return PunchCardDtoFactory.ToDetailDto(entity, aggregate); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/WritePunchCardUsageRecordCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/WritePunchCardUsageRecordCommandHandler.cs new file mode 100644 index 0000000..4d30427 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Handlers/WritePunchCardUsageRecordCommandHandler.cs @@ -0,0 +1,160 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Domain.Coupons.Entities; +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers; + +/// +/// 写入次卡使用记录处理器。 +/// +public sealed class WritePunchCardUsageRecordCommandHandler( + IPunchCardRepository repository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + WritePunchCardUsageRecordCommand request, + CancellationToken cancellationToken) + { + var tenantId = tenantProvider.GetCurrentTenantId(); + var usedAt = request.UsedAt.HasValue + ? PunchCardMapping.NormalizeUtc(request.UsedAt.Value) + : DateTime.UtcNow; + + var template = await repository.FindTemplateByIdAsync( + tenantId, + request.StoreId, + request.TemplateId, + cancellationToken) + ?? throw new BusinessException(ErrorCodes.NotFound, "次卡不存在"); + + if (template.Status != PunchCardStatus.Enabled) + { + throw new BusinessException(ErrorCodes.BadRequest, "次卡已下架,无法使用"); + } + + var productName = PunchCardMapping.NormalizeProductName(request.ProductName); + var usedTimes = PunchCardMapping.NormalizeRequiredPositiveInt(request.UsedTimes, "usedTimes", template.TotalTimes); + var extraPayAmount = PunchCardMapping.NormalizeOptionalAmount(request.ExtraPayAmount, "extraPayAmount", true); + + PunchCardInstance? instance = null; + if (request.InstanceId.HasValue && request.InstanceId.Value > 0) + { + instance = await repository.FindInstanceByIdAsync( + tenantId, + request.StoreId, + request.InstanceId.Value, + cancellationToken); + } + else if (!string.IsNullOrWhiteSpace(request.InstanceNo)) + { + var normalizedInstanceNo = PunchCardMapping.NormalizeInstanceNo(request.InstanceNo); + instance = await repository.FindInstanceByNoAsync( + tenantId, + request.StoreId, + normalizedInstanceNo, + cancellationToken); + } + + if (instance is not null && instance.PunchCardTemplateId != template.Id) + { + throw new BusinessException(ErrorCodes.BadRequest, "次卡实例与模板不匹配"); + } + + var isNewInstance = false; + if (instance is null) + { + var memberName = PunchCardMapping.NormalizeMemberName(request.MemberName); + var memberPhoneMasked = PunchCardMapping.NormalizeMemberPhoneMasked(request.MemberPhoneMasked); + var purchasedAt = usedAt; + + instance = new PunchCardInstance + { + StoreId = request.StoreId, + PunchCardTemplateId = template.Id, + InstanceNo = PunchCardDtoFactory.GenerateInstanceNo(usedAt), + MemberName = memberName, + MemberPhoneMasked = memberPhoneMasked, + PurchasedAt = purchasedAt, + ExpiresAt = PunchCardMapping.ResolveInstanceExpireAt(template, purchasedAt), + TotalTimes = template.TotalTimes, + RemainingTimes = template.TotalTimes, + PaidAmount = template.SalePrice, + Status = PunchCardInstanceStatus.Active + }; + isNewInstance = true; + } + + if (PunchCardMapping.IsInstanceExpired(instance, usedAt)) + { + throw new BusinessException(ErrorCodes.BadRequest, "次卡已过期"); + } + + if (instance.Status == PunchCardInstanceStatus.Refunded) + { + throw new BusinessException(ErrorCodes.BadRequest, "次卡已退款"); + } + + if (instance.RemainingTimes <= 0 || instance.Status == PunchCardInstanceStatus.UsedUp) + { + throw new BusinessException(ErrorCodes.BadRequest, "次卡已用完"); + } + + if (template.PerOrderLimit.HasValue && usedTimes > template.PerOrderLimit.Value) + { + throw new BusinessException(ErrorCodes.BadRequest, "超出每单限用次数"); + } + + if (usedTimes > instance.RemainingTimes) + { + throw new BusinessException(ErrorCodes.BadRequest, "超出次卡剩余次数"); + } + + var remainingTimes = instance.RemainingTimes - usedTimes; + var statusAfterUse = PunchCardMapping.ResolveUsageRecordStatus(instance, remainingTimes, usedAt); + + instance.RemainingTimes = remainingTimes; + instance.Status = statusAfterUse switch + { + PunchCardUsageRecordStatus.UsedUp => PunchCardInstanceStatus.UsedUp, + PunchCardUsageRecordStatus.Expired => PunchCardInstanceStatus.Expired, + _ => PunchCardInstanceStatus.Active + }; + + if (isNewInstance) + { + await repository.AddInstanceAsync(instance, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); + } + else + { + await repository.UpdateInstanceAsync(instance, cancellationToken); + } + + var record = new PunchCardUsageRecord + { + StoreId = request.StoreId, + PunchCardTemplateId = template.Id, + PunchCardInstanceId = instance.Id, + RecordNo = PunchCardDtoFactory.GenerateRecordNo(usedAt), + ProductName = productName, + UsedAt = usedAt, + UsedTimes = usedTimes, + RemainingTimesAfterUse = remainingTimes, + StatusAfterUse = statusAfterUse, + ExtraPayAmount = extraPayAmount + }; + + await repository.AddUsageRecordAsync(record, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); + + return PunchCardDtoFactory.ToUsageRecordDto(record, instance, template, usedAt); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/PunchCardDtoFactory.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/PunchCardDtoFactory.cs new file mode 100644 index 0000000..52223e3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/PunchCardDtoFactory.cs @@ -0,0 +1,210 @@ +using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Domain.Coupons.Entities; +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Domain.Coupons.Repositories; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard; + +/// +/// 次卡 DTO 构造器。 +/// +internal static class PunchCardDtoFactory +{ + public static PunchCardTemplateAggregateSnapshot EmptyAggregate(long templateId) + { + return new PunchCardTemplateAggregateSnapshot + { + TemplateId = templateId, + SoldCount = 0, + ActiveCount = 0, + RevenueAmount = 0m + }; + } + + public static PunchCardListItemDto ToListItemDto( + PunchCardTemplate template, + PunchCardTemplateAggregateSnapshot aggregate) + { + return new PunchCardListItemDto + { + Id = template.Id, + Name = template.Name, + CoverImageUrl = template.CoverImageUrl, + SalePrice = template.SalePrice, + OriginalPrice = template.OriginalPrice, + TotalTimes = template.TotalTimes, + ValiditySummary = PunchCardMapping.BuildValiditySummary(template), + ScopeType = PunchCardMapping.ToScopeTypeText(template.ScopeType), + UsageMode = PunchCardMapping.ToUsageModeText(template.UsageMode), + UsageCapAmount = template.UsageCapAmount, + DailyLimit = template.DailyLimit, + Status = PunchCardMapping.ToTemplateStatusText(template.Status), + IsDimmed = template.Status == PunchCardStatus.Disabled, + SoldCount = aggregate.SoldCount, + ActiveCount = aggregate.ActiveCount, + RevenueAmount = decimal.Round(aggregate.RevenueAmount, 2, MidpointRounding.AwayFromZero), + UpdatedAt = template.UpdatedAt ?? template.CreatedAt + }; + } + + public static PunchCardDetailDto ToDetailDto( + PunchCardTemplate template, + PunchCardTemplateAggregateSnapshot aggregate) + { + return new PunchCardDetailDto + { + Id = template.Id, + StoreId = template.StoreId, + Name = template.Name, + CoverImageUrl = template.CoverImageUrl, + SalePrice = template.SalePrice, + OriginalPrice = template.OriginalPrice, + TotalTimes = template.TotalTimes, + ValidityType = PunchCardMapping.ToValidityTypeText(template.ValidityType), + ValidityDays = template.ValidityDays, + ValidFrom = template.ValidFrom, + ValidTo = template.ValidTo, + Scope = new PunchCardScopeDto + { + ScopeType = PunchCardMapping.ToScopeTypeText(template.ScopeType), + CategoryIds = PunchCardMapping.DeserializeSnowflakeIds(template.ScopeCategoryIdsJson), + TagIds = PunchCardMapping.DeserializeSnowflakeIds(template.ScopeTagIdsJson), + ProductIds = PunchCardMapping.DeserializeSnowflakeIds(template.ScopeProductIdsJson) + }, + UsageMode = PunchCardMapping.ToUsageModeText(template.UsageMode), + UsageCapAmount = template.UsageCapAmount, + DailyLimit = template.DailyLimit, + PerOrderLimit = template.PerOrderLimit, + PerUserPurchaseLimit = template.PerUserPurchaseLimit, + AllowTransfer = template.AllowTransfer, + ExpireStrategy = PunchCardMapping.ToExpireStrategyText(template.ExpireStrategy), + Description = template.Description, + NotifyChannels = PunchCardMapping.DeserializeNotifyChannels(template.NotifyChannelsJson), + Status = PunchCardMapping.ToTemplateStatusText(template.Status), + SoldCount = aggregate.SoldCount, + ActiveCount = aggregate.ActiveCount, + RevenueAmount = decimal.Round(aggregate.RevenueAmount, 2, MidpointRounding.AwayFromZero), + UpdatedAt = template.UpdatedAt ?? template.CreatedAt + }; + } + + public static PunchCardStatsDto ToStatsDto(PunchCardTemplateStatsSnapshot source) + { + return new PunchCardStatsDto + { + OnSaleCount = source.OnSaleCount, + TotalSoldCount = source.TotalSoldCount, + TotalRevenueAmount = decimal.Round(source.TotalRevenueAmount, 2, MidpointRounding.AwayFromZero), + ActiveInUseCount = source.ActiveInUseCount + }; + } + + public static PunchCardUsageStatsDto ToUsageStatsDto(PunchCardUsageStatsSnapshot source) + { + return new PunchCardUsageStatsDto + { + TodayUsedCount = source.TodayUsedCount, + MonthUsedCount = source.MonthUsedCount, + ExpiringSoonCount = source.ExpiringSoonCount + }; + } + + public static PunchCardUsageRecordDto ToUsageRecordDto( + PunchCardUsageRecord record, + PunchCardInstance? instance, + PunchCardTemplate? template, + DateTime nowUtc) + { + var resolvedTotalTimes = instance?.TotalTimes ?? template?.TotalTimes ?? 0; + var status = record.StatusAfterUse; + + if (instance is not null) + { + status = PunchCardMapping.ResolveUsageRecordStatus(instance, record.RemainingTimesAfterUse, nowUtc); + } + + return new PunchCardUsageRecordDto + { + Id = record.Id, + RecordNo = record.RecordNo, + PunchCardTemplateId = record.PunchCardTemplateId, + PunchCardName = template?.Name ?? string.Empty, + PunchCardInstanceId = record.PunchCardInstanceId, + MemberName = instance?.MemberName ?? string.Empty, + MemberPhoneMasked = instance?.MemberPhoneMasked ?? string.Empty, + ProductName = record.ProductName, + UsedAt = record.UsedAt, + UsedTimes = record.UsedTimes, + RemainingTimesAfterUse = record.RemainingTimesAfterUse, + TotalTimes = resolvedTotalTimes, + DisplayStatus = PunchCardMapping.ToUsageDisplayStatusText(status), + ExtraPayAmount = record.ExtraPayAmount + }; + } + + public static PunchCardTemplate CreateTemplateEntity( + SavePunchCardTemplateCommand request, + string normalizedName, + string normalizedCoverImageUrl, + decimal normalizedSalePrice, + decimal? normalizedOriginalPrice, + int normalizedTotalTimes, + PunchCardValidityType validityType, + int? normalizedValidityDays, + DateTime? normalizedValidFrom, + DateTime? normalizedValidTo, + PunchCardScopeType scopeType, + string normalizedCategoryIdsJson, + string normalizedTagIdsJson, + string normalizedProductIdsJson, + PunchCardUsageMode usageMode, + decimal? normalizedUsageCapAmount, + int? normalizedDailyLimit, + int? normalizedPerOrderLimit, + int? normalizedPerUserPurchaseLimit, + PunchCardExpireStrategy expireStrategy, + string? normalizedDescription, + string normalizedNotifyChannelsJson) + { + return new PunchCardTemplate + { + StoreId = request.StoreId, + Name = normalizedName, + CoverImageUrl = string.IsNullOrWhiteSpace(normalizedCoverImageUrl) + ? null + : normalizedCoverImageUrl, + SalePrice = normalizedSalePrice, + OriginalPrice = normalizedOriginalPrice, + TotalTimes = normalizedTotalTimes, + ValidityType = validityType, + ValidityDays = normalizedValidityDays, + ValidFrom = normalizedValidFrom, + ValidTo = normalizedValidTo, + ScopeType = scopeType, + ScopeCategoryIdsJson = normalizedCategoryIdsJson, + ScopeTagIdsJson = normalizedTagIdsJson, + ScopeProductIdsJson = normalizedProductIdsJson, + UsageMode = usageMode, + UsageCapAmount = normalizedUsageCapAmount, + DailyLimit = normalizedDailyLimit, + PerOrderLimit = normalizedPerOrderLimit, + PerUserPurchaseLimit = normalizedPerUserPurchaseLimit, + AllowTransfer = request.AllowTransfer, + ExpireStrategy = expireStrategy, + Description = normalizedDescription, + NotifyChannelsJson = normalizedNotifyChannelsJson, + Status = PunchCardStatus.Enabled + }; + } + + public static string GenerateInstanceNo(DateTime nowUtc) + { + return $"PKI{nowUtc:yyyyMMddHHmmssfff}{Random.Shared.Next(1000, 9999)}"; + } + + public static string GenerateRecordNo(DateTime nowUtc) + { + return $"PK{nowUtc:yyyyMMddHHmmssfff}{Random.Shared.Next(1000, 9999)}"; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/PunchCardMapping.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/PunchCardMapping.cs new file mode 100644 index 0000000..5061e30 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/PunchCardMapping.cs @@ -0,0 +1,546 @@ +using System.Text.Json; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; +using TakeoutSaaS.Domain.Coupons.Entities; +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard; + +/// +/// 次卡模块映射与标准化。 +/// +internal static class PunchCardMapping +{ + private static readonly HashSet AllowedNotifyChannels = + [ + "in_app", + "sms" + ]; + + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) + { + PropertyNameCaseInsensitive = true + }; + + public static PunchCardStatus? ParseTemplateStatusFilter(string? value) + { + var normalized = (value ?? string.Empty).Trim().ToLowerInvariant(); + return normalized switch + { + "" => null, + "enabled" => PunchCardStatus.Enabled, + "disabled" => PunchCardStatus.Disabled, + _ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法") + }; + } + + public static PunchCardStatus ParseTemplateStatus(string? value) + { + var normalized = (value ?? string.Empty).Trim().ToLowerInvariant(); + return normalized switch + { + "enabled" => PunchCardStatus.Enabled, + "disabled" => PunchCardStatus.Disabled, + _ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法") + }; + } + + public static string ToTemplateStatusText(PunchCardStatus value) + { + return value switch + { + PunchCardStatus.Enabled => "enabled", + PunchCardStatus.Disabled => "disabled", + _ => "disabled" + }; + } + + public static PunchCardValidityType ParseValidityType(string? value) + { + var normalized = (value ?? string.Empty).Trim().ToLowerInvariant(); + return normalized switch + { + "days" => PunchCardValidityType.Days, + "range" => PunchCardValidityType.DateRange, + _ => throw new BusinessException(ErrorCodes.BadRequest, "validityType 参数不合法") + }; + } + + public static string ToValidityTypeText(PunchCardValidityType value) + { + return value switch + { + PunchCardValidityType.Days => "days", + PunchCardValidityType.DateRange => "range", + _ => "days" + }; + } + + public static PunchCardScopeType ParseScopeType(string? value) + { + var normalized = (value ?? string.Empty).Trim().ToLowerInvariant(); + return normalized switch + { + "all" => PunchCardScopeType.All, + "category" => PunchCardScopeType.Category, + "tag" => PunchCardScopeType.Tag, + "product" => PunchCardScopeType.Product, + _ => throw new BusinessException(ErrorCodes.BadRequest, "scopeType 参数不合法") + }; + } + + public static string ToScopeTypeText(PunchCardScopeType value) + { + return value switch + { + PunchCardScopeType.All => "all", + PunchCardScopeType.Category => "category", + PunchCardScopeType.Tag => "tag", + PunchCardScopeType.Product => "product", + _ => "all" + }; + } + + public static PunchCardUsageMode ParseUsageMode(string? value) + { + var normalized = (value ?? string.Empty).Trim().ToLowerInvariant(); + return normalized switch + { + "free" => PunchCardUsageMode.Free, + "cap" => PunchCardUsageMode.Cap, + _ => throw new BusinessException(ErrorCodes.BadRequest, "usageMode 参数不合法") + }; + } + + public static string ToUsageModeText(PunchCardUsageMode value) + { + return value switch + { + PunchCardUsageMode.Free => "free", + PunchCardUsageMode.Cap => "cap", + _ => "free" + }; + } + + public static PunchCardExpireStrategy ParseExpireStrategy(string? value) + { + var normalized = (value ?? string.Empty).Trim().ToLowerInvariant(); + return normalized switch + { + "invalidate" => PunchCardExpireStrategy.Invalidate, + "refund" => PunchCardExpireStrategy.Refund, + _ => throw new BusinessException(ErrorCodes.BadRequest, "expireStrategy 参数不合法") + }; + } + + public static string ToExpireStrategyText(PunchCardExpireStrategy value) + { + return value switch + { + PunchCardExpireStrategy.Invalidate => "invalidate", + PunchCardExpireStrategy.Refund => "refund", + _ => "invalidate" + }; + } + + public static PunchCardUsageRecordFilterStatus? ParseUsageStatusFilter(string? value) + { + var normalized = (value ?? string.Empty).Trim().ToLowerInvariant(); + return normalized switch + { + "" => null, + "normal" => PunchCardUsageRecordFilterStatus.Normal, + "used_up" => PunchCardUsageRecordFilterStatus.UsedUp, + "expired" => PunchCardUsageRecordFilterStatus.Expired, + _ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法") + }; + } + + public static string ToUsageDisplayStatusText(PunchCardUsageRecordStatus value) + { + return value switch + { + PunchCardUsageRecordStatus.Normal => "normal", + PunchCardUsageRecordStatus.AlmostUsedUp => "almost_used_up", + PunchCardUsageRecordStatus.UsedUp => "used_up", + PunchCardUsageRecordStatus.Expired => "expired", + _ => "normal" + }; + } + + public static DateTime NormalizeUtc(DateTime value) + { + return value.Kind switch + { + DateTimeKind.Utc => value, + DateTimeKind.Local => value.ToUniversalTime(), + _ => DateTime.SpecifyKind(value, DateTimeKind.Utc) + }; + } + + public static string NormalizeName(string? value) + { + var normalized = (value ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + throw new BusinessException(ErrorCodes.BadRequest, "name 不能为空"); + } + + if (normalized.Length > 64) + { + throw new BusinessException(ErrorCodes.BadRequest, "name 长度不能超过 64"); + } + + return normalized; + } + + public static string NormalizeOptionalCoverUrl(string? value) + { + var normalized = (value ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + return string.Empty; + } + + if (normalized.Length > 512) + { + throw new BusinessException(ErrorCodes.BadRequest, "coverImageUrl 长度不能超过 512"); + } + + return normalized; + } + + public static string? NormalizeOptionalDescription(string? value) + { + var normalized = (value ?? string.Empty).Trim(); + if (normalized.Length == 0) + { + return null; + } + + if (normalized.Length > 512) + { + throw new BusinessException(ErrorCodes.BadRequest, "description 长度不能超过 512"); + } + + return normalized; + } + + public static string NormalizeInstanceNo(string? value) + { + var normalized = (value ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + throw new BusinessException(ErrorCodes.BadRequest, "punchCardInstanceNo 不能为空"); + } + + if (normalized.Length > 32) + { + throw new BusinessException(ErrorCodes.BadRequest, "punchCardInstanceNo 长度不能超过 32"); + } + + return normalized; + } + + public static string NormalizeMemberName(string? value) + { + var normalized = (value ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + throw new BusinessException(ErrorCodes.BadRequest, "memberName 不能为空"); + } + + if (normalized.Length > 64) + { + throw new BusinessException(ErrorCodes.BadRequest, "memberName 长度不能超过 64"); + } + + return normalized; + } + + public static string NormalizeMemberPhoneMasked(string? value) + { + var normalized = (value ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + throw new BusinessException(ErrorCodes.BadRequest, "memberPhoneMasked 不能为空"); + } + + if (normalized.Length > 32) + { + throw new BusinessException(ErrorCodes.BadRequest, "memberPhoneMasked 长度不能超过 32"); + } + + return normalized; + } + + public static string NormalizeProductName(string? value) + { + var normalized = (value ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + throw new BusinessException(ErrorCodes.BadRequest, "productName 不能为空"); + } + + if (normalized.Length > 128) + { + throw new BusinessException(ErrorCodes.BadRequest, "productName 长度不能超过 128"); + } + + return normalized; + } + + public static decimal NormalizeAmount(decimal value, string fieldName, bool allowZero = false) + { + if (value < 0 || (!allowZero && value <= 0)) + { + throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法"); + } + + return decimal.Round(value, 2, MidpointRounding.AwayFromZero); + } + + public static decimal? NormalizeOptionalAmount(decimal? value, string fieldName, bool allowZero = true) + { + if (!value.HasValue) + { + return null; + } + + if (value.Value < 0 || (!allowZero && value.Value <= 0)) + { + throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法"); + } + + return decimal.Round(value.Value, 2, MidpointRounding.AwayFromZero); + } + + public static int NormalizeRequiredPositiveInt(int value, string fieldName, int max = 100_000) + { + if (value <= 0 || value > max) + { + throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法"); + } + + return value; + } + + public static int? NormalizeOptionalLimit(int? value, string fieldName, int max = 100_000) + { + if (!value.HasValue || value.Value <= 0) + { + return null; + } + + if (value.Value > max) + { + throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法"); + } + + return value; + } + + public static (int? ValidityDays, DateTime? ValidFrom, DateTime? ValidTo) NormalizeValidity( + PunchCardValidityType validityType, + int? validityDays, + DateTime? validFrom, + DateTime? validTo) + { + return validityType switch + { + PunchCardValidityType.Days => + ( + NormalizeRequiredPositiveInt(validityDays ?? 0, "validityDays", 3650), + null, + null + ), + PunchCardValidityType.DateRange => NormalizeRange(validFrom, validTo), + _ => throw new BusinessException(ErrorCodes.BadRequest, "validityType 参数不合法") + }; + } + + public static (IReadOnlyList CategoryIds, IReadOnlyList TagIds, IReadOnlyList ProductIds) NormalizeScopeIds( + PunchCardScopeType scopeType, + IReadOnlyCollection? categoryIds, + IReadOnlyCollection? tagIds, + IReadOnlyCollection? productIds) + { + var normalizedCategoryIds = NormalizeSnowflakeIds(categoryIds, "scopeCategoryIds", false); + var normalizedTagIds = NormalizeSnowflakeIds(tagIds, "scopeTagIds", false); + var normalizedProductIds = NormalizeSnowflakeIds(productIds, "scopeProductIds", false); + + return scopeType switch + { + PunchCardScopeType.All => ([], [], []), + PunchCardScopeType.Category => + normalizedCategoryIds.Count == 0 + ? throw new BusinessException(ErrorCodes.BadRequest, "scopeCategoryIds 不能为空") + : (normalizedCategoryIds, [], []), + PunchCardScopeType.Tag => + normalizedTagIds.Count == 0 + ? throw new BusinessException(ErrorCodes.BadRequest, "scopeTagIds 不能为空") + : ([], normalizedTagIds, []), + PunchCardScopeType.Product => + normalizedProductIds.Count == 0 + ? throw new BusinessException(ErrorCodes.BadRequest, "scopeProductIds 不能为空") + : ([], [], normalizedProductIds), + _ => throw new BusinessException(ErrorCodes.BadRequest, "scopeType 参数不合法") + }; + } + + public static IReadOnlyList NormalizeNotifyChannels(IEnumerable? values) + { + var normalized = (values ?? []) + .Select(item => (item ?? string.Empty).Trim().ToLowerInvariant()) + .Where(item => !string.IsNullOrWhiteSpace(item)) + .Distinct() + .ToList(); + + if (normalized.Count == 0) + { + throw new BusinessException(ErrorCodes.BadRequest, "notifyChannels 不能为空"); + } + + if (normalized.Any(item => !AllowedNotifyChannels.Contains(item))) + { + throw new BusinessException(ErrorCodes.BadRequest, "notifyChannels 存在非法值"); + } + + return normalized; + } + + public static IReadOnlyList DeserializeNotifyChannels(string? payload) + { + if (string.IsNullOrWhiteSpace(payload)) + { + return []; + } + + var values = JsonSerializer.Deserialize>(payload, JsonOptions) ?? []; + return values + .Select(item => (item ?? string.Empty).Trim().ToLowerInvariant()) + .Where(item => AllowedNotifyChannels.Contains(item)) + .Distinct() + .ToList(); + } + + public static string SerializeNotifyChannels(IEnumerable? values) + { + return JsonSerializer.Serialize(NormalizeNotifyChannels(values), JsonOptions); + } + + public static IReadOnlyList DeserializeSnowflakeIds(string? payload) + { + if (string.IsNullOrWhiteSpace(payload)) + { + return []; + } + + var values = JsonSerializer.Deserialize>(payload, JsonOptions) ?? []; + return values + .Where(id => id > 0) + .Distinct() + .OrderBy(id => id) + .ToList(); + } + + public static string SerializeSnowflakeIds(IEnumerable? values) + { + return JsonSerializer.Serialize(NormalizeSnowflakeIds(values, "ids", false), JsonOptions); + } + + public static string BuildValiditySummary(PunchCardTemplate template) + { + return template.ValidityType switch + { + PunchCardValidityType.Days => $"{template.ValidityDays ?? 0}天有效", + PunchCardValidityType.DateRange when template.ValidFrom.HasValue && template.ValidTo.HasValue => + $"{template.ValidFrom.Value:yyyy-MM-dd} 至 {template.ValidTo.Value:yyyy-MM-dd}", + _ => "-" + }; + } + + public static DateTime ResolveInstanceExpireAt(PunchCardTemplate template, DateTime purchasedAtUtc) + { + var purchasedAt = NormalizeUtc(purchasedAtUtc); + + return template.ValidityType switch + { + PunchCardValidityType.Days => purchasedAt.Date.AddDays(template.ValidityDays ?? 0).AddTicks(-1), + PunchCardValidityType.DateRange => template.ValidTo ?? purchasedAt.Date.AddTicks(-1), + _ => purchasedAt.Date.AddTicks(-1) + }; + } + + public static bool IsInstanceExpired(PunchCardInstance instance, DateTime nowUtc) + { + var utcNow = NormalizeUtc(nowUtc); + if (instance.Status == PunchCardInstanceStatus.Expired) + { + return true; + } + + return instance.ExpiresAt.HasValue && instance.ExpiresAt.Value < utcNow; + } + + public static PunchCardUsageRecordStatus ResolveUsageRecordStatus( + PunchCardInstance instance, + int remainingTimes, + DateTime usedAtUtc) + { + if (IsInstanceExpired(instance, usedAtUtc)) + { + return PunchCardUsageRecordStatus.Expired; + } + + if (remainingTimes <= 0) + { + return PunchCardUsageRecordStatus.UsedUp; + } + + return remainingTimes <= 2 + ? PunchCardUsageRecordStatus.AlmostUsedUp + : PunchCardUsageRecordStatus.Normal; + } + + private static IReadOnlyList NormalizeSnowflakeIds( + IEnumerable? values, + string fieldName, + bool required) + { + var normalized = (values ?? []) + .Where(id => id > 0) + .Distinct() + .OrderBy(id => id) + .ToList(); + + if (required && normalized.Count == 0) + { + throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 不能为空"); + } + + return normalized; + } + + private static (int? ValidityDays, DateTime? ValidFrom, DateTime? ValidTo) NormalizeRange( + DateTime? validFrom, + DateTime? validTo) + { + if (!validFrom.HasValue || !validTo.HasValue) + { + throw new BusinessException(ErrorCodes.BadRequest, "validFrom / validTo 不能为空"); + } + + var normalizedFrom = NormalizeUtc(validFrom.Value).Date; + var normalizedTo = NormalizeUtc(validTo.Value).Date.AddDays(1).AddTicks(-1); + + if (normalizedFrom > normalizedTo) + { + throw new BusinessException(ErrorCodes.BadRequest, "validFrom 不能晚于 validTo"); + } + + return (null, normalizedFrom, normalizedTo); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/ExportPunchCardUsageRecordCsvQuery.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/ExportPunchCardUsageRecordCsvQuery.cs new file mode 100644 index 0000000..597f1a4 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/ExportPunchCardUsageRecordCsvQuery.cs @@ -0,0 +1,30 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; + +/// +/// 导出次卡使用记录 CSV。 +/// +public sealed class ExportPunchCardUsageRecordCsvQuery : IRequest +{ + /// + /// 操作门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 次卡模板筛选 ID(可空)。 + /// + public long? TemplateId { get; init; } + + /// + /// 状态筛选(normal/used_up/expired)。 + /// + public string? Status { get; init; } + + /// + /// 关键字(会员/商品)。 + /// + public string? Keyword { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardTemplateDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardTemplateDetailQuery.cs new file mode 100644 index 0000000..80b48e2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardTemplateDetailQuery.cs @@ -0,0 +1,20 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; + +/// +/// 查询次卡模板详情。 +/// +public sealed class GetPunchCardTemplateDetailQuery : IRequest +{ + /// + /// 操作门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 次卡模板 ID。 + /// + public long TemplateId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardTemplateListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardTemplateListQuery.cs new file mode 100644 index 0000000..3b3b59b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardTemplateListQuery.cs @@ -0,0 +1,35 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; + +/// +/// 查询次卡模板列表。 +/// +public sealed class GetPunchCardTemplateListQuery : IRequest +{ + /// + /// 操作门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 名称关键字。 + /// + public string? Keyword { get; init; } + + /// + /// 状态筛选(enabled/disabled)。 + /// + public string? Status { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 4; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardUsageRecordListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardUsageRecordListQuery.cs new file mode 100644 index 0000000..30c7eaa --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Coupons/PunchCard/Queries/GetPunchCardUsageRecordListQuery.cs @@ -0,0 +1,40 @@ +using MediatR; +using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto; + +namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Queries; + +/// +/// 查询次卡使用记录列表。 +/// +public sealed class GetPunchCardUsageRecordListQuery : IRequest +{ + /// + /// 操作门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 次卡模板筛选 ID(可空)。 + /// + public long? TemplateId { get; init; } + + /// + /// 状态筛选(normal/used_up/expired)。 + /// + public string? Status { get; init; } + + /// + /// 关键字(会员/商品)。 + /// + public string? Keyword { get; init; } + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 10; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardInstance.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardInstance.cs new file mode 100644 index 0000000..124b61f --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardInstance.cs @@ -0,0 +1,65 @@ +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Coupons.Entities; + +/// +/// 次卡实例(顾客购买后生成)。 +/// +public sealed class PunchCardInstance : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 次卡模板 ID。 + /// + public long PunchCardTemplateId { get; set; } + + /// + /// 实例编号(业务唯一)。 + /// + public string InstanceNo { get; set; } = string.Empty; + + /// + /// 会员名称。 + /// + public string MemberName { get; set; } = string.Empty; + + /// + /// 会员手机号(脱敏)。 + /// + public string MemberPhoneMasked { get; set; } = string.Empty; + + /// + /// 购买时间(UTC)。 + /// + public DateTime PurchasedAt { get; set; } + + /// + /// 过期时间(UTC,可空)。 + /// + public DateTime? ExpiresAt { get; set; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; set; } + + /// + /// 剩余次数。 + /// + public int RemainingTimes { get; set; } + + /// + /// 实付金额。 + /// + public decimal PaidAmount { get; set; } + + /// + /// 实例状态。 + /// + public PunchCardInstanceStatus Status { get; set; } = PunchCardInstanceStatus.Active; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardTemplate.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardTemplate.cs new file mode 100644 index 0000000..4594300 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardTemplate.cs @@ -0,0 +1,130 @@ +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Coupons.Entities; + +/// +/// 次卡模板配置。 +/// +public sealed class PunchCardTemplate : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 次卡名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 封面图片地址。 + /// + public string? CoverImageUrl { get; set; } + + /// + /// 售价。 + /// + public decimal SalePrice { get; set; } + + /// + /// 原价。 + /// + public decimal? OriginalPrice { get; set; } + + /// + /// 总次数。 + /// + public int TotalTimes { get; set; } + + /// + /// 有效期类型。 + /// + public PunchCardValidityType ValidityType { get; set; } = PunchCardValidityType.Days; + + /// + /// 固定天数(ValidityType=Days 时有效)。 + /// + public int? ValidityDays { get; set; } + + /// + /// 固定开始日期(UTC,ValidityType=DateRange 时有效)。 + /// + public DateTime? ValidFrom { get; set; } + + /// + /// 固定结束日期(UTC,ValidityType=DateRange 时有效)。 + /// + public DateTime? ValidTo { get; set; } + + /// + /// 适用范围类型。 + /// + public PunchCardScopeType ScopeType { get; set; } = PunchCardScopeType.All; + + /// + /// 指定分类 ID JSON。 + /// + public string ScopeCategoryIdsJson { get; set; } = "[]"; + + /// + /// 指定标签 ID JSON。 + /// + public string ScopeTagIdsJson { get; set; } = "[]"; + + /// + /// 指定商品 ID JSON。 + /// + public string ScopeProductIdsJson { get; set; } = "[]"; + + /// + /// 使用模式。 + /// + public PunchCardUsageMode UsageMode { get; set; } = PunchCardUsageMode.Free; + + /// + /// 金额上限(UsageMode=Cap 时有效)。 + /// + public decimal? UsageCapAmount { get; set; } + + /// + /// 每日限用次数。 + /// + public int? DailyLimit { get; set; } + + /// + /// 每单限用次数。 + /// + public int? PerOrderLimit { get; set; } + + /// + /// 每人限购张数。 + /// + public int? PerUserPurchaseLimit { get; set; } + + /// + /// 是否允许转赠。 + /// + public bool AllowTransfer { get; set; } + + /// + /// 过期策略。 + /// + public PunchCardExpireStrategy ExpireStrategy { get; set; } = PunchCardExpireStrategy.Invalidate; + + /// + /// 次卡描述。 + /// + public string? Description { get; set; } + + /// + /// 购买通知渠道 JSON。 + /// + public string NotifyChannelsJson { get; set; } = "[]"; + + /// + /// 次卡状态。 + /// + public PunchCardStatus Status { get; set; } = PunchCardStatus.Enabled; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardUsageRecord.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardUsageRecord.cs new file mode 100644 index 0000000..6875b0a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Entities/PunchCardUsageRecord.cs @@ -0,0 +1,60 @@ +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Coupons.Entities; + +/// +/// 次卡使用记录。 +/// +public sealed class PunchCardUsageRecord : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 次卡模板 ID。 + /// + public long PunchCardTemplateId { get; set; } + + /// + /// 次卡实例 ID。 + /// + public long PunchCardInstanceId { get; set; } + + /// + /// 使用单号。 + /// + public string RecordNo { get; set; } = string.Empty; + + /// + /// 兑换商品名称。 + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// 使用时间(UTC)。 + /// + public DateTime UsedAt { get; set; } + + /// + /// 本次使用次数。 + /// + public int UsedTimes { get; set; } = 1; + + /// + /// 使用后剩余次数。 + /// + public int RemainingTimesAfterUse { get; set; } + + /// + /// 本次记录状态。 + /// + public PunchCardUsageRecordStatus StatusAfterUse { get; set; } = PunchCardUsageRecordStatus.Normal; + + /// + /// 超额补差金额。 + /// + public decimal? ExtraPayAmount { get; set; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardExpireStrategy.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardExpireStrategy.cs new file mode 100644 index 0000000..459b21c --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardExpireStrategy.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 次卡过期策略。 +/// +public enum PunchCardExpireStrategy +{ + /// + /// 剩余次数作废。 + /// + Invalidate = 0, + + /// + /// 可申请退款。 + /// + Refund = 1 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardInstanceStatus.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardInstanceStatus.cs new file mode 100644 index 0000000..dda2ebf --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardInstanceStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 次卡实例状态。 +/// +public enum PunchCardInstanceStatus +{ + /// + /// 使用中。 + /// + Active = 0, + + /// + /// 已用完。 + /// + UsedUp = 1, + + /// + /// 已过期。 + /// + Expired = 2, + + /// + /// 已退款。 + /// + Refunded = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardScopeType.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardScopeType.cs new file mode 100644 index 0000000..833e687 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardScopeType.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 次卡适用范围类型。 +/// +public enum PunchCardScopeType +{ + /// + /// 全部商品。 + /// + All = 0, + + /// + /// 指定分类。 + /// + Category = 1, + + /// + /// 指定标签。 + /// + Tag = 2, + + /// + /// 指定商品。 + /// + Product = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardStatus.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardStatus.cs new file mode 100644 index 0000000..e6c0c1c --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardStatus.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 次卡状态。 +/// +public enum PunchCardStatus +{ + /// + /// 已下架。 + /// + Disabled = 0, + + /// + /// 已上架。 + /// + Enabled = 1 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardUsageMode.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardUsageMode.cs new file mode 100644 index 0000000..0ad0e4e --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardUsageMode.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 次卡使用模式。 +/// +public enum PunchCardUsageMode +{ + /// + /// 完全免费。 + /// + Free = 0, + + /// + /// 金额上限。 + /// + Cap = 1 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardUsageRecordStatus.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardUsageRecordStatus.cs new file mode 100644 index 0000000..775fe5f --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardUsageRecordStatus.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 次卡使用记录状态。 +/// +public enum PunchCardUsageRecordStatus +{ + /// + /// 正常使用。 + /// + Normal = 0, + + /// + /// 即将用完。 + /// + AlmostUsedUp = 1, + + /// + /// 已用完。 + /// + UsedUp = 2, + + /// + /// 已过期。 + /// + Expired = 3 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardValidityType.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardValidityType.cs new file mode 100644 index 0000000..a6bc3a7 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Enums/PunchCardValidityType.cs @@ -0,0 +1,17 @@ +namespace TakeoutSaaS.Domain.Coupons.Enums; + +/// +/// 次卡有效期类型。 +/// +public enum PunchCardValidityType +{ + /// + /// 购买后固定天数。 + /// + Days = 0, + + /// + /// 固定日期区间。 + /// + DateRange = 1 +} diff --git a/src/Domain/TakeoutSaaS.Domain/Coupons/Repositories/IPunchCardRepository.cs b/src/Domain/TakeoutSaaS.Domain/Coupons/Repositories/IPunchCardRepository.cs new file mode 100644 index 0000000..fcda2d3 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Coupons/Repositories/IPunchCardRepository.cs @@ -0,0 +1,247 @@ +using TakeoutSaaS.Domain.Coupons.Entities; +using TakeoutSaaS.Domain.Coupons.Enums; + +namespace TakeoutSaaS.Domain.Coupons.Repositories; + +/// +/// 次卡管理仓储契约。 +/// +public interface IPunchCardRepository +{ + /// + /// 查询次卡模板分页。 + /// + Task<(IReadOnlyList Items, int TotalCount)> SearchTemplatesAsync( + long tenantId, + long storeId, + string? keyword, + PunchCardStatus? status, + int page, + int pageSize, + CancellationToken cancellationToken = default); + + /// + /// 读取指定次卡模板。 + /// + Task FindTemplateByIdAsync( + long tenantId, + long storeId, + long templateId, + CancellationToken cancellationToken = default); + + /// + /// 按标识批量读取次卡模板。 + /// + Task> GetTemplatesByIdsAsync( + long tenantId, + long storeId, + IReadOnlyCollection templateIds, + CancellationToken cancellationToken = default); + + /// + /// 按模板批量统计售卖与在用信息。 + /// + Task> GetTemplateAggregateByTemplateIdsAsync( + long tenantId, + long storeId, + IReadOnlyCollection templateIds, + CancellationToken cancellationToken = default); + + /// + /// 查询页面统计。 + /// + Task GetTemplateStatsAsync( + long tenantId, + long storeId, + CancellationToken cancellationToken = default); + + /// + /// 新增次卡模板。 + /// + Task AddTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default); + + /// + /// 更新次卡模板。 + /// + Task UpdateTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default); + + /// + /// 删除次卡模板。 + /// + Task DeleteTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default); + + /// + /// 查询次卡实例。 + /// + Task FindInstanceByNoAsync( + long tenantId, + long storeId, + string instanceNo, + CancellationToken cancellationToken = default); + + /// + /// 查询次卡实例。 + /// + Task FindInstanceByIdAsync( + long tenantId, + long storeId, + long instanceId, + CancellationToken cancellationToken = default); + + /// + /// 按标识批量读取次卡实例。 + /// + Task> GetInstancesByIdsAsync( + long tenantId, + long storeId, + IReadOnlyCollection instanceIds, + CancellationToken cancellationToken = default); + + /// + /// 新增次卡实例。 + /// + Task AddInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default); + + /// + /// 更新次卡实例。 + /// + Task UpdateInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default); + + /// + /// 查询使用记录分页。 + /// + Task<(IReadOnlyList Items, int TotalCount)> SearchUsageRecordsAsync( + long tenantId, + long storeId, + long? templateId, + string? keyword, + PunchCardUsageRecordFilterStatus? status, + int page, + int pageSize, + CancellationToken cancellationToken = default); + + /// + /// 查询导出使用记录(不分页)。 + /// + Task> ListUsageRecordsForExportAsync( + long tenantId, + long storeId, + long? templateId, + string? keyword, + PunchCardUsageRecordFilterStatus? status, + CancellationToken cancellationToken = default); + + /// + /// 查询使用记录统计。 + /// + Task GetUsageStatsAsync( + long tenantId, + long storeId, + long? templateId, + DateTime nowUtc, + CancellationToken cancellationToken = default); + + /// + /// 新增使用记录。 + /// + Task AddUsageRecordAsync(PunchCardUsageRecord entity, CancellationToken cancellationToken = default); + + /// + /// 持久化变更。 + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} + +/// +/// 次卡模板聚合快照。 +/// +public sealed record PunchCardTemplateAggregateSnapshot +{ + /// + /// 次卡模板标识。 + /// + public required long TemplateId { get; init; } + + /// + /// 已售数量。 + /// + public int SoldCount { get; init; } + + /// + /// 在用数量。 + /// + public int ActiveCount { get; init; } + + /// + /// 累计收入。 + /// + public decimal RevenueAmount { get; init; } +} + +/// +/// 次卡模板统计快照。 +/// +public sealed record PunchCardTemplateStatsSnapshot +{ + /// + /// 在售数量。 + /// + public int OnSaleCount { get; init; } + + /// + /// 累计售出数量。 + /// + public int TotalSoldCount { get; init; } + + /// + /// 累计收入。 + /// + public decimal TotalRevenueAmount { get; init; } + + /// + /// 使用中数量。 + /// + public int ActiveInUseCount { get; init; } +} + +/// +/// 使用记录筛选状态。 +/// +public enum PunchCardUsageRecordFilterStatus +{ + /// + /// 正常(包含即将用完)。 + /// + Normal = 0, + + /// + /// 已用完。 + /// + UsedUp = 1, + + /// + /// 已过期。 + /// + Expired = 2 +} + +/// +/// 使用记录统计快照。 +/// +public sealed record PunchCardUsageStatsSnapshot +{ + /// + /// 今日使用次数。 + /// + public int TodayUsedCount { get; init; } + + /// + /// 本月使用次数。 + /// + public int MonthUsedCount { get; init; } + + /// + /// 7 天内即将过期数量。 + /// + public int ExpiringSoonCount { get; init; } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs index d6a3a4b..ea54078 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs @@ -48,6 +48,7 @@ public static class AppServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 41091f5..db1e16d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -370,6 +370,18 @@ public sealed class TakeoutAppDbContext( /// public DbSet NewCustomerGrowthRecords => Set(); /// + /// 次卡模板。 + /// + public DbSet PunchCardTemplates => Set(); + /// + /// 次卡实例。 + /// + public DbSet PunchCardInstances => Set(); + /// + /// 次卡使用记录。 + /// + public DbSet PunchCardUsageRecords => Set(); + /// /// 会员档案。 /// public DbSet MemberProfiles => Set(); @@ -540,6 +552,9 @@ public sealed class TakeoutAppDbContext( ConfigureNewCustomerCouponRule(modelBuilder.Entity()); ConfigureNewCustomerInviteRecord(modelBuilder.Entity()); ConfigureNewCustomerGrowthRecord(modelBuilder.Entity()); + ConfigurePunchCardTemplate(modelBuilder.Entity()); + ConfigurePunchCardInstance(modelBuilder.Entity()); + ConfigurePunchCardUsageRecord(modelBuilder.Entity()); ConfigureMemberProfile(modelBuilder.Entity()); ConfigureMemberTier(modelBuilder.Entity()); ConfigureMemberPointLedger(modelBuilder.Entity()); @@ -1692,6 +1707,77 @@ public sealed class TakeoutAppDbContext( builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RegisteredAt }); } + private static void ConfigurePunchCardTemplate(EntityTypeBuilder builder) + { + builder.ToTable("punch_card_templates"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); + builder.Property(x => x.CoverImageUrl).HasMaxLength(512); + builder.Property(x => x.SalePrice).HasPrecision(18, 2); + builder.Property(x => x.OriginalPrice).HasPrecision(18, 2); + builder.Property(x => x.TotalTimes).IsRequired(); + builder.Property(x => x.ValidityType).HasConversion(); + builder.Property(x => x.ValidityDays); + builder.Property(x => x.ValidFrom); + builder.Property(x => x.ValidTo); + builder.Property(x => x.ScopeType).HasConversion(); + builder.Property(x => x.ScopeCategoryIdsJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.ScopeTagIdsJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.ScopeProductIdsJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.UsageMode).HasConversion(); + builder.Property(x => x.UsageCapAmount).HasPrecision(18, 2); + builder.Property(x => x.DailyLimit); + builder.Property(x => x.PerOrderLimit); + builder.Property(x => x.PerUserPurchaseLimit); + builder.Property(x => x.AllowTransfer).IsRequired(); + builder.Property(x => x.ExpireStrategy).HasConversion(); + builder.Property(x => x.Description).HasMaxLength(512); + builder.Property(x => x.NotifyChannelsJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name }).IsUnique(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Status }); + } + + private static void ConfigurePunchCardInstance(EntityTypeBuilder builder) + { + builder.ToTable("punch_card_instances"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.PunchCardTemplateId).IsRequired(); + builder.Property(x => x.InstanceNo).HasMaxLength(32).IsRequired(); + builder.Property(x => x.MemberName).HasMaxLength(64).IsRequired(); + builder.Property(x => x.MemberPhoneMasked).HasMaxLength(32).IsRequired(); + builder.Property(x => x.PurchasedAt).IsRequired(); + builder.Property(x => x.ExpiresAt); + builder.Property(x => x.TotalTimes).IsRequired(); + builder.Property(x => x.RemainingTimes).IsRequired(); + builder.Property(x => x.PaidAmount).HasPrecision(18, 2); + builder.Property(x => x.Status).HasConversion(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.InstanceNo }).IsUnique(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PunchCardTemplateId }); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Status, x.ExpiresAt }); + } + + private static void ConfigurePunchCardUsageRecord(EntityTypeBuilder builder) + { + builder.ToTable("punch_card_usage_records"); + builder.HasKey(x => x.Id); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.PunchCardTemplateId).IsRequired(); + builder.Property(x => x.PunchCardInstanceId).IsRequired(); + builder.Property(x => x.RecordNo).HasMaxLength(32).IsRequired(); + builder.Property(x => x.ProductName).HasMaxLength(128).IsRequired(); + builder.Property(x => x.UsedAt).IsRequired(); + builder.Property(x => x.UsedTimes).IsRequired(); + builder.Property(x => x.RemainingTimesAfterUse).IsRequired(); + builder.Property(x => x.StatusAfterUse).HasConversion(); + builder.Property(x => x.ExtraPayAmount).HasPrecision(18, 2); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RecordNo }).IsUnique(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PunchCardTemplateId, x.UsedAt }); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PunchCardInstanceId, x.UsedAt }); + } + private static void ConfigureMemberProfile(EntityTypeBuilder builder) { builder.ToTable("member_profiles"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPunchCardRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPunchCardRepository.cs new file mode 100644 index 0000000..8735ff5 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPunchCardRepository.cs @@ -0,0 +1,469 @@ +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Coupons.Entities; +using TakeoutSaaS.Domain.Coupons.Enums; +using TakeoutSaaS.Domain.Coupons.Repositories; +using TakeoutSaaS.Infrastructure.App.Persistence; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 次卡仓储 EF Core 实现。 +/// +public sealed class EfPunchCardRepository(TakeoutAppDbContext context) : IPunchCardRepository +{ + /// + public async Task<(IReadOnlyList Items, int TotalCount)> SearchTemplatesAsync( + long tenantId, + long storeId, + string? keyword, + PunchCardStatus? status, + int page, + int pageSize, + CancellationToken cancellationToken = default) + { + var normalizedPage = Math.Max(1, page); + var normalizedPageSize = Math.Clamp(pageSize, 1, 200); + + var query = context.PunchCardTemplates + .AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId); + + if (status.HasValue) + { + query = query.Where(item => item.Status == status.Value); + } + + var normalizedKeyword = (keyword ?? string.Empty).Trim(); + if (!string.IsNullOrWhiteSpace(normalizedKeyword)) + { + var keywordLike = $"%{normalizedKeyword}%"; + query = query.Where(item => EF.Functions.ILike(item.Name, keywordLike)); + } + + var totalCount = await query.CountAsync(cancellationToken); + if (totalCount == 0) + { + return ([], 0); + } + + var items = await query + .OrderByDescending(item => item.UpdatedAt ?? item.CreatedAt) + .ThenByDescending(item => item.Id) + .Skip((normalizedPage - 1) * normalizedPageSize) + .Take(normalizedPageSize) + .ToListAsync(cancellationToken); + + return (items, totalCount); + } + + /// + public Task FindTemplateByIdAsync( + long tenantId, + long storeId, + long templateId, + CancellationToken cancellationToken = default) + { + return context.PunchCardTemplates + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + item.Id == templateId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> GetTemplatesByIdsAsync( + long tenantId, + long storeId, + IReadOnlyCollection templateIds, + CancellationToken cancellationToken = default) + { + if (templateIds.Count == 0) + { + return []; + } + + return await context.PunchCardTemplates + .AsNoTracking() + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + templateIds.Contains(item.Id)) + .ToListAsync(cancellationToken); + } + + /// + public async Task> GetTemplateAggregateByTemplateIdsAsync( + long tenantId, + long storeId, + IReadOnlyCollection templateIds, + CancellationToken cancellationToken = default) + { + if (templateIds.Count == 0) + { + return []; + } + + var nowUtc = DateTime.UtcNow; + var aggregates = await context.PunchCardInstances + .AsNoTracking() + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + templateIds.Contains(item.PunchCardTemplateId)) + .GroupBy(item => item.PunchCardTemplateId) + .Select(group => new PunchCardTemplateAggregateSnapshot + { + TemplateId = group.Key, + SoldCount = group.Count(), + ActiveCount = group.Count(item => + item.Status == PunchCardInstanceStatus.Active && + item.RemainingTimes > 0 && + (!item.ExpiresAt.HasValue || item.ExpiresAt.Value >= nowUtc)), + RevenueAmount = group.Sum(item => item.PaidAmount) + }) + .ToListAsync(cancellationToken); + + return aggregates.ToDictionary(item => item.TemplateId, item => item); + } + + /// + public async Task GetTemplateStatsAsync( + long tenantId, + long storeId, + CancellationToken cancellationToken = default) + { + var onSaleCount = await context.PunchCardTemplates + .AsNoTracking() + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + item.Status == PunchCardStatus.Enabled) + .CountAsync(cancellationToken); + + var summary = await context.PunchCardInstances + .AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId) + .GroupBy(_ => 1) + .Select(group => new + { + TotalSoldCount = group.Count(), + TotalRevenueAmount = group.Sum(item => item.PaidAmount) + }) + .FirstOrDefaultAsync(cancellationToken); + + var nowUtc = DateTime.UtcNow; + var activeInUseCount = await context.PunchCardInstances + .AsNoTracking() + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + item.Status == PunchCardInstanceStatus.Active && + item.RemainingTimes > 0 && + (!item.ExpiresAt.HasValue || item.ExpiresAt.Value >= nowUtc)) + .CountAsync(cancellationToken); + + return new PunchCardTemplateStatsSnapshot + { + OnSaleCount = onSaleCount, + TotalSoldCount = summary?.TotalSoldCount ?? 0, + TotalRevenueAmount = summary?.TotalRevenueAmount ?? 0m, + ActiveInUseCount = activeInUseCount + }; + } + + /// + public Task AddTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default) + { + return context.PunchCardTemplates.AddAsync(entity, cancellationToken).AsTask(); + } + + /// + public Task UpdateTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default) + { + context.PunchCardTemplates.Update(entity); + return Task.CompletedTask; + } + + /// + public Task DeleteTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default) + { + context.PunchCardTemplates.Remove(entity); + return Task.CompletedTask; + } + + /// + public Task FindInstanceByNoAsync( + long tenantId, + long storeId, + string instanceNo, + CancellationToken cancellationToken = default) + { + return context.PunchCardInstances + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + item.InstanceNo == instanceNo) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public Task FindInstanceByIdAsync( + long tenantId, + long storeId, + long instanceId, + CancellationToken cancellationToken = default) + { + return context.PunchCardInstances + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + item.Id == instanceId) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> GetInstancesByIdsAsync( + long tenantId, + long storeId, + IReadOnlyCollection instanceIds, + CancellationToken cancellationToken = default) + { + if (instanceIds.Count == 0) + { + return []; + } + + return await context.PunchCardInstances + .AsNoTracking() + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + instanceIds.Contains(item.Id)) + .ToListAsync(cancellationToken); + } + + /// + public Task AddInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default) + { + return context.PunchCardInstances.AddAsync(entity, cancellationToken).AsTask(); + } + + /// + public Task UpdateInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default) + { + context.PunchCardInstances.Update(entity); + return Task.CompletedTask; + } + + /// + public async Task<(IReadOnlyList Items, int TotalCount)> SearchUsageRecordsAsync( + long tenantId, + long storeId, + long? templateId, + string? keyword, + PunchCardUsageRecordFilterStatus? status, + int page, + int pageSize, + CancellationToken cancellationToken = default) + { + var normalizedPage = Math.Max(1, page); + var normalizedPageSize = Math.Clamp(pageSize, 1, 500); + var query = BuildUsageRecordQuery( + tenantId, + storeId, + templateId, + keyword, + status, + DateTime.UtcNow); + + var totalCount = await query.CountAsync(cancellationToken); + if (totalCount == 0) + { + return ([], 0); + } + + var items = await query + .OrderByDescending(item => item.UsedAt) + .ThenByDescending(item => item.Id) + .Skip((normalizedPage - 1) * normalizedPageSize) + .Take(normalizedPageSize) + .ToListAsync(cancellationToken); + + return (items, totalCount); + } + + /// + public async Task> ListUsageRecordsForExportAsync( + long tenantId, + long storeId, + long? templateId, + string? keyword, + PunchCardUsageRecordFilterStatus? status, + CancellationToken cancellationToken = default) + { + return await BuildUsageRecordQuery( + tenantId, + storeId, + templateId, + keyword, + status, + DateTime.UtcNow) + .OrderByDescending(item => item.UsedAt) + .ThenByDescending(item => item.Id) + .Take(20_000) + .ToListAsync(cancellationToken); + } + + /// + public async Task GetUsageStatsAsync( + long tenantId, + long storeId, + long? templateId, + DateTime nowUtc, + CancellationToken cancellationToken = default) + { + var utcNow = NormalizeUtc(nowUtc); + var todayStart = utcNow.Date; + var monthStart = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc); + var soonEnd = todayStart.AddDays(7); + + var usageQuery = context.PunchCardUsageRecords + .AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId); + + if (templateId.HasValue) + { + usageQuery = usageQuery.Where(item => item.PunchCardTemplateId == templateId.Value); + } + + var todayUsedCount = await usageQuery + .Where(item => item.UsedAt >= todayStart && item.UsedAt < todayStart.AddDays(1)) + .CountAsync(cancellationToken); + + var monthUsedCount = await usageQuery + .Where(item => item.UsedAt >= monthStart && item.UsedAt < monthStart.AddMonths(1)) + .CountAsync(cancellationToken); + + var instanceQuery = context.PunchCardInstances + .AsNoTracking() + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + item.Status == PunchCardInstanceStatus.Active && + item.RemainingTimes > 0 && + item.ExpiresAt.HasValue && + item.ExpiresAt.Value >= todayStart && + item.ExpiresAt.Value < soonEnd); + + if (templateId.HasValue) + { + instanceQuery = instanceQuery.Where(item => item.PunchCardTemplateId == templateId.Value); + } + + var expiringSoonCount = await instanceQuery.CountAsync(cancellationToken); + + return new PunchCardUsageStatsSnapshot + { + TodayUsedCount = todayUsedCount, + MonthUsedCount = monthUsedCount, + ExpiringSoonCount = expiringSoonCount + }; + } + + /// + public Task AddUsageRecordAsync(PunchCardUsageRecord entity, CancellationToken cancellationToken = default) + { + return context.PunchCardUsageRecords.AddAsync(entity, cancellationToken).AsTask(); + } + + /// + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return context.SaveChangesAsync(cancellationToken); + } + + private IQueryable BuildUsageRecordQuery( + long tenantId, + long storeId, + long? templateId, + string? keyword, + PunchCardUsageRecordFilterStatus? status, + DateTime nowUtc) + { + var query = context.PunchCardUsageRecords + .AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId); + + if (templateId.HasValue) + { + query = query.Where(item => item.PunchCardTemplateId == templateId.Value); + } + + var instanceQuery = context.PunchCardInstances + .AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId); + + if (templateId.HasValue) + { + instanceQuery = instanceQuery.Where(item => item.PunchCardTemplateId == templateId.Value); + } + + var utcNow = NormalizeUtc(nowUtc); + if (status.HasValue) + { + instanceQuery = status.Value switch + { + PunchCardUsageRecordFilterStatus.Normal => instanceQuery.Where(item => + item.Status == PunchCardInstanceStatus.Active && + item.RemainingTimes > 0 && + (!item.ExpiresAt.HasValue || item.ExpiresAt.Value >= utcNow)), + PunchCardUsageRecordFilterStatus.UsedUp => instanceQuery.Where(item => + item.Status == PunchCardInstanceStatus.UsedUp || + item.RemainingTimes <= 0), + PunchCardUsageRecordFilterStatus.Expired => instanceQuery.Where(item => + (item.Status == PunchCardInstanceStatus.Expired || + (item.ExpiresAt.HasValue && item.ExpiresAt.Value < utcNow)) && + item.Status != PunchCardInstanceStatus.Refunded), + _ => instanceQuery + }; + } + + if (!string.IsNullOrWhiteSpace(keyword)) + { + var normalizedKeyword = $"%{keyword.Trim()}%"; + var matchedInstanceIds = context.PunchCardInstances + .AsNoTracking() + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + (EF.Functions.ILike(item.MemberName, normalizedKeyword) || + EF.Functions.ILike(item.MemberPhoneMasked, normalizedKeyword) || + EF.Functions.ILike(item.InstanceNo, normalizedKeyword))) + .Select(item => item.Id); + + query = query.Where(item => + EF.Functions.ILike(item.RecordNo, normalizedKeyword) || + EF.Functions.ILike(item.ProductName, normalizedKeyword) || + matchedInstanceIds.Contains(item.PunchCardInstanceId)); + } + + if (status.HasValue) + { + var filteredInstanceIds = instanceQuery.Select(item => item.Id); + query = query.Where(item => filteredInstanceIds.Contains(item.PunchCardInstanceId)); + } + + return query; + } + + private static DateTime NormalizeUtc(DateTime value) + { + return value.Kind switch + { + DateTimeKind.Utc => value, + DateTimeKind.Local => value.ToUniversalTime(), + _ => DateTime.SpecifyKind(value, DateTimeKind.Utc) + }; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.Designer.cs new file mode 100644 index 0000000..8fd09dd --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.Designer.cs @@ -0,0 +1,9547 @@ +// +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("20260302125930_AddPunchCardModule")] + partial class AddPunchCardModule + { + /// + 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("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + 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("PerUserLimit") + .HasColumnType("integer") + .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.NewCustomerCouponRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("MinimumSpend") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("使用门槛。"); + + b.Property("Scene") + .HasColumnType("integer") + .HasComment("券规则场景。"); + + b.Property("SortOrder") + .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.Property("ValidDays") + .HasColumnType("integer") + .HasComment("有效期天数。"); + + b.Property("Value") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("面值或折扣值。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Scene", "SortOrder"); + + b.ToTable("new_customer_coupon_rules", null, t => + { + t.HasComment("新客有礼券规则。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerGiftSetting", 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("DirectMinimumSpend") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("首单直减门槛金额。"); + + b.Property("DirectReduceAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("首单直减金额。"); + + b.Property("GiftEnabled") + .HasColumnType("boolean") + .HasComment("是否开启新客礼包。"); + + b.Property("GiftType") + .HasColumnType("integer") + .HasComment("礼包类型。"); + + b.Property("InviteEnabled") + .HasColumnType("boolean") + .HasComment("是否开启老带新分享。"); + + b.Property("ShareChannelsJson") + .IsRequired() + .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("new_customer_gift_settings", null, t => + { + t.HasComment("新客有礼门店配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerGrowthRecord", 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("CustomerKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("顾客业务唯一键。"); + + b.Property("CustomerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("顾客展示名。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("FirstOrderAt") + .HasColumnType("timestamp with time zone") + .HasComment("首单时间。"); + + b.Property("GiftClaimedAt") + .HasColumnType("timestamp with time zone") + .HasComment("礼包领取时间。"); + + b.Property("RegisteredAt") + .HasColumnType("timestamp with time zone") + .HasComment("注册时间。"); + + b.Property("SourceChannel") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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", "CustomerKey") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "RegisteredAt"); + + b.ToTable("new_customer_growth_records", null, t => + { + t.HasComment("新客成长记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerInviteRecord", 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("InviteTime") + .HasColumnType("timestamp with time zone") + .HasComment("邀请时间。"); + + b.Property("InviteeName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("被邀请人展示名。"); + + b.Property("InviterName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("邀请人展示名。"); + + b.Property("OrderStatus") + .HasColumnType("integer") + .HasComment("订单状态。"); + + b.Property("RewardIssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("奖励发放时间。"); + + b.Property("RewardStatus") + .HasColumnType("integer") + .HasComment("奖励状态。"); + + b.Property("SourceChannel") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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", "InviteTime"); + + b.ToTable("new_customer_invite_records", 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.Coupons.Entities.PunchCardInstance", 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("InstanceNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("实例编号(业务唯一)。"); + + b.Property("MemberName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会员名称。"); + + b.Property("MemberPhoneMasked") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("会员手机号(脱敏)。"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("PunchCardTemplateId") + .HasColumnType("bigint") + .HasComment("次卡模板 ID。"); + + b.Property("PurchasedAt") + .HasColumnType("timestamp with time zone") + .HasComment("购买时间(UTC)。"); + + b.Property("RemainingTimes") + .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("TotalTimes") + .HasColumnType("integer") + .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", "InstanceNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "PunchCardTemplateId"); + + b.HasIndex("TenantId", "StoreId", "Status", "ExpiresAt"); + + b.ToTable("punch_card_instances", null, t => + { + t.HasComment("次卡实例(顾客购买后生成)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowTransfer") + .HasColumnType("boolean") + .HasComment("是否允许转赠。"); + + b.Property("CoverImageUrl") + .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("DailyLimit") + .HasColumnType("integer") + .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("ExpireStrategy") + .HasColumnType("integer") + .HasComment("过期策略。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("次卡名称。"); + + b.Property("NotifyChannelsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("购买通知渠道 JSON。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("PerOrderLimit") + .HasColumnType("integer") + .HasComment("每单限用次数。"); + + b.Property("PerUserPurchaseLimit") + .HasColumnType("integer") + .HasComment("每人限购张数。"); + + b.Property("SalePrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("售价。"); + + b.Property("ScopeCategoryIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定分类 ID JSON。"); + + b.Property("ScopeProductIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定商品 ID JSON。"); + + b.Property("ScopeTagIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定标签 ID JSON。"); + + b.Property("ScopeType") + .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("TotalTimes") + .HasColumnType("integer") + .HasComment("总次数。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsageCapAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("金额上限(UsageMode=Cap 时有效)。"); + + b.Property("UsageMode") + .HasColumnType("integer") + .HasComment("使用模式。"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasComment("固定开始日期(UTC,ValidityType=DateRange 时有效)。"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone") + .HasComment("固定结束日期(UTC,ValidityType=DateRange 时有效)。"); + + b.Property("ValidityDays") + .HasColumnType("integer") + .HasComment("固定天数(ValidityType=Days 时有效)。"); + + b.Property("ValidityType") + .HasColumnType("integer") + .HasComment("有效期类型。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("punch_card_templates", null, t => + { + t.HasComment("次卡模板配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardUsageRecord", 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("ExtraPayAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("超额补差金额。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("兑换商品名称。"); + + b.Property("PunchCardInstanceId") + .HasColumnType("bigint") + .HasComment("次卡实例 ID。"); + + b.Property("PunchCardTemplateId") + .HasColumnType("bigint") + .HasComment("次卡模板 ID。"); + + b.Property("RecordNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("使用单号。"); + + b.Property("RemainingTimesAfterUse") + .HasColumnType("integer") + .HasComment("使用后剩余次数。"); + + b.Property("StatusAfterUse") + .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.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasComment("使用时间(UTC)。"); + + b.Property("UsedTimes") + .HasColumnType("integer") + .HasComment("本次使用次数。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "RecordNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "PunchCardInstanceId", "UsedAt"); + + b.HasIndex("TenantId", "StoreId", "PunchCardTemplateId", "UsedAt"); + + b.ToTable("punch_card_usage_records", 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("GeoFailReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("地理定位失败原因。"); + + b.Property("GeoNextRetryAt") + .HasColumnType("timestamp with time zone") + .HasComment("下次地理定位重试时间(UTC)。"); + + b.Property("GeoRetryCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位重试次数。"); + + b.Property("GeoStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位状态。"); + + b.Property("GeoUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("地理定位最近成功时间(UTC)。"); + + 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("Longitude", "Latitude") + .HasFilter("\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL"); + + b.HasIndex("TenantId", "Status"); + + b.HasIndex("TenantId", "GeoStatus", "GeoNextRetryAt"); + + 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("Kind") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("商品类型。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("最大每单限购。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); + + b.Property("NotifyManager") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否通知店长。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("PackingFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("打包费(元/份)。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("现价。"); + + b.Property("RecoverAt") + .HasColumnType("timestamp with time zone") + .HasComment("沽清恢复时间。"); + + b.Property("RemainStock") + .HasColumnType("integer") + .HasComment("沽清后剩余可售。"); + + b.Property("SalesMonthly") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("月销量。"); + + b.Property("SoldoutMode") + .HasColumnType("integer") + .HasComment("沽清模式。"); + + b.Property("SoldoutReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("沽清原因。"); + + b.Property("SortWeight") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .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("SyncToPlatform") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否同步通知外卖平台。"); + + b.Property("TagsJson") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]") + .HasComment("标签 JSON(字符串数组)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TimedOnShelfAt") + .HasColumnType("timestamp with time zone") + .HasComment("定时上架时间。"); + + 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.Property("WarningStock") + .HasColumnType("integer") + .HasComment("库存预警值。"); + + 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("ChannelsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[\"wm\"]") + .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("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("分类描述。"); + + b.Property("Icon") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .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.ProductComboGroup", 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("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("套餐商品 ID。"); + + 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.HasIndex("TenantId", "ProductId", "SortOrder"); + + b.ToTable("product_combo_groups", null, t => + { + t.HasComment("套餐分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductComboGroupItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ComboGroupId") + .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("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("Quantity") + .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", "ComboGroupId", "ProductId") + .IsUnique(); + + b.HasIndex("TenantId", "ComboGroupId", "SortOrder"); + + b.ToTable("product_combo_group_items", null, t => + { + t.HasComment("套餐分组内商品。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductLabel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Color") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("标签颜色(HEX)。"); + + 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("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", "Name") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "IsEnabled", "SortOrder"); + + b.ToTable("product_labels", null, t => + { + t.HasComment("商品标签模板(门店维度)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductLabelProduct", 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("LabelId") + .HasColumnType("bigint") + .HasComment("标签 ID。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + 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", "ProductId"); + + b.HasIndex("TenantId", "StoreId", "LabelId", "ProductId") + .IsUnique(); + + b.ToTable("product_label_products", 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.ProductSchedule", 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("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .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.Property("WeekDaysMask") + .HasColumnType("integer") + .HasComment("星期位掩码(周一到周日)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "IsEnabled"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.ToTable("product_schedules", null, t => + { + t.HasComment("商品时段规则(门店维度)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductScheduleProduct", 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("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("ScheduleId") + .HasColumnType("bigint") + .HasComment("时段规则 ID。"); + + 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", "ProductId"); + + b.HasIndex("TenantId", "StoreId", "ScheduleId", "ProductId") + .IsUnique(); + + b.ToTable("product_schedule_products", 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("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .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("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", "ProductId"); + + b.HasIndex("TenantId", "SkuCode") + .IsUnique(); + + b.ToTable("product_skus", null, t => + { + t.HasComment("商品 SKU,记录具体规格组合价格。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSkuSaveJob", 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("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasComment("失败摘要。"); + + b.Property("FailedCount") + .HasColumnType("integer") + .HasComment("失败条数。"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间(UTC)。"); + + b.Property("HangfireJobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("Hangfire 任务 ID。"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("任务模式(当前固定 replace)。"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("text") + .HasComment("任务请求负载 JSON 快照。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品 ID。"); + + b.Property("ProgressProcessed") + .HasColumnType("integer") + .HasComment("已处理数。"); + + b.Property("ProgressTotal") + .HasColumnType("integer") + .HasComment("总处理数。"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始执行时间(UTC)。"); + + 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", "ProductId", "CreatedAt"); + + b.HasIndex("TenantId", "Status", "CreatedAt"); + + b.ToTable("product_sku_save_jobs", null, t => + { + t.HasComment("商品 SKU 异步保存任务。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSpecTemplate", 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") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("模板描述。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + 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("SelectionType") + .HasColumnType("integer") + .HasComment("选择方式。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("所属门店。"); + + b.Property("TemplateType") + .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", "StoreId", "TemplateType", "IsEnabled"); + + b.HasIndex("TenantId", "StoreId", "TemplateType", "Name") + .IsUnique(); + + b.ToTable("product_spec_templates", null, t => + { + t.HasComment("门店规格做法模板。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSpecTemplateOption", 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("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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("Stock") + .HasColumnType("integer") + .HasComment("库存数量。"); + + b.Property("TemplateId") + .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", "TemplateId", "Name") + .IsUnique(); + + b.ToTable("product_spec_template_options", null, t => + { + t.HasComment("规格做法模板选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSpecTemplateProduct", 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("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("所属门店。"); + + b.Property("TemplateId") + .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", "ProductId"); + + b.HasIndex("TenantId", "StoreId", "TemplateId", "ProductId") + .IsUnique(); + + b.ToTable("product_spec_template_products", null, t => + { + t.HasComment("规格做法模板与商品关联。"); + }); + }); + + 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("GeoFailReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("地理定位失败原因。"); + + b.Property("GeoNextRetryAt") + .HasColumnType("timestamp with time zone") + .HasComment("下次地理定位重试时间(UTC)。"); + + b.Property("GeoRetryCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位重试次数。"); + + b.Property("GeoStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位状态。"); + + b.Property("GeoUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("地理定位最近成功时间(UTC)。"); + + 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.HasIndex("TenantId", "MerchantId", "GeoStatus", "GeoNextRetryAt"); + + 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("RadiusCenterLatitude") + .HasPrecision(10, 7) + .HasColumnType("numeric(10,7)") + .HasComment("半径配送中心点纬度。"); + + b.Property("RadiusCenterLongitude") + .HasPrecision(10, 7) + .HasColumnType("numeric(10,7)") + .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() + .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() + .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("GeoFailReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("地理定位失败原因。"); + + b.Property("GeoNextRetryAt") + .HasColumnType("timestamp with time zone") + .HasComment("下次地理定位重试时间(UTC)。"); + + b.Property("GeoRetryCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位重试次数。"); + + b.Property("GeoStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位状态。"); + + b.Property("GeoUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("地理定位最近成功时间(UTC)。"); + + b.Property("Industry") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所属行业,如餐饮、零售等。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度信息。"); + + b.Property("LegalEntityName") + .HasColumnType("text") + .HasComment("法人或公司主体名称。"); + + b.Property("LogoUrl") + .HasColumnType("text") + .HasComment("LOGO 图片地址。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度信息。"); + + 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.HasIndex("GeoStatus", "GeoNextRetryAt"); + + b.HasIndex("Longitude", "Latitude") + .HasFilter("\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL"); + + 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("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + 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/20260302125930_AddPunchCardModule.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.cs new file mode 100644 index 0000000..894c6b4 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.cs @@ -0,0 +1,177 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations +{ + /// + public partial class AddPunchCardModule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "punch_card_instances", + 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。"), + PunchCardTemplateId = table.Column(type: "bigint", nullable: false, comment: "次卡模板 ID。"), + InstanceNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "实例编号(业务唯一)。"), + MemberName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "会员名称。"), + MemberPhoneMasked = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "会员手机号(脱敏)。"), + PurchasedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "购买时间(UTC)。"), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "过期时间(UTC,可空)。"), + TotalTimes = table.Column(type: "integer", nullable: false, comment: "总次数。"), + RemainingTimes = table.Column(type: "integer", nullable: false, comment: "剩余次数。"), + PaidAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "实付金额。"), + Status = table.Column(type: "integer", 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_punch_card_instances", x => x.Id); + }, + comment: "次卡实例(顾客购买后生成)。"); + + migrationBuilder.CreateTable( + name: "punch_card_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。"), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "次卡名称。"), + CoverImageUrl = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "封面图片地址。"), + SalePrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "售价。"), + OriginalPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "原价。"), + TotalTimes = table.Column(type: "integer", nullable: false, comment: "总次数。"), + ValidityType = table.Column(type: "integer", nullable: false, comment: "有效期类型。"), + ValidityDays = table.Column(type: "integer", nullable: true, comment: "固定天数(ValidityType=Days 时有效)。"), + ValidFrom = table.Column(type: "timestamp with time zone", nullable: true, comment: "固定开始日期(UTC,ValidityType=DateRange 时有效)。"), + ValidTo = table.Column(type: "timestamp with time zone", nullable: true, comment: "固定结束日期(UTC,ValidityType=DateRange 时有效)。"), + ScopeType = table.Column(type: "integer", nullable: false, comment: "适用范围类型。"), + ScopeCategoryIdsJson = table.Column(type: "text", nullable: false, comment: "指定分类 ID JSON。"), + ScopeTagIdsJson = table.Column(type: "text", nullable: false, comment: "指定标签 ID JSON。"), + ScopeProductIdsJson = table.Column(type: "text", nullable: false, comment: "指定商品 ID JSON。"), + UsageMode = table.Column(type: "integer", nullable: false, comment: "使用模式。"), + UsageCapAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "金额上限(UsageMode=Cap 时有效)。"), + DailyLimit = table.Column(type: "integer", nullable: true, comment: "每日限用次数。"), + PerOrderLimit = table.Column(type: "integer", nullable: true, comment: "每单限用次数。"), + PerUserPurchaseLimit = table.Column(type: "integer", nullable: true, comment: "每人限购张数。"), + AllowTransfer = table.Column(type: "boolean", nullable: false, comment: "是否允许转赠。"), + ExpireStrategy = table.Column(type: "integer", nullable: false, comment: "过期策略。"), + Description = table.Column(type: "character varying(512)", maxLength: 512, nullable: true, comment: "次卡描述。"), + NotifyChannelsJson = table.Column(type: "text", nullable: false, comment: "购买通知渠道 JSON。"), + Status = table.Column(type: "integer", 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_punch_card_templates", x => x.Id); + }, + comment: "次卡模板配置。"); + + migrationBuilder.CreateTable( + name: "punch_card_usage_records", + 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。"), + PunchCardTemplateId = table.Column(type: "bigint", nullable: false, comment: "次卡模板 ID。"), + PunchCardInstanceId = table.Column(type: "bigint", nullable: false, comment: "次卡实例 ID。"), + RecordNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "使用单号。"), + ProductName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "兑换商品名称。"), + UsedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "使用时间(UTC)。"), + UsedTimes = table.Column(type: "integer", nullable: false, comment: "本次使用次数。"), + RemainingTimesAfterUse = table.Column(type: "integer", nullable: false, comment: "使用后剩余次数。"), + StatusAfterUse = table.Column(type: "integer", nullable: false, comment: "本次记录状态。"), + ExtraPayAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, 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_punch_card_usage_records", x => x.Id); + }, + comment: "次卡使用记录。"); + + migrationBuilder.CreateIndex( + name: "IX_punch_card_instances_TenantId_StoreId_InstanceNo", + table: "punch_card_instances", + columns: new[] { "TenantId", "StoreId", "InstanceNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_punch_card_instances_TenantId_StoreId_PunchCardTemplateId", + table: "punch_card_instances", + columns: new[] { "TenantId", "StoreId", "PunchCardTemplateId" }); + + migrationBuilder.CreateIndex( + name: "IX_punch_card_instances_TenantId_StoreId_Status_ExpiresAt", + table: "punch_card_instances", + columns: new[] { "TenantId", "StoreId", "Status", "ExpiresAt" }); + + migrationBuilder.CreateIndex( + name: "IX_punch_card_templates_TenantId_StoreId_Name", + table: "punch_card_templates", + columns: new[] { "TenantId", "StoreId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_punch_card_templates_TenantId_StoreId_Status", + table: "punch_card_templates", + columns: new[] { "TenantId", "StoreId", "Status" }); + + migrationBuilder.CreateIndex( + name: "IX_punch_card_usage_records_TenantId_StoreId_PunchCardInstance~", + table: "punch_card_usage_records", + columns: new[] { "TenantId", "StoreId", "PunchCardInstanceId", "UsedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_punch_card_usage_records_TenantId_StoreId_PunchCardTemplate~", + table: "punch_card_usage_records", + columns: new[] { "TenantId", "StoreId", "PunchCardTemplateId", "UsedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_punch_card_usage_records_TenantId_StoreId_RecordNo", + table: "punch_card_usage_records", + columns: new[] { "TenantId", "StoreId", "RecordNo" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "punch_card_instances"); + + migrationBuilder.DropTable( + name: "punch_card_templates"); + + migrationBuilder.DropTable( + name: "punch_card_usage_records"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs index 46da4ca..73c8b81 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/TakeoutAppDbContextModelSnapshot.cs @@ -1012,6 +1012,363 @@ namespace TakeoutSaaS.Infrastructure.Migrations }); }); + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardInstance", 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("InstanceNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("实例编号(业务唯一)。"); + + b.Property("MemberName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会员名称。"); + + b.Property("MemberPhoneMasked") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("会员手机号(脱敏)。"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("PunchCardTemplateId") + .HasColumnType("bigint") + .HasComment("次卡模板 ID。"); + + b.Property("PurchasedAt") + .HasColumnType("timestamp with time zone") + .HasComment("购买时间(UTC)。"); + + b.Property("RemainingTimes") + .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("TotalTimes") + .HasColumnType("integer") + .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", "InstanceNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "PunchCardTemplateId"); + + b.HasIndex("TenantId", "StoreId", "Status", "ExpiresAt"); + + b.ToTable("punch_card_instances", null, t => + { + t.HasComment("次卡实例(顾客购买后生成)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowTransfer") + .HasColumnType("boolean") + .HasComment("是否允许转赠。"); + + b.Property("CoverImageUrl") + .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("DailyLimit") + .HasColumnType("integer") + .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("ExpireStrategy") + .HasColumnType("integer") + .HasComment("过期策略。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("次卡名称。"); + + b.Property("NotifyChannelsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("购买通知渠道 JSON。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("PerOrderLimit") + .HasColumnType("integer") + .HasComment("每单限用次数。"); + + b.Property("PerUserPurchaseLimit") + .HasColumnType("integer") + .HasComment("每人限购张数。"); + + b.Property("SalePrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("售价。"); + + b.Property("ScopeCategoryIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定分类 ID JSON。"); + + b.Property("ScopeProductIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定商品 ID JSON。"); + + b.Property("ScopeTagIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定标签 ID JSON。"); + + b.Property("ScopeType") + .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("TotalTimes") + .HasColumnType("integer") + .HasComment("总次数。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsageCapAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("金额上限(UsageMode=Cap 时有效)。"); + + b.Property("UsageMode") + .HasColumnType("integer") + .HasComment("使用模式。"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasComment("固定开始日期(UTC,ValidityType=DateRange 时有效)。"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone") + .HasComment("固定结束日期(UTC,ValidityType=DateRange 时有效)。"); + + b.Property("ValidityDays") + .HasColumnType("integer") + .HasComment("固定天数(ValidityType=Days 时有效)。"); + + b.Property("ValidityType") + .HasColumnType("integer") + .HasComment("有效期类型。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("punch_card_templates", null, t => + { + t.HasComment("次卡模板配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardUsageRecord", 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("ExtraPayAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("超额补差金额。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("兑换商品名称。"); + + b.Property("PunchCardInstanceId") + .HasColumnType("bigint") + .HasComment("次卡实例 ID。"); + + b.Property("PunchCardTemplateId") + .HasColumnType("bigint") + .HasComment("次卡模板 ID。"); + + b.Property("RecordNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("使用单号。"); + + b.Property("RemainingTimesAfterUse") + .HasColumnType("integer") + .HasComment("使用后剩余次数。"); + + b.Property("StatusAfterUse") + .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.Property("UsedAt") + .HasColumnType("timestamp with time zone") + .HasComment("使用时间(UTC)。"); + + b.Property("UsedTimes") + .HasColumnType("integer") + .HasComment("本次使用次数。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "RecordNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "PunchCardInstanceId", "UsedAt"); + + b.HasIndex("TenantId", "StoreId", "PunchCardTemplateId", "UsedAt"); + + b.ToTable("punch_card_usage_records", null, t => + { + t.HasComment("次卡使用记录。"); + }); + }); + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => { b.Property("Id")