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

@@ -0,0 +1,260 @@
using System.Text.Json;
using TakeoutSaaS.Application.App.Members.MessageReach.Dto;
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.Members.MessageReach;
internal static class MemberMessageReachMapping
{
internal static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true
};
internal static MemberMessageAudienceType ParseAudienceType(string? value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
{
"all" => MemberMessageAudienceType.All,
"tag" => MemberMessageAudienceType.Tags,
"tags" => MemberMessageAudienceType.Tags,
_ => throw new BusinessException(ErrorCodes.BadRequest, "audienceType 非法")
};
}
internal static string ToAudienceTypeText(MemberMessageAudienceType value)
{
return value switch
{
MemberMessageAudienceType.All => "all",
MemberMessageAudienceType.Tags => "tag",
_ => "all"
};
}
internal static MemberMessageScheduleType ParseScheduleType(string? value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
{
"immediate" => MemberMessageScheduleType.Immediate,
"scheduled" => MemberMessageScheduleType.Scheduled,
_ => throw new BusinessException(ErrorCodes.BadRequest, "scheduleType 非法")
};
}
internal static string ToScheduleTypeText(MemberMessageScheduleType value)
{
return value switch
{
MemberMessageScheduleType.Immediate => "immediate",
MemberMessageScheduleType.Scheduled => "scheduled",
_ => "immediate"
};
}
internal static MemberMessageStatus ParseStatusOrNull(string? value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
{
"draft" => MemberMessageStatus.Draft,
"pending" => MemberMessageStatus.Pending,
"sending" => MemberMessageStatus.Sending,
"sent" => MemberMessageStatus.Sent,
"failed" => MemberMessageStatus.Failed,
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 非法")
};
}
internal static MemberMessageStatus? TryParseStatus(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return ParseStatusOrNull(value);
}
internal static string ToStatusText(MemberMessageStatus value)
{
return value switch
{
MemberMessageStatus.Draft => "draft",
MemberMessageStatus.Pending => "pending",
MemberMessageStatus.Sending => "sending",
MemberMessageStatus.Sent => "sent",
MemberMessageStatus.Failed => "failed",
_ => "draft"
};
}
internal static MemberMessageChannel ParseChannel(string? value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
{
"inapp" => MemberMessageChannel.InApp,
"sms" => MemberMessageChannel.Sms,
"wechat-mini" => MemberMessageChannel.WeChatMini,
"wechat" => MemberMessageChannel.WeChatMini,
_ => throw new BusinessException(ErrorCodes.BadRequest, "channel 非法")
};
}
internal static MemberMessageChannel? TryParseChannel(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return ParseChannel(value);
}
internal static string ToChannelText(MemberMessageChannel value)
{
return value switch
{
MemberMessageChannel.InApp => "inapp",
MemberMessageChannel.Sms => "sms",
MemberMessageChannel.WeChatMini => "wechat-mini",
_ => "inapp"
};
}
internal static string ToRecipientStatusText(MemberMessageRecipientStatus value)
{
return value switch
{
MemberMessageRecipientStatus.Pending => "pending",
MemberMessageRecipientStatus.Sent => "sent",
MemberMessageRecipientStatus.Failed => "failed",
_ => "pending"
};
}
internal static MemberMessageTemplateCategory ParseTemplateCategory(string? value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
{
"marketing" => MemberMessageTemplateCategory.Marketing,
"notice" => MemberMessageTemplateCategory.Notice,
"recall" => MemberMessageTemplateCategory.Recall,
_ => throw new BusinessException(ErrorCodes.BadRequest, "category 非法")
};
}
internal static MemberMessageTemplateCategory? TryParseTemplateCategory(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return ParseTemplateCategory(value);
}
internal static string ToTemplateCategoryText(MemberMessageTemplateCategory value)
{
return value switch
{
MemberMessageTemplateCategory.Marketing => "marketing",
MemberMessageTemplateCategory.Notice => "notice",
MemberMessageTemplateCategory.Recall => "recall",
_ => "notice"
};
}
internal static IReadOnlyList<string> NormalizeTags(IReadOnlyList<string>? tags)
{
return (tags ?? [])
.Select(item => (item ?? string.Empty).Trim())
.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item, StringComparer.OrdinalIgnoreCase)
.ToList();
}
internal static IReadOnlyList<string> NormalizeChannels(IReadOnlyList<string>? channels)
{
var parsed = (channels ?? [])
.Select(ParseChannel)
.Distinct()
.ToList();
if (parsed.Count == 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "channels 不能为空");
}
return parsed.Select(ToChannelText).ToList();
}
internal static string SerializeStringArray(IReadOnlyList<string> source)
{
return JsonSerializer.Serialize(source, JsonOptions);
}
internal static IReadOnlyList<string> DeserializeStringArray(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return [];
}
try
{
return JsonSerializer.Deserialize<List<string>>(value, JsonOptions)?
.Select(item => (item ?? string.Empty).Trim())
.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList() ?? [];
}
catch
{
return [];
}
}
internal static decimal ResolveRatePercent(int numerator, int denominator)
{
if (denominator <= 0 || numerator <= 0)
{
return 0;
}
return decimal.Round((decimal)numerator * 100m / denominator, 2, MidpointRounding.AwayFromZero);
}
internal static MemberMessageTemplateDto ToTemplateDto(MemberMessageTemplate source)
{
return new MemberMessageTemplateDto
{
TemplateId = source.Id,
Name = source.Name,
Category = ToTemplateCategoryText(source.Category),
Content = source.Content,
UsageCount = source.UsageCount,
LastUsedAt = source.LastUsedAt
};
}
internal static MemberMessageReachRecipientDto ToRecipientDto(MemberReachRecipient source)
{
return new MemberMessageReachRecipientDto
{
MemberId = source.MemberId,
Channel = ToChannelText(source.Channel),
Status = ToRecipientStatusText(source.Status),
Mobile = source.Mobile,
OpenId = source.OpenId,
SentAt = source.SentAt,
ReadAt = source.ReadAt,
ConvertedAt = source.ConvertedAt,
ErrorMessage = source.ErrorMessage
};
}
}