feat(admin): 新增管理员角色、账单、订阅、套餐管理功能

- 新增 AdminRolesController 实现角色 CRUD 和权限管理
- 新增 BillingsController 实现账单查询功能
- 新增 SubscriptionsController 实现订阅管理功能
- 新增 TenantPackagesController 实现套餐管理功能
- 新增租户详情、配额使用、账单列表等查询功能
- 新增 TenantPackage、TenantSubscription 等领域实体
- 新增相关枚举:SubscriptionStatus、TenantPackageType 等
- 更新 appsettings 配置文件
- 更新权限授权策略提供者

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
MSuMshk
2026-02-02 09:11:44 +08:00
parent 54feee53b8
commit 0f900e108d
97 changed files with 7047 additions and 12 deletions

View File

@@ -0,0 +1,95 @@
using TakeoutSaaS.Domain.Billings.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Billings.Entities;
/// <summary>
/// 租户账单。
/// </summary>
public sealed class TenantBillingStatement : AuditableEntityBase
{
/// <summary>
/// 租户 ID雪花算法
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 账单编号。
/// </summary>
public string StatementNo { get; set; } = string.Empty;
/// <summary>
/// 账单类型。
/// </summary>
public TenantBillingType BillingType { get; set; }
/// <summary>
/// 账单周期开始时间。
/// </summary>
public DateTime PeriodStart { get; set; }
/// <summary>
/// 账单周期结束时间。
/// </summary>
public DateTime PeriodEnd { get; set; }
/// <summary>
/// 应付金额。
/// </summary>
public decimal AmountDue { get; set; }
/// <summary>
/// 已付金额。
/// </summary>
public decimal AmountPaid { get; set; }
/// <summary>
/// 折扣金额。
/// </summary>
public decimal DiscountAmount { get; set; }
/// <summary>
/// 税额。
/// </summary>
public decimal TaxAmount { get; set; }
/// <summary>
/// 货币代码。
/// </summary>
public string Currency { get; set; } = "CNY";
/// <summary>
/// 账单状态。
/// </summary>
public TenantBillingStatus Status { get; set; }
/// <summary>
/// 到期日期。
/// </summary>
public DateTime DueDate { get; set; }
/// <summary>
/// 账单明细 JSON。
/// </summary>
public string? LineItemsJson { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string? Notes { get; set; }
/// <summary>
/// 逾期通知时间。
/// </summary>
public DateTime? OverdueNotifiedAt { get; set; }
/// <summary>
/// 提醒发送时间。
/// </summary>
public DateTime? ReminderSentAt { get; set; }
/// <summary>
/// 关联订阅 ID。
/// </summary>
public long? SubscriptionId { get; set; }
}

View File

@@ -0,0 +1,75 @@
using TakeoutSaaS.Domain.Billings.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Billings.Entities;
/// <summary>
/// 租户支付记录。
/// </summary>
public sealed class TenantPayment : AuditableEntityBase
{
/// <summary>
/// 租户 ID雪花算法
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 账单 ID雪花算法
/// </summary>
public long BillingStatementId { get; set; }
/// <summary>
/// 支付金额。
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// 支付方式。
/// </summary>
public TenantPaymentMethod Method { get; set; }
/// <summary>
/// 支付状态。
/// </summary>
public TenantPaymentStatus Status { get; set; }
/// <summary>
/// 交易号。
/// </summary>
public string? TransactionNo { get; set; }
/// <summary>
/// 支付凭证 URL。
/// </summary>
public string? ProofUrl { get; set; }
/// <summary>
/// 支付时间。
/// </summary>
public DateTime? PaidAt { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string? Notes { get; set; }
/// <summary>
/// 审核人 ID。
/// </summary>
public long? VerifiedBy { get; set; }
/// <summary>
/// 审核时间。
/// </summary>
public DateTime? VerifiedAt { get; set; }
/// <summary>
/// 退款原因。
/// </summary>
public string? RefundReason { get; set; }
/// <summary>
/// 退款时间。
/// </summary>
public DateTime? RefundedAt { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace TakeoutSaaS.Domain.Billings.Enums;
/// <summary>
/// 租户账单状态。
/// </summary>
public enum TenantBillingStatus
{
/// <summary>
/// 待支付。
/// </summary>
Pending = 0,
/// <summary>
/// 已支付。
/// </summary>
Paid = 1,
/// <summary>
/// 已逾期。
/// </summary>
Overdue = 2,
/// <summary>
/// 已取消。
/// </summary>
Cancelled = 3
}

View File

@@ -0,0 +1,27 @@
namespace TakeoutSaaS.Domain.Billings.Enums;
/// <summary>
/// 租户账单类型。
/// </summary>
public enum TenantBillingType
{
/// <summary>
/// 订阅账单。
/// </summary>
Subscription = 0,
/// <summary>
/// 配额包购买。
/// </summary>
QuotaPurchase = 1,
/// <summary>
/// 手动创建。
/// </summary>
Manual = 2,
/// <summary>
/// 续费账单。
/// </summary>
Renewal = 3
}

View File

@@ -0,0 +1,22 @@
namespace TakeoutSaaS.Domain.Billings.Enums;
/// <summary>
/// 租户支付方式。
/// </summary>
public enum TenantPaymentMethod
{
/// <summary>
/// 在线支付。
/// </summary>
Online = 0,
/// <summary>
/// 银行转账。
/// </summary>
BankTransfer = 1,
/// <summary>
/// 其他方式。
/// </summary>
Other = 2
}

View File

@@ -0,0 +1,27 @@
namespace TakeoutSaaS.Domain.Billings.Enums;
/// <summary>
/// 租户支付状态。
/// </summary>
public enum TenantPaymentStatus
{
/// <summary>
/// 待处理。
/// </summary>
Pending = 0,
/// <summary>
/// 成功。
/// </summary>
Success = 1,
/// <summary>
/// 失败。
/// </summary>
Failed = 2,
/// <summary>
/// 已退款。
/// </summary>
Refunded = 3
}

View File

@@ -0,0 +1,128 @@
using TakeoutSaaS.Domain.Billings.Entities;
using TakeoutSaaS.Domain.Billings.Enums;
namespace TakeoutSaaS.Domain.Billings.Repositories;
/// <summary>
/// 账单仓储AdminApi 使用)。
/// </summary>
public interface IBillingRepository
{
/// <summary>
/// 获取账单列表(分页)。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="status">账单状态。</param>
/// <param name="billingType">账单类型。</param>
/// <param name="startDate">开始日期。</param>
/// <param name="endDate">结束日期。</param>
/// <param name="minAmount">最小金额。</param>
/// <param name="maxAmount">最大金额。</param>
/// <param name="keyword">关键词(账单号、租户名)。</param>
/// <param name="sortBy">排序字段。</param>
/// <param name="sortDesc">是否降序。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页条数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单列表和总数。</returns>
Task<(IReadOnlyList<BillingListResult> Items, int TotalCount)> GetListAsync(
long? tenantId,
TenantBillingStatus? status,
TenantBillingType? billingType,
DateTime? startDate,
DateTime? endDate,
decimal? minAmount,
decimal? maxAmount,
string? keyword,
string? sortBy,
bool? sortDesc,
int page,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 根据 ID 获取账单。
/// </summary>
/// <param name="billingId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体,不存在则返回 null。</returns>
Task<TenantBillingStatement?> GetByIdAsync(long billingId, CancellationToken cancellationToken = default);
/// <summary>
/// 根据 ID 获取账单(用于更新,带跟踪)。
/// </summary>
/// <param name="billingId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体,不存在则返回 null。</returns>
Task<TenantBillingStatement?> GetByIdForUpdateAsync(long billingId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取账单详情(带租户信息)。
/// </summary>
/// <param name="billingId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单详情结果,不存在则返回 null。</returns>
Task<BillingDetailResult?> GetDetailAsync(long billingId, CancellationToken cancellationToken = default);
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步操作任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 添加支付记录。
/// </summary>
/// <param name="payment">支付记录实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步操作任务。</returns>
Task AddPaymentAsync(TenantPayment payment, CancellationToken cancellationToken = default);
}
/// <summary>
/// 账单列表结果。
/// </summary>
public sealed record BillingListResult(
long Id,
long TenantId,
string? TenantName,
string StatementNo,
TenantBillingType BillingType,
DateTime PeriodStart,
DateTime PeriodEnd,
decimal AmountDue,
decimal AmountPaid,
decimal DiscountAmount,
decimal TaxAmount,
string Currency,
TenantBillingStatus Status,
DateTime DueDate,
DateTime? OverdueNotifiedAt,
DateTime CreatedAt,
DateTime? UpdatedAt);
/// <summary>
/// 账单详情结果。
/// </summary>
public sealed record BillingDetailResult(
long Id,
long TenantId,
string? TenantName,
string StatementNo,
TenantBillingType BillingType,
DateTime PeriodStart,
DateTime PeriodEnd,
decimal AmountDue,
decimal AmountPaid,
decimal DiscountAmount,
decimal TaxAmount,
string Currency,
TenantBillingStatus Status,
DateTime DueDate,
string? LineItemsJson,
string? Notes,
DateTime? OverdueNotifiedAt,
DateTime? ReminderSentAt,
DateTime CreatedAt,
DateTime? UpdatedAt);

View File

@@ -0,0 +1,100 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户套餐定义,描述不同等级的服务套餐。
/// </summary>
public sealed class TenantPackage : AuditableEntityBase
{
/// <summary>
/// 套餐名称。
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 套餐描述。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 套餐类型。
/// </summary>
public TenantPackageType PackageType { get; set; } = TenantPackageType.Free;
/// <summary>
/// 月付价格。
/// </summary>
public decimal? MonthlyPrice { get; set; }
/// <summary>
/// 年付价格。
/// </summary>
public decimal? YearlyPrice { get; set; }
/// <summary>
/// 最大门店数。
/// </summary>
public int? MaxStoreCount { get; set; }
/// <summary>
/// 最大账号数。
/// </summary>
public int? MaxAccountCount { get; set; }
/// <summary>
/// 最大存储空间GB
/// </summary>
public int? MaxStorageGb { get; set; }
/// <summary>
/// 最大短信额度。
/// </summary>
public int? MaxSmsCredits { get; set; }
/// <summary>
/// 最大配送订单数。
/// </summary>
public int? MaxDeliveryOrders { get; set; }
/// <summary>
/// 功能策略 JSON。
/// </summary>
public string? FeaturePoliciesJson { get; set; }
/// <summary>
/// 是否启用。
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 排序序号。
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 是否允许新租户购买。
/// </summary>
public bool IsAllowNewTenantPurchase { get; set; } = true;
/// <summary>
/// 是否公开可见。
/// </summary>
public bool IsPublicVisible { get; set; } = true;
/// <summary>
/// 发布状态。
/// </summary>
public TenantPackagePublishStatus PublishStatus { get; set; } = TenantPackagePublishStatus.Draft;
/// <summary>
/// 是否推荐。
/// </summary>
public bool IsRecommended { get; set; }
/// <summary>
/// 标签列表。
/// </summary>
public string[] Tags { get; set; } = [];
}

View File

@@ -0,0 +1,40 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户配额使用情况。
/// </summary>
public sealed class TenantQuotaUsage : AuditableEntityBase
{
/// <summary>
/// 租户 ID雪花算法
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 配额类型。
/// </summary>
public TenantQuotaType QuotaType { get; set; }
/// <summary>
/// 配额上限值。
/// </summary>
public decimal LimitValue { get; set; }
/// <summary>
/// 已使用值。
/// </summary>
public decimal UsedValue { get; set; }
/// <summary>
/// 重置周期(如 monthly、yearly
/// </summary>
public string? ResetCycle { get; set; }
/// <summary>
/// 上次重置时间。
/// </summary>
public DateTime? LastResetAt { get; set; }
}

View File

@@ -0,0 +1,60 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户订阅记录,描述租户当前的套餐订阅状态。
/// </summary>
public sealed class TenantSubscription : AuditableEntityBase
{
/// <summary>
/// 关联的租户 ID。
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 订阅的套餐 ID。
/// </summary>
public long TenantPackageId { get; set; }
/// <summary>
/// 订阅状态。
/// </summary>
public SubscriptionStatus Status { get; set; } = SubscriptionStatus.Active;
/// <summary>
/// 生效时间。
/// </summary>
public DateTime EffectiveFrom { get; set; }
/// <summary>
/// 到期时间。
/// </summary>
public DateTime EffectiveTo { get; set; }
/// <summary>
/// 下次计费日期。
/// </summary>
public DateTime? NextBillingDate { get; set; }
/// <summary>
/// 是否自动续费。
/// </summary>
public bool AutoRenew { get; set; }
/// <summary>
/// 预约变更的套餐 ID下个周期生效
/// </summary>
public long? ScheduledPackageId { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string? Notes { get; set; }
/// <summary>
/// 关联的套餐(导航属性)。
/// </summary>
public TenantPackage? TenantPackage { get; set; }
}

View File

@@ -0,0 +1,60 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户订阅变更历史记录。
/// </summary>
public sealed class TenantSubscriptionHistory : AuditableEntityBase
{
/// <summary>
/// 关联的租户 ID。
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 关联的订阅 ID。
/// </summary>
public long TenantSubscriptionId { get; set; }
/// <summary>
/// 变更前套餐 ID。
/// </summary>
public long FromPackageId { get; set; }
/// <summary>
/// 变更后套餐 ID。
/// </summary>
public long ToPackageId { get; set; }
/// <summary>
/// 变更类型。
/// </summary>
public SubscriptionChangeType ChangeType { get; set; }
/// <summary>
/// 生效时间。
/// </summary>
public DateTime EffectiveFrom { get; set; }
/// <summary>
/// 到期时间。
/// </summary>
public DateTime EffectiveTo { get; set; }
/// <summary>
/// 金额。
/// </summary>
public decimal? Amount { get; set; }
/// <summary>
/// 货币。
/// </summary>
public string? Currency { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string? Notes { get; set; }
}

View File

@@ -0,0 +1,95 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户认证资料,用于企业资质审核。
/// </summary>
public sealed class TenantVerificationProfile : AuditableEntityBase
{
/// <summary>
/// 关联的租户 ID。
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 认证状态。
/// </summary>
public TenantVerificationStatus Status { get; set; } = TenantVerificationStatus.Draft;
/// <summary>
/// 营业执照号。
/// </summary>
public string? BusinessLicenseNumber { get; set; }
/// <summary>
/// 营业执照图片 URL。
/// </summary>
public string? BusinessLicenseUrl { get; set; }
/// <summary>
/// 法人姓名。
/// </summary>
public string? LegalPersonName { get; set; }
/// <summary>
/// 法人身份证号。
/// </summary>
public string? LegalPersonIdNumber { get; set; }
/// <summary>
/// 法人身份证正面 URL。
/// </summary>
public string? LegalPersonIdFrontUrl { get; set; }
/// <summary>
/// 法人身份证背面 URL。
/// </summary>
public string? LegalPersonIdBackUrl { get; set; }
/// <summary>
/// 银行账户名。
/// </summary>
public string? BankAccountName { get; set; }
/// <summary>
/// 银行账号。
/// </summary>
public string? BankAccountNumber { get; set; }
/// <summary>
/// 开户银行。
/// </summary>
public string? BankName { get; set; }
/// <summary>
/// 附加数据 JSON。
/// </summary>
public string? AdditionalDataJson { get; set; }
/// <summary>
/// 提交时间。
/// </summary>
public DateTime? SubmittedAt { get; set; }
/// <summary>
/// 审核时间。
/// </summary>
public DateTime? ReviewedAt { get; set; }
/// <summary>
/// 审核人 ID。
/// </summary>
public long? ReviewedBy { get; set; }
/// <summary>
/// 审核人姓名。
/// </summary>
public string? ReviewedByName { get; set; }
/// <summary>
/// 审核备注。
/// </summary>
public string? ReviewRemarks { get; set; }
}

View File

@@ -0,0 +1,47 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 订阅变更类型枚举。
/// </summary>
public enum SubscriptionChangeType
{
/// <summary>
/// 新建订阅。
/// </summary>
Created = 0,
/// <summary>
/// 续费。
/// </summary>
Renewed = 1,
/// <summary>
/// 升级套餐。
/// </summary>
Upgraded = 2,
/// <summary>
/// 降级套餐。
/// </summary>
Downgraded = 3,
/// <summary>
/// 延期。
/// </summary>
Extended = 4,
/// <summary>
/// 取消。
/// </summary>
Cancelled = 5,
/// <summary>
/// 暂停。
/// </summary>
Suspended = 6,
/// <summary>
/// 恢复。
/// </summary>
Resumed = 7
}

View File

@@ -0,0 +1,27 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 订阅状态枚举。
/// </summary>
public enum SubscriptionStatus
{
/// <summary>
/// 活跃。
/// </summary>
Active = 0,
/// <summary>
/// 已过期。
/// </summary>
Expired = 1,
/// <summary>
/// 已取消。
/// </summary>
Cancelled = 2,
/// <summary>
/// 已暂停。
/// </summary>
Suspended = 3
}

View File

@@ -0,0 +1,17 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 套餐发布状态枚举。
/// </summary>
public enum TenantPackagePublishStatus
{
/// <summary>
/// 草稿。
/// </summary>
Draft = 0,
/// <summary>
/// 已发布。
/// </summary>
Published = 1
}

View File

@@ -0,0 +1,27 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 套餐类型枚举。
/// </summary>
public enum TenantPackageType
{
/// <summary>
/// 免费版。
/// </summary>
Free = 0,
/// <summary>
/// 标准版。
/// </summary>
Standard = 1,
/// <summary>
/// 专业版。
/// </summary>
Professional = 2,
/// <summary>
/// 企业版。
/// </summary>
Enterprise = 3
}

View File

@@ -0,0 +1,32 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 租户配额类型。
/// </summary>
public enum TenantQuotaType
{
/// <summary>
/// 门店数量。
/// </summary>
Store = 0,
/// <summary>
/// 账号数量。
/// </summary>
Account = 1,
/// <summary>
/// 存储空间GB
/// </summary>
StorageGb = 2,
/// <summary>
/// 短信额度。
/// </summary>
SmsCredits = 3,
/// <summary>
/// 配送订单数。
/// </summary>
DeliveryOrders = 4
}

View File

@@ -0,0 +1,27 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 租户认证状态枚举。
/// </summary>
public enum TenantVerificationStatus
{
/// <summary>
/// 草稿。
/// </summary>
Draft = 0,
/// <summary>
/// 待审核。
/// </summary>
Pending = 1,
/// <summary>
/// 已通过。
/// </summary>
Approved = 2,
/// <summary>
/// 已驳回。
/// </summary>
Rejected = 3
}

View File

@@ -0,0 +1,146 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 订阅仓储AdminApi 使用)。
/// </summary>
public interface ISubscriptionRepository
{
/// <summary>
/// 获取订阅列表(分页)。
/// </summary>
/// <param name="status">订阅状态。</param>
/// <param name="tenantPackageId">套餐 ID。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="tenantKeyword">租户关键词(名称或编码)。</param>
/// <param name="expiringWithinDays">即将到期天数筛选。</param>
/// <param name="autoRenew">是否自动续费。</param>
/// <param name="expireFrom">到期时间范围开始。</param>
/// <param name="expireTo">到期时间范围结束。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页条数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅列表和总数。</returns>
Task<(IReadOnlyList<SubscriptionListResult> Items, int TotalCount)> GetListAsync(
SubscriptionStatus? status,
long? tenantPackageId,
long? tenantId,
string? tenantKeyword,
int? expiringWithinDays,
bool? autoRenew,
DateTime? expireFrom,
DateTime? expireTo,
int page,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 根据 ID 获取订阅。
/// </summary>
/// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅实体,不存在则返回 null。</returns>
Task<TenantSubscription?> GetByIdAsync(long subscriptionId, CancellationToken cancellationToken = default);
/// <summary>
/// 根据 ID 获取订阅(用于更新,带跟踪)。
/// </summary>
/// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅实体,不存在则返回 null。</returns>
Task<TenantSubscription?> GetByIdForUpdateAsync(long subscriptionId, CancellationToken cancellationToken = default);
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步操作任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 获取订阅详情。
/// </summary>
/// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅详情结果,不存在则返回 null。</returns>
Task<SubscriptionDetailResult?> GetDetailAsync(long subscriptionId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取订阅变更历史。
/// </summary>
/// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>变更历史列表。</returns>
Task<IReadOnlyList<SubscriptionHistoryResult>> GetHistoriesAsync(long subscriptionId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取订阅列表结果(单条,用于更新后返回)。
/// </summary>
/// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅列表结果,不存在则返回 null。</returns>
Task<SubscriptionListResult?> GetListResultByIdAsync(long subscriptionId, CancellationToken cancellationToken = default);
}
/// <summary>
/// 订阅列表结果。
/// </summary>
public sealed record SubscriptionListResult(
long Id,
long TenantId,
string TenantName,
string TenantCode,
long TenantPackageId,
string PackageName,
long? ScheduledPackageId,
string? ScheduledPackageName,
SubscriptionStatus Status,
DateTime EffectiveFrom,
DateTime EffectiveTo,
DateTime? NextBillingDate,
bool AutoRenew,
string? Notes,
DateTime CreatedAt,
DateTime? UpdatedAt);
/// <summary>
/// 订阅详情结果。
/// </summary>
public sealed record SubscriptionDetailResult(
long Id,
long TenantId,
string TenantName,
string TenantCode,
long TenantPackageId,
string PackageName,
long? ScheduledPackageId,
string? ScheduledPackageName,
SubscriptionStatus Status,
DateTime EffectiveFrom,
DateTime EffectiveTo,
DateTime? NextBillingDate,
bool AutoRenew,
string? Notes,
DateTime CreatedAt,
DateTime? UpdatedAt,
TenantPackage Package,
TenantPackage? ScheduledPackage);
/// <summary>
/// 订阅变更历史结果。
/// </summary>
public sealed record SubscriptionHistoryResult(
long Id,
long SubscriptionId,
SubscriptionChangeType ChangeType,
long FromPackageId,
string FromPackageName,
long ToPackageId,
string ToPackageName,
DateTime EffectiveFrom,
DateTime EffectiveTo,
string? Notes,
DateTime CreatedAt,
long? CreatedBy);

View File

@@ -0,0 +1,120 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户套餐仓储AdminApi 使用)。
/// </summary>
public interface ITenantPackageRepository
{
/// <summary>
/// 根据 ID 获取租户套餐。
/// </summary>
/// <param name="tenantPackageId">套餐 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>套餐实体,不存在则返回 null。</returns>
Task<TenantPackage?> GetByIdAsync(long tenantPackageId, CancellationToken cancellationToken = default);
/// <summary>
/// 根据 ID 获取租户套餐(用于更新,带跟踪)。
/// </summary>
/// <param name="tenantPackageId">套餐 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>套餐实体,不存在则返回 null。</returns>
Task<TenantPackage?> GetByIdForUpdateAsync(long tenantPackageId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取租户套餐列表(分页)。
/// </summary>
/// <param name="keyword">关键字(套餐名称)。</param>
/// <param name="isActive">是否启用。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页条数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>套餐列表和总数。</returns>
Task<(IReadOnlyList<TenantPackage> Items, int TotalCount)> GetListAsync(
string? keyword,
bool? isActive,
int page,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 获取套餐使用统计。
/// </summary>
/// <param name="tenantPackageIds">套餐 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>套餐使用统计列表。</returns>
Task<IReadOnlyList<TenantPackageUsageResult>> GetUsagesAsync(
IReadOnlyList<long> tenantPackageIds,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增租户套餐。
/// </summary>
/// <param name="package">套餐实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步操作任务。</returns>
Task AddAsync(TenantPackage package, CancellationToken cancellationToken = default);
/// <summary>
/// 软删除租户套餐。
/// </summary>
/// <param name="package">套餐实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步操作任务。</returns>
Task SoftDeleteAsync(TenantPackage package, CancellationToken cancellationToken = default);
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步操作任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 获取套餐当前使用租户列表(按有效订阅口径)。
/// </summary>
/// <param name="tenantPackageId">套餐 ID。</param>
/// <param name="keyword">关键字(租户名称或编码)。</param>
/// <param name="expiringWithinDays">即将到期天数筛选。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页条数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>租户列表和总数。</returns>
Task<(IReadOnlyList<TenantPackageTenantResult> Items, int TotalCount)> GetTenantsAsync(
long tenantPackageId,
string? keyword,
int? expiringWithinDays,
int page,
int pageSize,
CancellationToken cancellationToken = default);
}
/// <summary>
/// 套餐使用统计结果。
/// </summary>
public sealed record TenantPackageUsageResult(
long TenantPackageId,
int ActiveSubscriptionCount,
int ActiveTenantCount,
int TotalSubscriptionCount,
decimal Mrr,
decimal Arr,
int ExpiringTenantCount7Days,
int ExpiringTenantCount15Days,
int ExpiringTenantCount30Days);
/// <summary>
/// 套餐使用租户结果。
/// </summary>
public sealed record TenantPackageTenantResult(
long TenantId,
string Code,
string Name,
TenantStatus Status,
string? ContactName,
string? ContactPhone,
DateTime SubscriptionEffectiveFrom,
DateTime SubscriptionEffectiveTo);

View File

@@ -1,3 +1,4 @@
using TakeoutSaaS.Domain.Billings.Entities;
using TakeoutSaaS.Domain.Tenants.Entities;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
@@ -30,4 +31,43 @@ public interface ITenantRepository
/// <param name="cancellationToken">取消标记。</param>
/// <returns>租户列表。</returns>
Task<IReadOnlyList<Tenant>> GetAllAsync(string? keyword, CancellationToken cancellationToken = default);
/// <summary>
/// 获取租户详情(包含认证、订阅、套餐信息)。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>租户详情元组,未找到返回 null。</returns>
Task<TenantDetailResult?> GetDetailAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取租户配额使用情况列表。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>配额使用情况列表。</returns>
Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取租户账单列表(分页)。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页条数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单列表和总数。</returns>
Task<(IReadOnlyList<TenantBillingStatement> Items, int TotalCount)> GetBillingsAsync(
long tenantId,
int page,
int pageSize,
CancellationToken cancellationToken = default);
}
/// <summary>
/// 租户详情查询结果。
/// </summary>
public sealed record TenantDetailResult(
Tenant Tenant,
TenantVerificationProfile? Verification,
TenantSubscription? Subscription,
TenantPackage? Package);