feat: 完成会员消息触达后端模块
This commit is contained in:
@@ -23,6 +23,14 @@ public interface IMiniUserRepository
|
||||
/// <returns>小程序用户,如果不存在则返回 null</returns>
|
||||
Task<MiniUser?> FindByIdAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按用户标识集合批量查询小程序用户。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MiniUser>> GetByIdsAsync(
|
||||
IReadOnlyCollection<long> ids,
|
||||
long tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 创建或更新小程序用户(如果 OpenId 已存在则更新,否则创建)。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Membership.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 会员消息模板。
|
||||
/// </summary>
|
||||
public sealed class MemberMessageTemplate : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 模板名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 模板分类。
|
||||
/// </summary>
|
||||
public MemberMessageTemplateCategory Category { get; set; } = MemberMessageTemplateCategory.Notice;
|
||||
|
||||
/// <summary>
|
||||
/// 模板内容。
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 使用次数。
|
||||
/// </summary>
|
||||
public int UsageCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近使用时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? LastUsedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Membership.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 会员消息触达主记录。
|
||||
/// </summary>
|
||||
public sealed class MemberReachMessage : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识(可空,空表示当前商户全部可见门店)。
|
||||
/// </summary>
|
||||
public long? StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模板标识(可空)。
|
||||
/// </summary>
|
||||
public long? TemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 消息正文。
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道数组 JSON(字符串枚举)。
|
||||
/// </summary>
|
||||
public string ChannelsJson { get; set; } = "[]";
|
||||
|
||||
/// <summary>
|
||||
/// 目标人群类型。
|
||||
/// </summary>
|
||||
public MemberMessageAudienceType AudienceType { get; set; } = MemberMessageAudienceType.All;
|
||||
|
||||
/// <summary>
|
||||
/// 目标标签 JSON(字符串数组)。
|
||||
/// </summary>
|
||||
public string AudienceTagsJson { get; set; } = "[]";
|
||||
|
||||
/// <summary>
|
||||
/// 预计触达人数。
|
||||
/// </summary>
|
||||
public int EstimatedReachCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送时间类型。
|
||||
/// </summary>
|
||||
public MemberMessageScheduleType ScheduleType { get; set; } = MemberMessageScheduleType.Immediate;
|
||||
|
||||
/// <summary>
|
||||
/// 定时发送时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? ScheduledAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public MemberMessageStatus Status { get; set; } = MemberMessageStatus.Draft;
|
||||
|
||||
/// <summary>
|
||||
/// 实际发送时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? SentAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实际发送成功数量。
|
||||
/// </summary>
|
||||
public int SentCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已读数量。
|
||||
/// </summary>
|
||||
public int ReadCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 转化数量。
|
||||
/// </summary>
|
||||
public int ConvertedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hangfire 任务 ID。
|
||||
/// </summary>
|
||||
public string? HangfireJobId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近失败摘要。
|
||||
/// </summary>
|
||||
public string? LastError { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Membership.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 会员消息触达收件明细。
|
||||
/// </summary>
|
||||
public sealed class MemberReachRecipient : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息标识。
|
||||
/// </summary>
|
||||
public long MessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会员标识。
|
||||
/// </summary>
|
||||
public long MemberId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道。
|
||||
/// </summary>
|
||||
public MemberMessageChannel Channel { get; set; } = MemberMessageChannel.InApp;
|
||||
|
||||
/// <summary>
|
||||
/// 手机号快照。
|
||||
/// </summary>
|
||||
public string? Mobile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信 OpenId 快照。
|
||||
/// </summary>
|
||||
public string? OpenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送状态。
|
||||
/// </summary>
|
||||
public MemberMessageRecipientStatus Status { get; set; } = MemberMessageRecipientStatus.Pending;
|
||||
|
||||
/// <summary>
|
||||
/// 发送时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? SentAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已读时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? ReadAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 转化时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? ConvertedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败摘要。
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 会员消息目标人群类型。
|
||||
/// </summary>
|
||||
public enum MemberMessageAudienceType
|
||||
{
|
||||
/// <summary>
|
||||
/// 全部会员。
|
||||
/// </summary>
|
||||
All = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 按标签筛选。
|
||||
/// </summary>
|
||||
Tags = 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 会员消息发送渠道。
|
||||
/// </summary>
|
||||
public enum MemberMessageChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// 站内信。
|
||||
/// </summary>
|
||||
InApp = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 短信。
|
||||
/// </summary>
|
||||
Sms = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 微信小程序订阅消息。
|
||||
/// </summary>
|
||||
WeChatMini = 2
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 消息收件明细发送状态。
|
||||
/// </summary>
|
||||
public enum MemberMessageRecipientStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 待发送。
|
||||
/// </summary>
|
||||
Pending = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 发送成功。
|
||||
/// </summary>
|
||||
Sent = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 发送失败。
|
||||
/// </summary>
|
||||
Failed = 2
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 消息发送时间类型。
|
||||
/// </summary>
|
||||
public enum MemberMessageScheduleType
|
||||
{
|
||||
/// <summary>
|
||||
/// 立即发送。
|
||||
/// </summary>
|
||||
Immediate = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 定时发送。
|
||||
/// </summary>
|
||||
Scheduled = 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 会员消息状态。
|
||||
/// </summary>
|
||||
public enum MemberMessageStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 草稿。
|
||||
/// </summary>
|
||||
Draft = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 待发送。
|
||||
/// </summary>
|
||||
Pending = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 发送中。
|
||||
/// </summary>
|
||||
Sending = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 已发送。
|
||||
/// </summary>
|
||||
Sent = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 发送失败。
|
||||
/// </summary>
|
||||
Failed = 4
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 消息模板分类。
|
||||
/// </summary>
|
||||
public enum MemberMessageTemplateCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// 营销类。
|
||||
/// </summary>
|
||||
Marketing = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 通知类。
|
||||
/// </summary>
|
||||
Notice = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 召回类。
|
||||
/// </summary>
|
||||
Recall = 2
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using TakeoutSaaS.Domain.Membership.Entities;
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Membership.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 会员消息触达仓储。
|
||||
/// </summary>
|
||||
public interface IMemberMessageReachRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询消息。
|
||||
/// </summary>
|
||||
Task<(IReadOnlyList<MemberReachMessage> Items, int Total)> SearchMessagesAsync(
|
||||
long tenantId,
|
||||
MemberMessageStatus? status,
|
||||
MemberMessageChannel? channel,
|
||||
string? keyword,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按标识查询消息。
|
||||
/// </summary>
|
||||
Task<MemberReachMessage?> FindMessageByIdAsync(long tenantId, long messageId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增消息。
|
||||
/// </summary>
|
||||
Task AddMessageAsync(MemberReachMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新消息。
|
||||
/// </summary>
|
||||
Task UpdateMessageAsync(MemberReachMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除消息。
|
||||
/// </summary>
|
||||
Task DeleteMessageAsync(MemberReachMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询消息收件明细。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MemberReachRecipient>> GetRecipientsAsync(
|
||||
long tenantId,
|
||||
long messageId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除消息收件明细。
|
||||
/// </summary>
|
||||
Task RemoveRecipientsAsync(
|
||||
long tenantId,
|
||||
long messageId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 批量新增消息收件明细。
|
||||
/// </summary>
|
||||
Task AddRecipientsAsync(
|
||||
IReadOnlyCollection<MemberReachRecipient> recipients,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询模板。
|
||||
/// </summary>
|
||||
Task<(IReadOnlyList<MemberMessageTemplate> Items, int Total)> SearchTemplatesAsync(
|
||||
long tenantId,
|
||||
MemberMessageTemplateCategory? category,
|
||||
string? keyword,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按标识查询模板。
|
||||
/// </summary>
|
||||
Task<MemberMessageTemplate?> FindTemplateByIdAsync(long tenantId, long templateId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按名称查询模板(忽略大小写)。
|
||||
/// </summary>
|
||||
Task<MemberMessageTemplate?> FindTemplateByNameAsync(long tenantId, string name, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增模板。
|
||||
/// </summary>
|
||||
Task AddTemplateAsync(MemberMessageTemplate template, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新模板。
|
||||
/// </summary>
|
||||
Task UpdateTemplateAsync(MemberMessageTemplate template, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除模板。
|
||||
/// </summary>
|
||||
Task DeleteTemplateAsync(MemberMessageTemplate template, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取月度发送统计。
|
||||
/// </summary>
|
||||
Task<MemberMessageMonthlyStatsSnapshot> GetMonthlyStatsAsync(
|
||||
long tenantId,
|
||||
DateTime monthStartUtc,
|
||||
DateTime monthEndUtc,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 保存变更。
|
||||
/// </summary>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 会员消息月度统计快照。
|
||||
/// </summary>
|
||||
public sealed record MemberMessageMonthlyStatsSnapshot(
|
||||
int SentMessageCount,
|
||||
int ReachMemberCount,
|
||||
int SentRecipientCount,
|
||||
int ReadRecipientCount,
|
||||
int ConvertedRecipientCount);
|
||||
@@ -83,6 +83,14 @@ public interface IMemberRepository
|
||||
long memberProfileId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按会员集合批量查询标签。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MemberProfileTag>> GetProfileTagsByMemberIdsAsync(
|
||||
long tenantId,
|
||||
IReadOnlyCollection<long> memberProfileIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 替换会员标签集合。
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user