Files
TakeoutSaaS.AdminApi/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantPackageRepository.cs
MSuMshk 0f900e108d feat(admin): 新增管理员角色、账单、订阅、套餐管理功能
- 新增 AdminRolesController 实现角色 CRUD 和权限管理
- 新增 BillingsController 实现账单查询功能
- 新增 SubscriptionsController 实现订阅管理功能
- 新增 TenantPackagesController 实现套餐管理功能
- 新增租户详情、配额使用、账单列表等查询功能
- 新增 TenantPackage、TenantSubscription 等领域实体
- 新增相关枚举:SubscriptionStatus、TenantPackageType 等
- 更新 appsettings 配置文件
- 更新权限授权策略提供者

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 09:11:44 +08:00

235 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}