feat: implement marketing punch card backend module
This commit is contained in:
@@ -48,6 +48,7 @@ public static class AppServiceCollectionExtensions
|
||||
services.AddScoped<ICouponRepository, EfCouponRepository>();
|
||||
services.AddScoped<INewCustomerGiftRepository, EfNewCustomerGiftRepository>();
|
||||
services.AddScoped<IPromotionCampaignRepository, EfPromotionCampaignRepository>();
|
||||
services.AddScoped<IPunchCardRepository, EfPunchCardRepository>();
|
||||
services.AddScoped<IOrderRepository, EfOrderRepository>();
|
||||
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
|
||||
services.AddScoped<IDeliveryRepository, EfDeliveryRepository>();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡仓储 EF Core 实现。
|
||||
/// </summary>
|
||||
public sealed class EfPunchCardRepository(TakeoutAppDbContext context) : IPunchCardRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<(IReadOnlyList<PunchCardTemplate> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PunchCardTemplate?> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PunchCardTemplate>> GetTemplatesByIdsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
IReadOnlyCollection<long> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<long, PunchCardTemplateAggregateSnapshot>> GetTemplateAggregateByTemplateIdsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
IReadOnlyCollection<long> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PunchCardTemplateStatsSnapshot> 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.PunchCardTemplates.AddAsync(entity, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.PunchCardTemplates.Update(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task DeleteTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.PunchCardTemplates.Remove(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PunchCardInstance?> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PunchCardInstance?> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PunchCardInstance>> GetInstancesByIdsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
IReadOnlyCollection<long> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.PunchCardInstances.AddAsync(entity, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.PunchCardInstances.Update(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(IReadOnlyList<PunchCardUsageRecord> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PunchCardUsageRecord>> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PunchCardUsageStatsSnapshot> 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddUsageRecordAsync(PunchCardUsageRecord entity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.PunchCardUsageRecords.AddAsync(entity, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private IQueryable<PunchCardUsageRecord> 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
9547
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.Designer.cs
generated
Normal file
9547
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPunchCardModule : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "punch_card_instances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||
PunchCardTemplateId = table.Column<long>(type: "bigint", nullable: false, comment: "次卡模板 ID。"),
|
||||
InstanceNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "实例编号(业务唯一)。"),
|
||||
MemberName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "会员名称。"),
|
||||
MemberPhoneMasked = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "会员手机号(脱敏)。"),
|
||||
PurchasedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "购买时间(UTC)。"),
|
||||
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "过期时间(UTC,可空)。"),
|
||||
TotalTimes = table.Column<int>(type: "integer", nullable: false, comment: "总次数。"),
|
||||
RemainingTimes = table.Column<int>(type: "integer", nullable: false, comment: "剩余次数。"),
|
||||
PaidAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "实付金额。"),
|
||||
Status = table.Column<int>(type: "integer", nullable: false, comment: "实例状态。"),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_punch_card_instances", x => x.Id);
|
||||
},
|
||||
comment: "次卡实例(顾客购买后生成)。");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "punch_card_templates",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "次卡名称。"),
|
||||
CoverImageUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true, comment: "封面图片地址。"),
|
||||
SalePrice = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "售价。"),
|
||||
OriginalPrice = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "原价。"),
|
||||
TotalTimes = table.Column<int>(type: "integer", nullable: false, comment: "总次数。"),
|
||||
ValidityType = table.Column<int>(type: "integer", nullable: false, comment: "有效期类型。"),
|
||||
ValidityDays = table.Column<int>(type: "integer", nullable: true, comment: "固定天数(ValidityType=Days 时有效)。"),
|
||||
ValidFrom = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "固定开始日期(UTC,ValidityType=DateRange 时有效)。"),
|
||||
ValidTo = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "固定结束日期(UTC,ValidityType=DateRange 时有效)。"),
|
||||
ScopeType = table.Column<int>(type: "integer", nullable: false, comment: "适用范围类型。"),
|
||||
ScopeCategoryIdsJson = table.Column<string>(type: "text", nullable: false, comment: "指定分类 ID JSON。"),
|
||||
ScopeTagIdsJson = table.Column<string>(type: "text", nullable: false, comment: "指定标签 ID JSON。"),
|
||||
ScopeProductIdsJson = table.Column<string>(type: "text", nullable: false, comment: "指定商品 ID JSON。"),
|
||||
UsageMode = table.Column<int>(type: "integer", nullable: false, comment: "使用模式。"),
|
||||
UsageCapAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "金额上限(UsageMode=Cap 时有效)。"),
|
||||
DailyLimit = table.Column<int>(type: "integer", nullable: true, comment: "每日限用次数。"),
|
||||
PerOrderLimit = table.Column<int>(type: "integer", nullable: true, comment: "每单限用次数。"),
|
||||
PerUserPurchaseLimit = table.Column<int>(type: "integer", nullable: true, comment: "每人限购张数。"),
|
||||
AllowTransfer = table.Column<bool>(type: "boolean", nullable: false, comment: "是否允许转赠。"),
|
||||
ExpireStrategy = table.Column<int>(type: "integer", nullable: false, comment: "过期策略。"),
|
||||
Description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true, comment: "次卡描述。"),
|
||||
NotifyChannelsJson = table.Column<string>(type: "text", nullable: false, comment: "购买通知渠道 JSON。"),
|
||||
Status = table.Column<int>(type: "integer", nullable: false, comment: "次卡状态。"),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_punch_card_templates", x => x.Id);
|
||||
},
|
||||
comment: "次卡模板配置。");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "punch_card_usage_records",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||
PunchCardTemplateId = table.Column<long>(type: "bigint", nullable: false, comment: "次卡模板 ID。"),
|
||||
PunchCardInstanceId = table.Column<long>(type: "bigint", nullable: false, comment: "次卡实例 ID。"),
|
||||
RecordNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "使用单号。"),
|
||||
ProductName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false, comment: "兑换商品名称。"),
|
||||
UsedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "使用时间(UTC)。"),
|
||||
UsedTimes = table.Column<int>(type: "integer", nullable: false, comment: "本次使用次数。"),
|
||||
RemainingTimesAfterUse = table.Column<int>(type: "integer", nullable: false, comment: "使用后剩余次数。"),
|
||||
StatusAfterUse = table.Column<int>(type: "integer", nullable: false, comment: "本次记录状态。"),
|
||||
ExtraPayAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "超额补差金额。"),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1012,6 +1012,363 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardInstance", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("实体唯一标识。");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("创建时间(UTC)。");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||
|
||||
b.Property<DateTime?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("过期时间(UTC,可空)。");
|
||||
|
||||
b.Property<string>("InstanceNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasComment("实例编号(业务唯一)。");
|
||||
|
||||
b.Property<string>("MemberName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasComment("会员名称。");
|
||||
|
||||
b.Property<string>("MemberPhoneMasked")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasComment("会员手机号(脱敏)。");
|
||||
|
||||
b.Property<decimal>("PaidAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)")
|
||||
.HasComment("实付金额。");
|
||||
|
||||
b.Property<long>("PunchCardTemplateId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("次卡模板 ID。");
|
||||
|
||||
b.Property<DateTime>("PurchasedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("购买时间(UTC)。");
|
||||
|
||||
b.Property<int>("RemainingTimes")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("剩余次数。");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("实例状态。");
|
||||
|
||||
b.Property<long>("StoreId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("门店 ID。");
|
||||
|
||||
b.Property<long>("TenantId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("所属租户 ID。");
|
||||
|
||||
b.Property<int>("TotalTimes")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("总次数。");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantId", "StoreId", "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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("实体唯一标识。");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<bool>("AllowTransfer")
|
||||
.HasColumnType("boolean")
|
||||
.HasComment("是否允许转赠。");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasComment("封面图片地址。");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("创建时间(UTC)。");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||
|
||||
b.Property<int?>("DailyLimit")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("每日限用次数。");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)")
|
||||
.HasComment("次卡描述。");
|
||||
|
||||
b.Property<int>("ExpireStrategy")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("过期策略。");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasComment("次卡名称。");
|
||||
|
||||
b.Property<string>("NotifyChannelsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasComment("购买通知渠道 JSON。");
|
||||
|
||||
b.Property<decimal?>("OriginalPrice")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)")
|
||||
.HasComment("原价。");
|
||||
|
||||
b.Property<int?>("PerOrderLimit")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("每单限用次数。");
|
||||
|
||||
b.Property<int?>("PerUserPurchaseLimit")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("每人限购张数。");
|
||||
|
||||
b.Property<decimal>("SalePrice")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)")
|
||||
.HasComment("售价。");
|
||||
|
||||
b.Property<string>("ScopeCategoryIdsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasComment("指定分类 ID JSON。");
|
||||
|
||||
b.Property<string>("ScopeProductIdsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasComment("指定商品 ID JSON。");
|
||||
|
||||
b.Property<string>("ScopeTagIdsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasComment("指定标签 ID JSON。");
|
||||
|
||||
b.Property<int>("ScopeType")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("适用范围类型。");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("次卡状态。");
|
||||
|
||||
b.Property<long>("StoreId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("门店 ID。");
|
||||
|
||||
b.Property<long>("TenantId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("所属租户 ID。");
|
||||
|
||||
b.Property<int>("TotalTimes")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("总次数。");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||
|
||||
b.Property<decimal?>("UsageCapAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)")
|
||||
.HasComment("金额上限(UsageMode=Cap 时有效)。");
|
||||
|
||||
b.Property<int>("UsageMode")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("使用模式。");
|
||||
|
||||
b.Property<DateTime?>("ValidFrom")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("固定开始日期(UTC,ValidityType=DateRange 时有效)。");
|
||||
|
||||
b.Property<DateTime?>("ValidTo")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("固定结束日期(UTC,ValidityType=DateRange 时有效)。");
|
||||
|
||||
b.Property<int?>("ValidityDays")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("固定天数(ValidityType=Days 时有效)。");
|
||||
|
||||
b.Property<int>("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<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("实体唯一标识。");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("创建时间(UTC)。");
|
||||
|
||||
b.Property<long?>("CreatedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||
|
||||
b.Property<decimal?>("ExtraPayAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)")
|
||||
.HasComment("超额补差金额。");
|
||||
|
||||
b.Property<string>("ProductName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasComment("兑换商品名称。");
|
||||
|
||||
b.Property<long>("PunchCardInstanceId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("次卡实例 ID。");
|
||||
|
||||
b.Property<long>("PunchCardTemplateId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("次卡模板 ID。");
|
||||
|
||||
b.Property<string>("RecordNo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasComment("使用单号。");
|
||||
|
||||
b.Property<int>("RemainingTimesAfterUse")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("使用后剩余次数。");
|
||||
|
||||
b.Property<int>("StatusAfterUse")
|
||||
.HasColumnType("integer")
|
||||
.HasComment("本次记录状态。");
|
||||
|
||||
b.Property<long>("StoreId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("门店 ID。");
|
||||
|
||||
b.Property<long>("TenantId")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("所属租户 ID。");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||
|
||||
b.Property<long?>("UpdatedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||
|
||||
b.Property<DateTime>("UsedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("使用时间(UTC)。");
|
||||
|
||||
b.Property<int>("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<long>("Id")
|
||||
|
||||
Reference in New Issue
Block a user