feat: implement marketing punch card backend module

This commit is contained in:
2026-03-02 21:43:09 +08:00
parent 6588c85f27
commit 3b3bdcee71
48 changed files with 14863 additions and 1 deletions

View File

@@ -370,6 +370,18 @@ public sealed class TakeoutAppDbContext(
/// </summary>
public DbSet<NewCustomerGrowthRecord> NewCustomerGrowthRecords => Set<NewCustomerGrowthRecord>();
/// <summary>
/// 次卡模板。
/// </summary>
public DbSet<PunchCardTemplate> PunchCardTemplates => Set<PunchCardTemplate>();
/// <summary>
/// 次卡实例。
/// </summary>
public DbSet<PunchCardInstance> PunchCardInstances => Set<PunchCardInstance>();
/// <summary>
/// 次卡使用记录。
/// </summary>
public DbSet<PunchCardUsageRecord> PunchCardUsageRecords => Set<PunchCardUsageRecord>();
/// <summary>
/// 会员档案。
/// </summary>
public DbSet<MemberProfile> MemberProfiles => Set<MemberProfile>();
@@ -540,6 +552,9 @@ public sealed class TakeoutAppDbContext(
ConfigureNewCustomerCouponRule(modelBuilder.Entity<NewCustomerCouponRule>());
ConfigureNewCustomerInviteRecord(modelBuilder.Entity<NewCustomerInviteRecord>());
ConfigureNewCustomerGrowthRecord(modelBuilder.Entity<NewCustomerGrowthRecord>());
ConfigurePunchCardTemplate(modelBuilder.Entity<PunchCardTemplate>());
ConfigurePunchCardInstance(modelBuilder.Entity<PunchCardInstance>());
ConfigurePunchCardUsageRecord(modelBuilder.Entity<PunchCardUsageRecord>());
ConfigureMemberProfile(modelBuilder.Entity<MemberProfile>());
ConfigureMemberTier(modelBuilder.Entity<MemberTier>());
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
@@ -1692,6 +1707,77 @@ public sealed class TakeoutAppDbContext(
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RegisteredAt });
}
private static void ConfigurePunchCardTemplate(EntityTypeBuilder<PunchCardTemplate> 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<int>();
builder.Property(x => x.ValidityDays);
builder.Property(x => x.ValidFrom);
builder.Property(x => x.ValidTo);
builder.Property(x => x.ScopeType).HasConversion<int>();
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<int>();
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<int>();
builder.Property(x => x.Description).HasMaxLength(512);
builder.Property(x => x.NotifyChannelsJson).HasColumnType("text").IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
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<PunchCardInstance> 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<int>();
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<PunchCardUsageRecord> 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<int>();
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<MemberProfile> builder)
{
builder.ToTable("member_profiles");