feat: 完成会员消息触达后端模块

This commit is contained in:
2026-03-04 11:53:52 +08:00
parent 2970134200
commit a8cfda88f7
33 changed files with 4282 additions and 0 deletions

View File

@@ -51,6 +51,7 @@ public static class AppServiceCollectionExtensions
services.AddScoped<IPromotionCampaignRepository, EfPromotionCampaignRepository>();
services.AddScoped<IPunchCardRepository, EfPunchCardRepository>();
services.AddScoped<IMemberRepository, EfMemberRepository>();
services.AddScoped<IMemberMessageReachRepository, EfMemberMessageReachRepository>();
services.AddScoped<IStoredCardRepository, EfStoredCardRepository>();
services.AddScoped<IOrderRepository, EfOrderRepository>();
services.AddScoped<IPaymentRepository, EfPaymentRepository>();

View File

@@ -410,6 +410,18 @@ public sealed class TakeoutAppDbContext(
/// </summary>
public DbSet<MemberStoredCardRechargeRecord> MemberStoredCardRechargeRecords => Set<MemberStoredCardRechargeRecord>();
/// <summary>
/// 会员消息触达记录。
/// </summary>
public DbSet<MemberReachMessage> MemberReachMessages => Set<MemberReachMessage>();
/// <summary>
/// 会员消息模板。
/// </summary>
public DbSet<MemberMessageTemplate> MemberMessageTemplates => Set<MemberMessageTemplate>();
/// <summary>
/// 会员消息触达收件明细。
/// </summary>
public DbSet<MemberReachRecipient> MemberReachRecipients => Set<MemberReachRecipient>();
/// <summary>
/// 会话记录。
/// </summary>
public DbSet<ChatSession> ChatSessions => Set<ChatSession>();
@@ -578,6 +590,9 @@ public sealed class TakeoutAppDbContext(
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
ConfigureMemberStoredCardPlan(modelBuilder.Entity<MemberStoredCardPlan>());
ConfigureMemberStoredCardRechargeRecord(modelBuilder.Entity<MemberStoredCardRechargeRecord>());
ConfigureMemberReachMessage(modelBuilder.Entity<MemberReachMessage>());
ConfigureMemberMessageTemplate(modelBuilder.Entity<MemberMessageTemplate>());
ConfigureMemberReachRecipient(modelBuilder.Entity<MemberReachRecipient>());
ConfigureChatSession(modelBuilder.Entity<ChatSession>());
ConfigureChatMessage(modelBuilder.Entity<ChatMessage>());
ConfigureSupportTicket(modelBuilder.Entity<SupportTicket>());
@@ -1892,6 +1907,62 @@ public sealed class TakeoutAppDbContext(
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RechargedAt });
}
private static void ConfigureMemberReachMessage(EntityTypeBuilder<MemberReachMessage> builder)
{
builder.ToTable("member_reach_messages");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId);
builder.Property(x => x.TemplateId);
builder.Property(x => x.Title).HasMaxLength(128).IsRequired();
builder.Property(x => x.Content).HasColumnType("text").IsRequired();
builder.Property(x => x.ChannelsJson).HasColumnType("text").IsRequired();
builder.Property(x => x.AudienceType).HasConversion<int>();
builder.Property(x => x.AudienceTagsJson).HasColumnType("text").IsRequired();
builder.Property(x => x.EstimatedReachCount).IsRequired();
builder.Property(x => x.ScheduleType).HasConversion<int>();
builder.Property(x => x.ScheduledAt);
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.SentAt);
builder.Property(x => x.SentCount).IsRequired();
builder.Property(x => x.ReadCount).IsRequired();
builder.Property(x => x.ConvertedCount).IsRequired();
builder.Property(x => x.HangfireJobId).HasMaxLength(64);
builder.Property(x => x.LastError).HasMaxLength(1024);
builder.HasIndex(x => new { x.TenantId, x.Status, x.ScheduledAt });
builder.HasIndex(x => new { x.TenantId, x.CreatedAt });
}
private static void ConfigureMemberMessageTemplate(EntityTypeBuilder<MemberMessageTemplate> builder)
{
builder.ToTable("member_message_templates");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.Category).HasConversion<int>();
builder.Property(x => x.Content).HasColumnType("text").IsRequired();
builder.Property(x => x.UsageCount).IsRequired();
builder.Property(x => x.LastUsedAt);
builder.HasIndex(x => new { x.TenantId, x.Name }).IsUnique();
builder.HasIndex(x => new { x.TenantId, x.Category, x.UsageCount });
}
private static void ConfigureMemberReachRecipient(EntityTypeBuilder<MemberReachRecipient> builder)
{
builder.ToTable("member_reach_recipients");
builder.HasKey(x => x.Id);
builder.Property(x => x.MessageId).IsRequired();
builder.Property(x => x.MemberId).IsRequired();
builder.Property(x => x.Channel).HasConversion<int>();
builder.Property(x => x.Mobile).HasMaxLength(32);
builder.Property(x => x.OpenId).HasMaxLength(128);
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.SentAt);
builder.Property(x => x.ReadAt);
builder.Property(x => x.ConvertedAt);
builder.Property(x => x.ErrorMessage).HasMaxLength(512);
builder.HasIndex(x => new { x.TenantId, x.MessageId, x.MemberId, x.Channel }).IsUnique();
builder.HasIndex(x => new { x.TenantId, x.MessageId, x.Status });
}
private static void ConfigureChatSession(EntityTypeBuilder<ChatSession> builder)
{
builder.ToTable("chat_sessions");

View File

@@ -0,0 +1,296 @@
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Domain.Membership.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence;
namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary>
/// EF 会员消息触达仓储实现。
/// </summary>
public sealed class EfMemberMessageReachRepository(TakeoutAppDbContext context) : IMemberMessageReachRepository
{
/// <inheritdoc />
public async Task<(IReadOnlyList<MemberReachMessage> Items, int Total)> SearchMessagesAsync(
long tenantId,
MemberMessageStatus? status,
MemberMessageChannel? channel,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
var query = context.MemberReachMessages
.AsNoTracking()
.Where(item => item.TenantId == tenantId);
if (status.HasValue)
{
query = query.Where(item => item.Status == status.Value);
}
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalizedKeyword = keyword.Trim();
query = query.Where(item => EF.Functions.ILike(item.Title, $"%{normalizedKeyword}%"));
}
var source = await query
.OrderByDescending(item => item.CreatedAt)
.ThenByDescending(item => item.Id)
.ToListAsync(cancellationToken);
if (channel.HasValue)
{
source = source
.Where(item => HasChannel(item.ChannelsJson, channel.Value))
.ToList();
}
var total = source.Count;
var items = source
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return (items, total);
}
/// <inheritdoc />
public Task<MemberReachMessage?> FindMessageByIdAsync(long tenantId, long messageId, CancellationToken cancellationToken = default)
{
return context.MemberReachMessages
.FirstOrDefaultAsync(item => item.TenantId == tenantId && item.Id == messageId, cancellationToken);
}
/// <inheritdoc />
public Task AddMessageAsync(MemberReachMessage message, CancellationToken cancellationToken = default)
{
return context.MemberReachMessages.AddAsync(message, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateMessageAsync(MemberReachMessage message, CancellationToken cancellationToken = default)
{
context.MemberReachMessages.Update(message);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task DeleteMessageAsync(MemberReachMessage message, CancellationToken cancellationToken = default)
{
context.MemberReachMessages.Remove(message);
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task<IReadOnlyList<MemberReachRecipient>> GetRecipientsAsync(
long tenantId,
long messageId,
CancellationToken cancellationToken = default)
{
return await context.MemberReachRecipients
.AsNoTracking()
.Where(item => item.TenantId == tenantId && item.MessageId == messageId)
.OrderBy(item => item.MemberId)
.ThenBy(item => item.Channel)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task RemoveRecipientsAsync(long tenantId, long messageId, CancellationToken cancellationToken = default)
{
var recipients = await context.MemberReachRecipients
.Where(item => item.TenantId == tenantId && item.MessageId == messageId)
.ToListAsync(cancellationToken);
if (recipients.Count == 0)
{
return;
}
context.MemberReachRecipients.RemoveRange(recipients);
}
/// <inheritdoc />
public async Task AddRecipientsAsync(
IReadOnlyCollection<MemberReachRecipient> recipients,
CancellationToken cancellationToken = default)
{
if (recipients.Count == 0)
{
return;
}
await context.MemberReachRecipients.AddRangeAsync(recipients, cancellationToken);
}
/// <inheritdoc />
public async Task<(IReadOnlyList<MemberMessageTemplate> Items, int Total)> SearchTemplatesAsync(
long tenantId,
MemberMessageTemplateCategory? category,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
var query = context.MemberMessageTemplates
.AsNoTracking()
.Where(item => item.TenantId == tenantId);
if (category.HasValue)
{
query = query.Where(item => item.Category == category.Value);
}
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalizedKeyword = keyword.Trim();
query = query.Where(item => EF.Functions.ILike(item.Name, $"%{normalizedKeyword}%"));
}
var total = await query.CountAsync(cancellationToken);
var items = await query
.OrderByDescending(item => item.UsageCount)
.ThenByDescending(item => item.UpdatedAt ?? item.CreatedAt)
.ThenByDescending(item => item.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (items, total);
}
/// <inheritdoc />
public Task<MemberMessageTemplate?> FindTemplateByIdAsync(long tenantId, long templateId, CancellationToken cancellationToken = default)
{
return context.MemberMessageTemplates
.FirstOrDefaultAsync(item => item.TenantId == tenantId && item.Id == templateId, cancellationToken);
}
/// <inheritdoc />
public Task<MemberMessageTemplate?> FindTemplateByNameAsync(long tenantId, string name, CancellationToken cancellationToken = default)
{
var normalizedName = (name ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(normalizedName))
{
return Task.FromResult<MemberMessageTemplate?>(null);
}
return context.MemberMessageTemplates
.FirstOrDefaultAsync(
item =>
item.TenantId == tenantId &&
EF.Functions.ILike(item.Name, normalizedName),
cancellationToken);
}
/// <inheritdoc />
public Task AddTemplateAsync(MemberMessageTemplate template, CancellationToken cancellationToken = default)
{
return context.MemberMessageTemplates.AddAsync(template, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateTemplateAsync(MemberMessageTemplate template, CancellationToken cancellationToken = default)
{
context.MemberMessageTemplates.Update(template);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task DeleteTemplateAsync(MemberMessageTemplate template, CancellationToken cancellationToken = default)
{
context.MemberMessageTemplates.Remove(template);
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task<MemberMessageMonthlyStatsSnapshot> GetMonthlyStatsAsync(
long tenantId,
DateTime monthStartUtc,
DateTime monthEndUtc,
CancellationToken cancellationToken = default)
{
var sentMessageCount = await context.MemberReachMessages
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.Status == MemberMessageStatus.Sent &&
item.SentAt.HasValue &&
item.SentAt.Value >= monthStartUtc &&
item.SentAt.Value < monthEndUtc)
.CountAsync(cancellationToken);
var recipients = await context.MemberReachRecipients
.AsNoTracking()
.Where(item =>
item.TenantId == tenantId &&
item.SentAt.HasValue &&
item.SentAt.Value >= monthStartUtc &&
item.SentAt.Value < monthEndUtc)
.Select(item => new
{
item.MemberId,
item.Status,
item.ReadAt,
item.ConvertedAt
})
.ToListAsync(cancellationToken);
var reachMemberCount = recipients
.Where(item => item.Status == MemberMessageRecipientStatus.Sent)
.Select(item => item.MemberId)
.Distinct()
.Count();
var sentRecipientCount = recipients.Count(item => item.Status == MemberMessageRecipientStatus.Sent);
var readRecipientCount = recipients.Count(item => item.ReadAt.HasValue);
var convertedRecipientCount = recipients.Count(item => item.ConvertedAt.HasValue);
return new MemberMessageMonthlyStatsSnapshot(
sentMessageCount,
reachMemberCount,
sentRecipientCount,
readRecipientCount,
convertedRecipientCount);
}
/// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return context.SaveChangesAsync(cancellationToken);
}
private static bool HasChannel(string channelsJson, MemberMessageChannel channel)
{
if (string.IsNullOrWhiteSpace(channelsJson))
{
return false;
}
try
{
var channels = JsonSerializer.Deserialize<List<string>>(channelsJson) ?? [];
var target = channel switch
{
MemberMessageChannel.InApp => "inapp",
MemberMessageChannel.Sms => "sms",
MemberMessageChannel.WeChatMini => "wechat-mini",
_ => string.Empty
};
if (string.IsNullOrWhiteSpace(target))
{
return false;
}
return channels.Any(item => string.Equals(item, target, StringComparison.OrdinalIgnoreCase));
}
catch
{
return false;
}
}
}

View File

@@ -135,6 +135,25 @@ public sealed class EfMemberRepository(TakeoutAppDbContext context) : IMemberRep
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<MemberProfileTag>> GetProfileTagsByMemberIdsAsync(
long tenantId,
IReadOnlyCollection<long> memberProfileIds,
CancellationToken cancellationToken = default)
{
if (memberProfileIds.Count == 0)
{
return [];
}
return await context.MemberProfileTags
.AsNoTracking()
.Where(x => x.TenantId == tenantId && memberProfileIds.Contains(x.MemberProfileId))
.OrderBy(x => x.MemberProfileId)
.ThenBy(x => x.TagName)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task ReplaceProfileTagsAsync(
long tenantId,

View File

@@ -27,6 +27,23 @@ public sealed class EfMiniUserRepository(IdentityDbContext dbContext) : IMiniUse
public Task<MiniUser?> FindByIdAsync(long id, CancellationToken cancellationToken = default)
=> dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
/// <inheritdoc />
public async Task<IReadOnlyList<MiniUser>> GetByIdsAsync(
IReadOnlyCollection<long> ids,
long tenantId,
CancellationToken cancellationToken = default)
{
if (ids.Count == 0)
{
return [];
}
return await dbContext.MiniUsers
.AsNoTracking()
.Where(x => x.TenantId == tenantId && ids.Contains(x.Id))
.ToListAsync(cancellationToken);
}
/// <summary>
/// 创建或更新小程序用户信息。
/// </summary>

View File

@@ -0,0 +1,154 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddMemberMessageReachModule : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "member_message_templates",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "模板名称。"),
Category = table.Column<int>(type: "integer", nullable: false, comment: "模板分类。"),
Content = table.Column<string>(type: "text", nullable: false, comment: "模板内容。"),
UsageCount = table.Column<int>(type: "integer", nullable: false, comment: "使用次数。"),
LastUsedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近使用时间UTC。"),
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_member_message_templates", x => x.Id);
},
comment: "会员消息模板。");
migrationBuilder.CreateTable(
name: "member_reach_messages",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
StoreId = table.Column<long>(type: "bigint", nullable: true, comment: "门店标识。"),
TemplateId = table.Column<long>(type: "bigint", nullable: true, comment: "模板标识。"),
Title = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false, comment: "消息标题。"),
Content = table.Column<string>(type: "text", nullable: false, comment: "消息内容。"),
ChannelsJson = table.Column<string>(type: "text", nullable: false, comment: "发送渠道 JSON。"),
AudienceType = table.Column<int>(type: "integer", nullable: false, comment: "目标人群类型。"),
AudienceTagsJson = table.Column<string>(type: "text", nullable: false, comment: "目标标签 JSON。"),
EstimatedReachCount = table.Column<int>(type: "integer", nullable: false, comment: "预计触达人数。"),
ScheduleType = table.Column<int>(type: "integer", nullable: false, comment: "发送时间类型。"),
ScheduledAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "定时发送时间UTC。"),
Status = table.Column<int>(type: "integer", nullable: false, comment: "消息状态。"),
SentAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "发送时间UTC。"),
SentCount = table.Column<int>(type: "integer", nullable: false, comment: "发送成功数量。"),
ReadCount = table.Column<int>(type: "integer", nullable: false, comment: "已读数量。"),
ConvertedCount = table.Column<int>(type: "integer", nullable: false, comment: "转化数量。"),
HangfireJobId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true, comment: "Hangfire 任务 ID。"),
LastError = table.Column<string>(type: "character varying(1024)", maxLength: 1024, 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_member_reach_messages", x => x.Id);
},
comment: "会员消息触达主记录。");
migrationBuilder.CreateTable(
name: "member_reach_recipients",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
MessageId = table.Column<long>(type: "bigint", nullable: false, comment: "消息标识。"),
MemberId = table.Column<long>(type: "bigint", nullable: false, comment: "会员标识。"),
Channel = table.Column<int>(type: "integer", nullable: false, comment: "触达渠道。"),
Mobile = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true, comment: "手机号快照。"),
OpenId = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true, comment: "OpenId 快照。"),
Status = table.Column<int>(type: "integer", nullable: false, comment: "发送状态。"),
SentAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "发送时间UTC。"),
ReadAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "已读时间UTC。"),
ConvertedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "转化时间UTC。"),
ErrorMessage = table.Column<string>(type: "character varying(512)", maxLength: 512, 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_member_reach_recipients", x => x.Id);
},
comment: "会员消息触达收件明细。");
migrationBuilder.CreateIndex(
name: "IX_member_message_templates_TenantId_Category_UsageCount",
table: "member_message_templates",
columns: new[] { "TenantId", "Category", "UsageCount" });
migrationBuilder.CreateIndex(
name: "IX_member_message_templates_TenantId_Name",
table: "member_message_templates",
columns: new[] { "TenantId", "Name" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_member_reach_messages_TenantId_CreatedAt",
table: "member_reach_messages",
columns: new[] { "TenantId", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_member_reach_messages_TenantId_Status_ScheduledAt",
table: "member_reach_messages",
columns: new[] { "TenantId", "Status", "ScheduledAt" });
migrationBuilder.CreateIndex(
name: "IX_member_reach_recipients_TenantId_MessageId_MemberId_Channel",
table: "member_reach_recipients",
columns: new[] { "TenantId", "MessageId", "MemberId", "Channel" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_member_reach_recipients_TenantId_MessageId_Status",
table: "member_reach_recipients",
columns: new[] { "TenantId", "MessageId", "Status" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "member_message_templates");
migrationBuilder.DropTable(
name: "member_reach_messages");
migrationBuilder.DropTable(
name: "member_reach_recipients");
}
}
}