- 新增 AdminRolesController 实现角色 CRUD 和权限管理 - 新增 BillingsController 实现账单查询功能 - 新增 SubscriptionsController 实现订阅管理功能 - 新增 TenantPackagesController 实现套餐管理功能 - 新增租户详情、配额使用、账单列表等查询功能 - 新增 TenantPackage、TenantSubscription 等领域实体 - 新增相关枚举:SubscriptionStatus、TenantPackageType 等 - 更新 appsettings 配置文件 - 更新权限授权策略提供者 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
235 lines
8.3 KiB
C#
235 lines
8.3 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>
|
||
/// 租户套餐仓储实现(AdminApi 使用)。
|
||
/// </summary>
|
||
public sealed class EfTenantPackageRepository(TakeoutAdminDbContext context) : ITenantPackageRepository
|
||
{
|
||
/// <inheritdoc />
|
||
public async Task<TenantPackage?> GetByIdAsync(long tenantPackageId, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 查询套餐(排除已删除,无跟踪)
|
||
return await context.TenantPackages
|
||
.AsNoTracking()
|
||
.Where(p => p.Id == tenantPackageId && p.DeletedAt == null)
|
||
.FirstOrDefaultAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<TenantPackage?> GetByIdForUpdateAsync(long tenantPackageId, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 查询套餐(排除已删除,带跟踪用于更新)
|
||
return await context.TenantPackages
|
||
.Where(p => p.Id == tenantPackageId && p.DeletedAt == null)
|
||
.FirstOrDefaultAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<(IReadOnlyList<TenantPackage> Items, int TotalCount)> GetListAsync(
|
||
string? keyword,
|
||
bool? isActive,
|
||
int page,
|
||
int pageSize,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 构建基础查询
|
||
var query = context.TenantPackages
|
||
.AsNoTracking()
|
||
.Where(p => p.DeletedAt == null);
|
||
|
||
// 2. 应用关键字过滤
|
||
if (!string.IsNullOrWhiteSpace(keyword))
|
||
{
|
||
var normalized = keyword.Trim();
|
||
query = query.Where(p => p.Name.Contains(normalized));
|
||
}
|
||
|
||
// 3. 应用启用状态过滤
|
||
if (isActive.HasValue)
|
||
{
|
||
query = query.Where(p => p.IsActive == isActive.Value);
|
||
}
|
||
|
||
// 4. 获取总数
|
||
var totalCount = await query.CountAsync(cancellationToken);
|
||
|
||
// 5. 分页查询(按排序序号升序)
|
||
var items = await query
|
||
.OrderBy(p => p.SortOrder)
|
||
.ThenBy(p => p.Id)
|
||
.Skip((page - 1) * pageSize)
|
||
.Take(pageSize)
|
||
.ToListAsync(cancellationToken);
|
||
|
||
// 6. 返回结果
|
||
return (items, totalCount);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<IReadOnlyList<TenantPackageUsageResult>> GetUsagesAsync(
|
||
IReadOnlyList<long> tenantPackageIds,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 如果没有传入套餐 ID,返回空列表
|
||
if (tenantPackageIds.Count == 0)
|
||
{
|
||
return [];
|
||
}
|
||
|
||
// 2. 获取当前时间用于计算到期天数
|
||
var now = DateTime.UtcNow;
|
||
var in7Days = now.AddDays(7);
|
||
var in15Days = now.AddDays(15);
|
||
var in30Days = now.AddDays(30);
|
||
|
||
// 3. 查询订阅数据并按套餐分组统计
|
||
var subscriptions = await context.TenantSubscriptions
|
||
.AsNoTracking()
|
||
.Where(s => tenantPackageIds.Contains(s.TenantPackageId) && s.DeletedAt == null)
|
||
.Select(s => new
|
||
{
|
||
s.TenantPackageId,
|
||
s.TenantId,
|
||
s.Status,
|
||
s.EffectiveTo,
|
||
MonthlyPrice = context.TenantPackages
|
||
.Where(p => p.Id == s.TenantPackageId)
|
||
.Select(p => p.MonthlyPrice)
|
||
.FirstOrDefault()
|
||
})
|
||
.ToListAsync(cancellationToken);
|
||
|
||
// 4. 按套餐 ID 分组统计
|
||
var results = tenantPackageIds.Select(packageId =>
|
||
{
|
||
var packageSubscriptions = subscriptions.Where(s => s.TenantPackageId == packageId).ToList();
|
||
|
||
// 4.1 活跃订阅(状态为 Active 且未过期)
|
||
var activeSubscriptions = packageSubscriptions
|
||
.Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo > now)
|
||
.ToList();
|
||
|
||
// 4.2 活跃租户数(去重)
|
||
var activeTenantCount = activeSubscriptions.Select(s => s.TenantId).Distinct().Count();
|
||
|
||
// 4.3 总订阅数
|
||
var totalSubscriptionCount = packageSubscriptions.Count;
|
||
|
||
// 4.4 计算 MRR(月度经常性收入)
|
||
var mrr = activeSubscriptions.Sum(s => s.MonthlyPrice ?? 0);
|
||
|
||
// 4.5 计算 ARR(年度经常性收入)
|
||
var arr = mrr * 12;
|
||
|
||
// 4.6 计算到期租户数(基于活跃订阅)
|
||
var expiringIn7Days = activeSubscriptions.Count(s => s.EffectiveTo <= in7Days);
|
||
var expiringIn15Days = activeSubscriptions.Count(s => s.EffectiveTo <= in15Days);
|
||
var expiringIn30Days = activeSubscriptions.Count(s => s.EffectiveTo <= in30Days);
|
||
|
||
return new TenantPackageUsageResult(
|
||
packageId,
|
||
activeSubscriptions.Count,
|
||
activeTenantCount,
|
||
totalSubscriptionCount,
|
||
mrr,
|
||
arr,
|
||
expiringIn7Days,
|
||
expiringIn15Days,
|
||
expiringIn30Days);
|
||
}).ToList();
|
||
|
||
// 5. 返回结果
|
||
return results;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task AddAsync(TenantPackage package, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 添加套餐实体
|
||
await context.TenantPackages.AddAsync(package, cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task SoftDeleteAsync(TenantPackage package, CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 设置软删除时间
|
||
package.DeletedAt = DateTime.UtcNow;
|
||
|
||
// 2. 返回已完成任务
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 保存变更
|
||
await context.SaveChangesAsync(cancellationToken);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<(IReadOnlyList<TenantPackageTenantResult> Items, int TotalCount)> GetTenantsAsync(
|
||
long tenantPackageId,
|
||
string? keyword,
|
||
int? expiringWithinDays,
|
||
int page,
|
||
int pageSize,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
// 1. 获取当前时间
|
||
var now = DateTime.UtcNow;
|
||
|
||
// 2. 构建基础查询:活跃订阅(状态为 Active 且未过期)
|
||
var query = from s in context.TenantSubscriptions.AsNoTracking()
|
||
join t in context.Tenants.AsNoTracking() on s.TenantId equals t.Id
|
||
where s.TenantPackageId == tenantPackageId
|
||
&& s.DeletedAt == null
|
||
&& t.DeletedAt == null
|
||
&& s.Status == SubscriptionStatus.Active
|
||
&& s.EffectiveTo > now
|
||
select new { Subscription = s, Tenant = t };
|
||
|
||
// 3. 应用关键字过滤
|
||
if (!string.IsNullOrWhiteSpace(keyword))
|
||
{
|
||
var normalized = keyword.Trim();
|
||
query = query.Where(x => x.Tenant.Name.Contains(normalized) || x.Tenant.Code.Contains(normalized));
|
||
}
|
||
|
||
// 4. 应用到期天数筛选
|
||
if (expiringWithinDays.HasValue)
|
||
{
|
||
var expiryDate = now.AddDays(expiringWithinDays.Value);
|
||
query = query.Where(x => x.Subscription.EffectiveTo <= expiryDate);
|
||
}
|
||
|
||
// 5. 获取总数
|
||
var totalCount = await query.CountAsync(cancellationToken);
|
||
|
||
// 6. 分页查询(按到期时间升序,即将到期的排前面)
|
||
var items = await query
|
||
.OrderBy(x => x.Subscription.EffectiveTo)
|
||
.ThenBy(x => x.Tenant.Id)
|
||
.Skip((page - 1) * pageSize)
|
||
.Take(pageSize)
|
||
.Select(x => new TenantPackageTenantResult(
|
||
x.Tenant.Id,
|
||
x.Tenant.Code,
|
||
x.Tenant.Name,
|
||
x.Tenant.Status,
|
||
x.Tenant.ContactName,
|
||
x.Tenant.ContactPhone,
|
||
x.Subscription.EffectiveFrom,
|
||
x.Subscription.EffectiveTo))
|
||
.ToListAsync(cancellationToken);
|
||
|
||
// 7. 返回结果
|
||
return (items, totalCount);
|
||
}
|
||
}
|