新增定时任务 (Scheduler Module): - SubscriptionAutoRenewalJob: 自动续费账单生成 - SubscriptionRenewalReminderJob: 续费提醒发送 (7/3/1天) - SubscriptionExpiryCheckJob: 到期检查与宽限期处理 新增 Command/Handler: - ProcessAutoRenewalCommand: 处理自动续费逻辑 - ProcessRenewalRemindersCommand: 处理续费提醒逻辑 - ProcessSubscriptionExpiryCommand: 处理订阅到期逻辑 配置项 (SubscriptionAutomationOptions): - AutoRenewalDaysBeforeExpiry: 到期前N天生成续费账单 - ReminderDaysBeforeExpiry: 提醒天数数组 - GracePeriodDays: 宽限期天数 - 各任务执行小时配置 Repository 增强: - ISubscriptionRepository: 新增自动化查询方法 - ITenantBillingRepository: 新增账单创建方法 - ITenantNotificationRepository: 新增通知创建方法 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
95 lines
2.9 KiB
C#
95 lines
2.9 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using TakeoutSaaS.Domain.Tenants.Entities;
|
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
|
|
|
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
|
|
|
/// <summary>
|
|
/// EF 租户通知仓储。
|
|
/// </summary>
|
|
public sealed class EfTenantNotificationRepository(TakeoutAppDbContext context) : ITenantNotificationRepository
|
|
{
|
|
/// <inheritdoc />
|
|
public Task<IReadOnlyList<TenantNotification>> SearchAsync(
|
|
long tenantId,
|
|
TenantNotificationSeverity? severity,
|
|
bool? unreadOnly,
|
|
DateTime? from,
|
|
DateTime? to,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var query = context.TenantNotifications.AsNoTracking()
|
|
.Where(x => x.TenantId == tenantId);
|
|
|
|
if (severity.HasValue)
|
|
{
|
|
query = query.Where(x => x.Severity == severity.Value);
|
|
}
|
|
|
|
if (unreadOnly == true)
|
|
{
|
|
query = query.Where(x => x.ReadAt == null);
|
|
}
|
|
|
|
if (from.HasValue)
|
|
{
|
|
query = query.Where(x => x.SentAt >= from.Value);
|
|
}
|
|
|
|
if (to.HasValue)
|
|
{
|
|
query = query.Where(x => x.SentAt <= to.Value);
|
|
}
|
|
|
|
return query
|
|
.OrderByDescending(x => x.SentAt)
|
|
.ToListAsync(cancellationToken)
|
|
.ContinueWith(t => (IReadOnlyList<TenantNotification>)t.Result, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<TenantNotification?> FindByIdAsync(long tenantId, long notificationId, CancellationToken cancellationToken = default)
|
|
{
|
|
return context.TenantNotifications
|
|
.FirstOrDefaultAsync(x => x.TenantId == tenantId && x.Id == notificationId, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> ExistsByMetadataAsync(
|
|
long tenantId,
|
|
string title,
|
|
string metadataJson,
|
|
DateTime sentAfter,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return context.TenantNotifications.AsNoTracking()
|
|
.AnyAsync(
|
|
x => x.TenantId == tenantId
|
|
&& x.Title == title
|
|
&& x.MetadataJson == metadataJson
|
|
&& x.SentAt >= sentAfter,
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task AddAsync(TenantNotification notification, CancellationToken cancellationToken = default)
|
|
{
|
|
return context.TenantNotifications.AddAsync(notification, cancellationToken).AsTask();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task UpdateAsync(TenantNotification notification, CancellationToken cancellationToken = default)
|
|
{
|
|
context.TenantNotifications.Update(notification);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return context.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|