413 lines
14 KiB
C#
413 lines
14 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;
|
|
using TakeoutSaaS.Infrastructure.Logs.Persistence;
|
|
|
|
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
|
|
|
/// <summary>
|
|
/// 订阅管理仓储实现。
|
|
/// </summary>
|
|
public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, TakeoutLogsDbContext logsContext) : ISubscriptionRepository
|
|
{
|
|
#region 订阅查询
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TenantSubscription?> FindByIdAsync(
|
|
long subscriptionId,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
var query = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
return await query
|
|
.FirstOrDefaultAsync(s => s.Id == subscriptionId, cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<TenantSubscription>> FindByIdsAsync(
|
|
IEnumerable<long> subscriptionIds,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
var ids = subscriptionIds.ToList();
|
|
var query = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
return await query
|
|
.Where(s => ids.Contains(s.Id))
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<(IReadOnlyList<SubscriptionWithRelations> Items, int Total)> SearchPagedAsync(
|
|
SubscriptionSearchFilter filter,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
// 1. 构建基础查询
|
|
var subscriptionQuery = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
var query = subscriptionQuery
|
|
.AsNoTracking()
|
|
.Join(
|
|
dbContext.Tenants,
|
|
sub => sub.TenantId,
|
|
tenant => tenant.Id,
|
|
(sub, tenant) => new { Subscription = sub, Tenant = tenant }
|
|
)
|
|
.Join(
|
|
dbContext.TenantPackages,
|
|
combined => combined.Subscription.TenantPackageId,
|
|
package => package.Id,
|
|
(combined, package) => new { combined.Subscription, combined.Tenant, Package = package }
|
|
)
|
|
.GroupJoin(
|
|
dbContext.TenantPackages,
|
|
combined => combined.Subscription.ScheduledPackageId,
|
|
scheduledPackage => scheduledPackage.Id,
|
|
(combined, scheduledPackages) => new { combined.Subscription, combined.Tenant, combined.Package, ScheduledPackage = scheduledPackages.FirstOrDefault() }
|
|
);
|
|
|
|
// 2. 应用过滤条件
|
|
if (filter.Status.HasValue)
|
|
{
|
|
query = query.Where(x => x.Subscription.Status == filter.Status.Value);
|
|
}
|
|
|
|
if (filter.TenantPackageId.HasValue)
|
|
{
|
|
query = query.Where(x => x.Subscription.TenantPackageId == filter.TenantPackageId.Value);
|
|
}
|
|
|
|
if (filter.TenantId.HasValue)
|
|
{
|
|
query = query.Where(x => x.Subscription.TenantId == filter.TenantId.Value);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(filter.TenantKeyword))
|
|
{
|
|
var keyword = filter.TenantKeyword.Trim().ToLower();
|
|
query = query.Where(x => x.Tenant.Name.ToLower().Contains(keyword) || x.Tenant.Code.ToLower().Contains(keyword));
|
|
}
|
|
|
|
if (filter.ExpiringWithinDays.HasValue)
|
|
{
|
|
var expiryDate = DateTime.UtcNow.AddDays(filter.ExpiringWithinDays.Value);
|
|
query = query.Where(x => x.Subscription.EffectiveTo <= expiryDate && x.Subscription.EffectiveTo >= DateTime.UtcNow);
|
|
}
|
|
|
|
if (filter.AutoRenew.HasValue)
|
|
{
|
|
query = query.Where(x => x.Subscription.AutoRenew == filter.AutoRenew.Value);
|
|
}
|
|
|
|
// 3. 获取总数
|
|
var total = await query.CountAsync(cancellationToken);
|
|
|
|
// 4. 排序和分页
|
|
var items = await query
|
|
.OrderByDescending(x => x.Subscription.CreatedAt)
|
|
.Skip((filter.Page - 1) * filter.PageSize)
|
|
.Take(filter.PageSize)
|
|
.Select(x => new SubscriptionWithRelations
|
|
{
|
|
Subscription = x.Subscription,
|
|
TenantName = x.Tenant.Name,
|
|
TenantCode = x.Tenant.Code,
|
|
PackageName = x.Package.Name,
|
|
ScheduledPackageName = x.ScheduledPackage != null ? x.ScheduledPackage.Name : null
|
|
})
|
|
.ToListAsync(cancellationToken);
|
|
|
|
return (items, total);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<SubscriptionDetailInfo?> GetDetailAsync(
|
|
long subscriptionId,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
var subscriptionQuery = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
var result = await subscriptionQuery
|
|
.AsNoTracking()
|
|
.Where(s => s.Id == subscriptionId)
|
|
.Select(s => new
|
|
{
|
|
Subscription = s,
|
|
Tenant = dbContext.Tenants.FirstOrDefault(t => t.Id == s.TenantId),
|
|
Package = dbContext.TenantPackages.FirstOrDefault(p => p.Id == s.TenantPackageId),
|
|
ScheduledPackage = s.ScheduledPackageId.HasValue
|
|
? dbContext.TenantPackages.FirstOrDefault(p => p.Id == s.ScheduledPackageId)
|
|
: null
|
|
})
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|
|
|
if (result == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new SubscriptionDetailInfo
|
|
{
|
|
Subscription = result.Subscription,
|
|
TenantName = result.Tenant?.Name ?? "",
|
|
TenantCode = result.Tenant?.Code ?? "",
|
|
Package = result.Package,
|
|
ScheduledPackage = result.ScheduledPackage
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<SubscriptionWithTenant>> FindByIdsWithTenantAsync(
|
|
IEnumerable<long> subscriptionIds,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
var ids = subscriptionIds.ToList();
|
|
|
|
var query = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
return await query
|
|
.Where(s => ids.Contains(s.Id))
|
|
.Join(
|
|
dbContext.Tenants,
|
|
sub => sub.TenantId,
|
|
tenant => tenant.Id,
|
|
(sub, tenant) => new SubscriptionWithTenant
|
|
{
|
|
Subscription = sub,
|
|
Tenant = tenant
|
|
}
|
|
)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<AutoRenewalCandidate>> FindAutoRenewalCandidatesAsync(
|
|
DateTime now,
|
|
DateTime renewalThreshold,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
// 1. 查询开启自动续费且即将到期的活跃订阅
|
|
var subscriptionQuery = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
var query = subscriptionQuery
|
|
.Where(s => s.Status == SubscriptionStatus.Active
|
|
&& s.AutoRenew
|
|
&& s.EffectiveTo <= renewalThreshold
|
|
&& s.EffectiveTo > now)
|
|
.Join(
|
|
dbContext.TenantPackages,
|
|
subscription => subscription.TenantPackageId,
|
|
package => package.Id,
|
|
(subscription, package) => new AutoRenewalCandidate
|
|
{
|
|
Subscription = subscription,
|
|
Package = package
|
|
});
|
|
|
|
// 2. 返回候选列表
|
|
return await query.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<RenewalReminderCandidate>> FindRenewalReminderCandidatesAsync(
|
|
DateTime startOfDay,
|
|
DateTime endOfDay,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
// 1. 查询到期落在指定区间的订阅(且未开启自动续费)
|
|
var subscriptionQuery = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
var query = subscriptionQuery
|
|
.Where(s => s.Status == SubscriptionStatus.Active
|
|
&& !s.AutoRenew
|
|
&& s.EffectiveTo >= startOfDay
|
|
&& s.EffectiveTo < endOfDay)
|
|
.Join(
|
|
dbContext.Tenants,
|
|
subscription => subscription.TenantId,
|
|
tenant => tenant.Id,
|
|
(subscription, tenant) => new { Subscription = subscription, Tenant = tenant })
|
|
.Join(
|
|
dbContext.TenantPackages,
|
|
combined => combined.Subscription.TenantPackageId,
|
|
package => package.Id,
|
|
(combined, package) => new RenewalReminderCandidate
|
|
{
|
|
Subscription = combined.Subscription,
|
|
Tenant = combined.Tenant,
|
|
Package = package
|
|
});
|
|
|
|
// 2. 返回候选列表
|
|
return await query.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync(
|
|
DateTime now,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
var query = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
// 1. 查询已到期仍为 Active 的订阅
|
|
return await query
|
|
.Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo < now)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<TenantSubscription>> FindGracePeriodExpiredSubscriptionsAsync(
|
|
DateTime now,
|
|
int gracePeriodDays,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
var query = ignoreTenantFilter
|
|
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
|
: dbContext.TenantSubscriptions;
|
|
|
|
// 1. 查询宽限期已结束的订阅
|
|
return await query
|
|
.Where(s => s.Status == SubscriptionStatus.GracePeriod
|
|
&& s.EffectiveTo.AddDays(gracePeriodDays) < now)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 套餐查询
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TenantPackage?> FindPackageByIdAsync(long packageId, CancellationToken cancellationToken = default)
|
|
{
|
|
return await dbContext.TenantPackages
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(p => p.Id == packageId, cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 订阅更新
|
|
|
|
/// <inheritdoc />
|
|
public Task UpdateAsync(TenantSubscription subscription, CancellationToken cancellationToken = default)
|
|
{
|
|
dbContext.TenantSubscriptions.Update(subscription);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 订阅历史
|
|
|
|
/// <inheritdoc />
|
|
public Task AddHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default)
|
|
{
|
|
dbContext.TenantSubscriptionHistories.Add(history);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<SubscriptionHistoryWithPackageNames>> GetHistoryAsync(
|
|
long subscriptionId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return await dbContext.TenantSubscriptionHistories
|
|
.AsNoTracking()
|
|
.Where(h => h.TenantSubscriptionId == subscriptionId)
|
|
.OrderByDescending(h => h.CreatedAt)
|
|
.Select(h => new SubscriptionHistoryWithPackageNames
|
|
{
|
|
History = h,
|
|
FromPackageName = dbContext.TenantPackages
|
|
.Where(p => p.Id == h.FromPackageId)
|
|
.Select(p => p.Name)
|
|
.FirstOrDefault() ?? "",
|
|
ToPackageName = dbContext.TenantPackages
|
|
.Where(p => p.Id == h.ToPackageId)
|
|
.Select(p => p.Name)
|
|
.FirstOrDefault() ?? ""
|
|
})
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 配额使用
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync(
|
|
long tenantId,
|
|
CancellationToken cancellationToken = default,
|
|
bool ignoreTenantFilter = false)
|
|
{
|
|
var query = ignoreTenantFilter
|
|
? dbContext.TenantQuotaUsages.IgnoreQueryFilters()
|
|
: dbContext.TenantQuotaUsages;
|
|
|
|
return await query
|
|
.AsNoTracking()
|
|
.Where(q => q.TenantId == tenantId)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 通知
|
|
|
|
/// <inheritdoc />
|
|
public Task AddNotificationAsync(TenantNotification notification, CancellationToken cancellationToken = default)
|
|
{
|
|
dbContext.TenantNotifications.Add(notification);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 操作日志
|
|
|
|
/// <inheritdoc />
|
|
public Task AddOperationLogAsync(OperationLog log, CancellationToken cancellationToken = default)
|
|
{
|
|
logsContext.OperationLogs.Add(log);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <inheritdoc />
|
|
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
// 1. 保存业务库变更
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
|
|
// 2. 保存日志库变更
|
|
await logsContext.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|