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:
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
100
src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPackage.cs
Normal file
100
src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantPackage.cs
Normal 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; } = [];
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Tenants.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 套餐发布状态枚举。
|
||||
/// </summary>
|
||||
public enum TenantPackagePublishStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 草稿。
|
||||
/// </summary>
|
||||
Draft = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 已发布。
|
||||
/// </summary>
|
||||
Published = 1
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user