using MediatR;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Ids;
namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
///
/// 续费提醒处理器:在到期前 7/3/1 天等时间点发送站内提醒(幂等)。
///
public sealed class ProcessRenewalRemindersCommandHandler(
ISubscriptionRepository subscriptionRepository,
ITenantNotificationRepository notificationRepository,
IIdGenerator idGenerator,
ILogger logger)
: IRequestHandler
{
private const string ReminderTitle = "订阅续费提醒";
///
public async Task Handle(ProcessRenewalRemindersCommand request, CancellationToken cancellationToken)
{
// 1. 读取提醒配置
var now = DateTime.UtcNow;
var candidateCount = 0;
var createdReminderCount = 0;
var dedupeWindowStart = now.AddHours(-24);
// 2. 按提醒时间点扫描到期订阅
foreach (var daysBeforeExpiry in request.ReminderDaysBeforeExpiry.Distinct().OrderByDescending(x => x))
{
// 2.1 计算目标日期区间(按天匹配)
var targetDate = now.AddDays(daysBeforeExpiry);
var startOfDay = targetDate.Date;
var endOfDay = startOfDay.AddDays(1);
// 2.2 查询候选订阅(活跃 + 未开自动续费 + 到期在当日)
var candidates = await subscriptionRepository.FindRenewalReminderCandidatesAsync(
startOfDay,
endOfDay,
cancellationToken);
candidateCount += candidates.Count;
foreach (var item in candidates)
{
// 2.3 幂等:同一订阅 + 同一天数提醒,在 24 小时内只发送一次
var metadataJson = BuildReminderMetadata(item.Subscription.Id, daysBeforeExpiry, item.Subscription.EffectiveTo);
var alreadySent = await notificationRepository.ExistsByMetadataAsync(
item.Subscription.TenantId,
ReminderTitle,
metadataJson,
dedupeWindowStart,
cancellationToken);
if (alreadySent)
{
continue;
}
// 2.4 构造提醒内容并入库
var notification = new TenantNotification
{
Id = idGenerator.NextId(),
TenantId = item.Subscription.TenantId,
Title = ReminderTitle,
Message = $"您的订阅套餐「{item.Package.Name}」将在 {daysBeforeExpiry} 天内到期(到期时间:{item.Subscription.EffectiveTo:yyyy-MM-dd HH:mm}),请及时续费以免影响使用。",
Severity = daysBeforeExpiry <= 1
? TenantNotificationSeverity.Critical
: TenantNotificationSeverity.Warning,
Channel = TenantNotificationChannel.InApp,
SentAt = now,
ReadAt = null,
MetadataJson = metadataJson,
CreatedAt = now
};
await notificationRepository.AddAsync(notification, cancellationToken);
createdReminderCount++;
logger.LogInformation(
"续费提醒:TenantId={TenantId}, TenantName={TenantName}, SubscriptionId={SubscriptionId}, DaysBeforeExpiry={DaysBeforeExpiry}",
item.Tenant.Id,
item.Tenant.Name,
item.Subscription.Id,
daysBeforeExpiry);
}
}
// 3. 保存变更
if (createdReminderCount > 0)
{
await notificationRepository.SaveChangesAsync(cancellationToken);
}
return new ProcessRenewalRemindersResult
{
CandidateCount = candidateCount,
CreatedReminderCount = createdReminderCount
};
}
private static string BuildReminderMetadata(long subscriptionId, int daysBeforeExpiry, DateTime effectiveTo)
{
// 1. 使用稳定字段顺序的 JSON 作为幂等键
return JsonSerializer.Serialize(new
{
Type = "RenewalReminder",
SubscriptionId = subscriptionId,
DaysBeforeExpiry = daysBeforeExpiry,
EffectiveTo = effectiveTo.ToString("O")
});
}
}