refactor: 收敛租户领域至最小集

This commit is contained in:
2026-01-30 03:18:41 +00:00
parent 66aebabd87
commit 69a9adfc25
57 changed files with 6 additions and 4105 deletions

View File

@@ -1,34 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
/// <summary>
/// 租户账单分页查询请求QueryString 参数)。
/// </summary>
public sealed record SearchTenantBillsRequest
{
/// <summary>
/// 账单状态筛选。
/// </summary>
public TenantBillingStatus? Status { get; init; }
/// <summary>
/// 账单起始时间UTC筛选。
/// </summary>
public DateTime? From { get; init; }
/// <summary>
/// 账单结束时间UTC筛选。
/// </summary>
public DateTime? To { get; init; }
/// <summary>
/// 页码(从 1 开始)。
/// </summary>
public int Page { get; init; } = 1;
/// <summary>
/// 每页条数。
/// </summary>
public int PageSize { get; init; } = 20;
}

View File

@@ -75,14 +75,6 @@
"user_role" "user_role"
] ]
}, },
"Tenancy": {
"TenantIdHeaderName": "X-Tenant-Id",
"TenantCodeHeaderName": "X-Tenant-Code",
"IgnoredPaths": [
"/health"
],
"RootDomain": ""
},
"Storage": { "Storage": {
"Provider": "TencentCos", "Provider": "TencentCos",
"CdnBaseUrl": "https://image-admin.laosankeji.com", "CdnBaseUrl": "https://image-admin.laosankeji.com",
@@ -192,4 +184,3 @@
"UseConsoleExporter": true "UseConsoleExporter": true
} }
} }

View File

@@ -75,14 +75,6 @@
"user_role" "user_role"
] ]
}, },
"Tenancy": {
"TenantIdHeaderName": "X-Tenant-Id",
"TenantCodeHeaderName": "X-Tenant-Code",
"IgnoredPaths": [
"/health"
],
"RootDomain": ""
},
"Storage": { "Storage": {
"Provider": "TencentCos", "Provider": "TencentCos",
"CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com", "CdnBaseUrl": "https://saas2025-1388556178.cos.ap-beijing.myqcloud.com",
@@ -192,4 +184,3 @@
"UseConsoleExporter": true "UseConsoleExporter": true
} }
} }

View File

@@ -1,84 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 账单明细项(值对象)。
/// 用于记录账单中的单项费用明细,如套餐费用、配额包费用等。
/// </summary>
public sealed class BillingLineItem
{
/// <summary>
/// 明细项类型(如:套餐费、配额包、其他费用)。
/// </summary>
public string ItemType { get; set; } = string.Empty;
/// <summary>
/// 明细项描述。
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 数量。
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// 单价。
/// </summary>
public decimal UnitPrice { get; set; }
/// <summary>
/// 金额(数量 × 单价)。
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// 折扣率0-1 之间,如 0.1 表示 10% 折扣)。
/// </summary>
public decimal DiscountRate { get; set; }
/// <summary>
/// 创建账单明细项。
/// </summary>
/// <param name="itemType">明细项类型。</param>
/// <param name="description">描述。</param>
/// <param name="quantity">数量。</param>
/// <param name="unitPrice">单价。</param>
/// <param name="discountRate">折扣率。</param>
/// <returns>账单明细项实例。</returns>
public static BillingLineItem Create(
string itemType,
string description,
decimal quantity,
decimal unitPrice,
decimal discountRate = 0)
{
var amount = quantity * unitPrice * (1 - discountRate);
return new BillingLineItem
{
ItemType = itemType,
Description = description,
Quantity = quantity,
UnitPrice = unitPrice,
Amount = amount,
DiscountRate = discountRate
};
}
/// <summary>
/// 计算折扣后的金额。
/// </summary>
/// <returns>折扣后金额。</returns>
public decimal CalculateDiscountedAmount()
{
return Quantity * UnitPrice * (1 - DiscountRate);
}
/// <summary>
/// 获取折扣金额。
/// </summary>
/// <returns>折扣金额。</returns>
public decimal GetDiscountAmount()
{
return Quantity * UnitPrice * DiscountRate;
}
}

View File

@@ -1,45 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 配额包定义(平台提供的可购买配额包)。
/// </summary>
public sealed class QuotaPackage : AuditableEntityBase
{
/// <summary>
/// 配额包名称。
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 配额类型。
/// </summary>
public TenantQuotaType QuotaType { get; set; }
/// <summary>
/// 配额数值。
/// </summary>
public decimal QuotaValue { get; set; }
/// <summary>
/// 价格。
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// 是否上架。
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 排序。
/// </summary>
public int SortOrder { get; set; } = 0;
/// <summary>
/// 描述。
/// </summary>
public string? Description { get; set; }
}

View File

@@ -1,91 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户公告。
/// </summary>
public sealed class TenantAnnouncement : MultiTenantEntityBase
{
/// <summary>
/// 公告标题。
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 公告正文(可为 Markdown/HTML前端自行渲染
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// 公告类型。
/// </summary>
public TenantAnnouncementType AnnouncementType { get; set; } = TenantAnnouncementType.System;
/// <summary>
/// 展示优先级,数值越大越靠前。
/// </summary>
public int Priority { get; set; } = 0;
/// <summary>
/// 生效时间UTC
/// </summary>
public DateTime EffectiveFrom { get; set; } = DateTime.UtcNow;
/// <summary>
/// 失效时间UTC为空表示长期有效。
/// </summary>
public DateTime? EffectiveTo { get; set; }
/// <summary>
/// 发布者范围。
/// </summary>
public PublisherScope PublisherScope { get; set; }
/// <summary>
/// 发布者用户 ID平台或租户后台账号
/// </summary>
public long? PublisherUserId { get; set; }
/// <summary>
/// 公告状态。
/// </summary>
public AnnouncementStatus Status { get; set; } = AnnouncementStatus.Draft;
/// <summary>
/// 实际发布时间UTC
/// </summary>
public DateTime? PublishedAt { get; set; }
/// <summary>
/// 撤销时间UTC
/// </summary>
public DateTime? RevokedAt { get; set; }
/// <summary>
/// 预定发布时间UTC
/// </summary>
public DateTime? ScheduledPublishAt { get; set; }
/// <summary>
/// 目标受众类型。
/// </summary>
public string TargetType { get; set; } = string.Empty;
/// <summary>
/// 目标受众参数JSON
/// </summary>
public string? TargetParameters { get; set; }
/// <summary>
/// 并发控制字段。
/// </summary>
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
/// <summary>
/// 是否启用(已弃用,迁移期保留)。
/// </summary>
[Obsolete("Use Status instead.")]
public bool IsActive { get; set; } = true;
}

View File

@@ -1,24 +0,0 @@
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户公告已读记录。
/// </summary>
public sealed class TenantAnnouncementRead : MultiTenantEntityBase
{
/// <summary>
/// 公告 ID。
/// </summary>
public long AnnouncementId { get; set; }
/// <summary>
/// 已读用户 ID后台账号为空表示租户级已读。
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 已读时间。
/// </summary>
public DateTime ReadAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,50 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户运营审核日志。
/// </summary>
public sealed class TenantAuditLog : AuditableEntityBase
{
/// <summary>
/// 关联的租户标识。
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 操作类型。
/// </summary>
public TenantAuditAction Action { get; set; }
/// <summary>
/// 日志标题。
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 详细描述。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 操作人 ID。
/// </summary>
public long? OperatorId { get; set; }
/// <summary>
/// 操作人名称。
/// </summary>
public string? OperatorName { get; set; }
/// <summary>
/// 原状态。
/// </summary>
public TenantStatus? PreviousStatus { get; set; }
/// <summary>
/// 新状态。
/// </summary>
public TenantStatus? CurrentStatus { get; set; }
}

View File

@@ -1,200 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户账单,用于呈现周期性收费。
/// </summary>
public sealed class TenantBillingStatement : MultiTenantEntityBase
{
/// <summary>
/// 账单编号,供对账查询。
/// </summary>
public string StatementNo { get; set; } = string.Empty;
/// <summary>
/// 账单类型(订阅账单/配额包账单/手动账单/续费账单)。
/// </summary>
public BillingType BillingType { get; set; } = BillingType.Subscription;
/// <summary>
/// 关联的订阅 ID仅当 BillingType 为 Subscription 或 Renewal 时有值)。
/// </summary>
public long? SubscriptionId { 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 DiscountAmount { get; set; }
/// <summary>
/// 税费金额。
/// </summary>
public decimal TaxAmount { get; set; }
/// <summary>
/// 实付金额。
/// </summary>
public decimal AmountPaid { get; set; }
/// <summary>
/// 货币类型(默认 CNY
/// </summary>
public string Currency { get; set; } = "CNY";
/// <summary>
/// 当前付款状态。
/// </summary>
public TenantBillingStatus Status { get; set; } = TenantBillingStatus.Pending;
/// <summary>
/// 到期日。
/// </summary>
public DateTime DueDate { get; set; }
/// <summary>
/// 提醒发送时间(续费提醒、逾期提醒等)。
/// </summary>
public DateTime? ReminderSentAt { get; set; }
/// <summary>
/// 逾期通知时间。
/// </summary>
public DateTime? OverdueNotifiedAt { get; set; }
/// <summary>
/// 账单明细 JSON记录各项费用。
/// </summary>
public string? LineItemsJson { get; set; }
/// <summary>
/// 备注信息(如:人工备注、取消原因等)。
/// </summary>
public string? Notes { get; set; }
/// <summary>
/// 计算总金额(应付金额 - 折扣 + 税费)。
/// </summary>
/// <returns>总金额。</returns>
public decimal CalculateTotalAmount()
{
return AmountDue - DiscountAmount + TaxAmount;
}
/// <summary>
/// 标记为已支付(直接结清)。
/// </summary>
public void MarkAsPaid()
{
// 1. 计算剩余应付金额
var remainingAmount = CalculateTotalAmount() - AmountPaid;
// 2. 若已结清则直接返回
if (remainingAmount <= 0)
{
Status = TenantBillingStatus.Paid;
return;
}
// 3. 补足剩余金额并标记为已支付
MarkAsPaid(remainingAmount, string.Empty);
}
/// <summary>
/// 标记为已支付。
/// </summary>
/// <param name="amount">支付金额。</param>
/// <param name="transactionNo">交易号。</param>
public void MarkAsPaid(decimal amount, string transactionNo)
{
if (Status == TenantBillingStatus.Paid)
{
throw new InvalidOperationException("账单已经处于已支付状态,不能重复标记。");
}
if (Status == TenantBillingStatus.Cancelled)
{
throw new InvalidOperationException("已取消的账单不能标记为已支付。");
}
// 1. 累加支付金额
AmountPaid += amount;
// 2. 如果实付金额大于等于应付总额,则标记为已支付
if (AmountPaid >= CalculateTotalAmount())
{
Status = TenantBillingStatus.Paid;
}
}
/// <summary>
/// 标记为逾期。
/// </summary>
public void MarkAsOverdue()
{
// 1. 仅待支付账单允许标记逾期
if (Status != TenantBillingStatus.Pending)
{
return;
}
// 2. 未超过到期日则不处理
if (DateTime.UtcNow <= DueDate)
{
return;
}
// 3. 标记为逾期(通知时间由外部流程在发送通知时写入)
Status = TenantBillingStatus.Overdue;
}
/// <summary>
/// 取消账单。
/// </summary>
public void Cancel()
{
Cancel(null);
}
/// <summary>
/// 取消账单。
/// </summary>
/// <param name="reason">取消原因。</param>
public void Cancel(string? reason)
{
if (Status == TenantBillingStatus.Paid)
{
throw new InvalidOperationException("已支付的账单不能取消。");
}
if (Status == TenantBillingStatus.Cancelled)
{
throw new InvalidOperationException("账单已经处于取消状态。");
}
// 1. 变更状态
Status = TenantBillingStatus.Cancelled;
// 2. 记录取消原因(可选)
if (!string.IsNullOrWhiteSpace(reason))
{
Notes = string.IsNullOrWhiteSpace(Notes) ? $"[取消原因] {reason}" : $"{Notes}\n[取消原因] {reason}";
}
}
}

View File

@@ -1,45 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 面向租户的站内通知或消息推送。
/// </summary>
public sealed class TenantNotification : MultiTenantEntityBase
{
/// <summary>
/// 通知标题。
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 通知正文。
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 发布通道(站内、邮件、短信等)。
/// </summary>
public TenantNotificationChannel Channel { get; set; } = TenantNotificationChannel.InApp;
/// <summary>
/// 通知重要级别。
/// </summary>
public TenantNotificationSeverity Severity { get; set; } = TenantNotificationSeverity.Info;
/// <summary>
/// 推送时间。
/// </summary>
public DateTime SentAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// 租户是否已阅读。
/// </summary>
public DateTime? ReadAt { get; set; }
/// <summary>
/// 附加元数据 JSON。
/// </summary>
public string? MetadataJson { get; set; }
}

View File

@@ -1,100 +0,0 @@
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.Standard;
/// <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 bool IsPublicVisible { get; set; } = true;
/// <summary>
/// 是否允许新租户购买/选择(仅影响新购,不影响已订阅租户)。
/// </summary>
public bool IsAllowNewTenantPurchase { 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; } = [];
/// <summary>
/// 展示排序,数值越小越靠前。
/// </summary>
public int SortOrder { get; set; } = 0;
}

View File

@@ -1,158 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户支付记录。
/// </summary>
public sealed class TenantPayment : MultiTenantEntityBase
{
/// <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? RefundReason { get; set; }
/// <summary>
/// 退款时间。
/// </summary>
public DateTime? RefundedAt { get; set; }
/// <summary>
/// 审核人 ID管理员
/// </summary>
public long? VerifiedBy { get; set; }
/// <summary>
/// 审核时间。
/// </summary>
public DateTime? VerifiedAt { get; set; }
/// <summary>
/// 备注信息。
/// </summary>
public string? Notes { get; set; }
/// <summary>
/// 审核支付记录(确认支付有效性)。
/// </summary>
public void Verify()
{
if (Status != TenantPaymentStatus.Pending)
{
throw new InvalidOperationException("只有待审核的支付记录才能被审核。");
}
if (VerifiedAt.HasValue)
{
throw new InvalidOperationException("该支付记录已经被审核过。");
}
// 1. 标记为支付成功
Status = TenantPaymentStatus.Success;
// 2. 写入审核时间与支付时间
VerifiedAt = DateTime.UtcNow;
PaidAt ??= DateTime.UtcNow;
}
/// <summary>
/// 审核支付记录(确认支付有效性)。
/// </summary>
/// <param name="verifierId">审核人 ID。</param>
public void Verify(long verifierId)
{
Verify();
VerifiedBy = verifierId;
}
/// <summary>
/// 退款。
/// </summary>
public void Refund()
{
if (string.IsNullOrWhiteSpace(RefundReason))
{
throw new InvalidOperationException("退款原因不能为空。");
}
Refund(RefundReason);
}
/// <summary>
/// 退款。
/// </summary>
/// <param name="reason">退款原因。</param>
public void Refund(string reason)
{
if (Status == TenantPaymentStatus.Refunded)
{
throw new InvalidOperationException("该支付记录已经处于退款状态。");
}
if (Status != TenantPaymentStatus.Success)
{
throw new InvalidOperationException("只有支付成功的记录才能退款。");
}
// 1. 标记退款状态
Status = TenantPaymentStatus.Refunded;
// 2. 写入退款原因与退款时间
RefundReason = reason;
RefundedAt = DateTime.UtcNow;
}
/// <summary>
/// 拒绝支付(审核不通过)。
/// </summary>
/// <param name="verifierId">审核人 ID。</param>
/// <param name="reason">拒绝原因。</param>
public void Reject(long verifierId, string reason)
{
if (Status != TenantPaymentStatus.Pending)
{
throw new InvalidOperationException("只有待审核的支付记录才能被拒绝。");
}
Status = TenantPaymentStatus.Failed;
VerifiedBy = verifierId;
VerifiedAt = DateTime.UtcNow;
Notes = $"拒绝原因: {reason}";
}
}

View File

@@ -1,39 +0,0 @@
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户配额包购买记录。
/// </summary>
public sealed class TenantQuotaPackagePurchase : MultiTenantEntityBase
{
/// <summary>
/// 配额包 ID。
/// </summary>
public long QuotaPackageId { get; set; }
/// <summary>
/// 购买时的配额值。
/// </summary>
public decimal QuotaValue { get; set; }
/// <summary>
/// 购买价格。
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// 购买时间。
/// </summary>
public DateTime PurchasedAt { get; set; }
/// <summary>
/// 过期时间(可选)。
/// </summary>
public DateTime? ExpiredAt { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string? Notes { get; set; }
}

View File

@@ -1,35 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户配额使用情况快照。
/// </summary>
public sealed class TenantQuotaUsage : MultiTenantEntityBase
{
/// <summary>
/// 配额类型,例如门店数、短信条数等。
/// </summary>
public TenantQuotaType QuotaType { get; set; }
/// <summary>
/// 当前配额上限。
/// </summary>
public decimal LimitValue { get; set; }
/// <summary>
/// 已消耗的数量。
/// </summary>
public decimal UsedValue { get; set; }
/// <summary>
/// 配额刷新周期描述(如月、年)。
/// </summary>
public string? ResetCycle { get; set; }
/// <summary>
/// 最近一次重置时间。
/// </summary>
public DateTime? LastResetAt { get; set; }
}

View File

@@ -1,46 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户配额使用历史记录(用于追踪配额上下限与使用量的时间序列变化)。
/// </summary>
public sealed class TenantQuotaUsageHistory : MultiTenantEntityBase
{
/// <summary>
/// 配额类型。
/// </summary>
public TenantQuotaType QuotaType { get; set; }
/// <summary>
/// 已使用值(记录时刻的快照)。
/// </summary>
public decimal UsedValue { get; set; }
/// <summary>
/// 限额值(记录时刻的快照)。
/// </summary>
public decimal LimitValue { get; set; }
/// <summary>
/// 记录时间UTC
/// </summary>
public DateTime RecordedAt { get; set; }
/// <summary>
/// 变更类型。
/// </summary>
public TenantQuotaUsageHistoryChangeType ChangeType { get; set; } = TenantQuotaUsageHistoryChangeType.Snapshot;
/// <summary>
/// 变更量(可选)。
/// </summary>
public decimal? ChangeAmount { get; set; }
/// <summary>
/// 变更原因(可选)。
/// </summary>
public string? ChangeReason { get; set; }
}

View File

@@ -1,34 +0,0 @@
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户入驻审核领取记录(防止多管理员并发审核)。
/// </summary>
public sealed class TenantReviewClaim : AuditableEntityBase
{
/// <summary>
/// 被领取的租户 ID。
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 领取人用户 ID。
/// </summary>
public long ClaimedBy { get; set; }
/// <summary>
/// 领取人名称(展示用快照)。
/// </summary>
public string ClaimedByName { get; set; } = string.Empty;
/// <summary>
/// 领取时间UTC
/// </summary>
public DateTime ClaimedAt { get; set; }
/// <summary>
/// 释放时间UTC未释放时为 null。
/// </summary>
public DateTime? ReleasedAt { get; set; }
}

View File

@@ -1,50 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户套餐订阅记录。
/// </summary>
public sealed class TenantSubscription : MultiTenantEntityBase
{
/// <summary>
/// 当前订阅关联的套餐标识。
/// </summary>
public long TenantPackageId { get; set; }
/// <summary>
/// 订阅生效时间UTC
/// </summary>
public DateTime EffectiveFrom { get; set; }
/// <summary>
/// 订阅到期时间UTC
/// </summary>
public DateTime EffectiveTo { get; set; }
/// <summary>
/// 下一个计费时间,配合自动续费使用。
/// </summary>
public DateTime? NextBillingDate { get; set; }
/// <summary>
/// 订阅当前状态。
/// </summary>
public SubscriptionStatus Status { get; set; } = SubscriptionStatus.Pending;
/// <summary>
/// 是否开启自动续费。
/// </summary>
public bool AutoRenew { get; set; }
/// <summary>
/// 若已排期升降配,对应的新套餐 ID。
/// </summary>
public long? ScheduledPackageId { get; set; }
/// <summary>
/// 运营备注信息。
/// </summary>
public string? Notes { get; set; }
}

View File

@@ -1,60 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户套餐订阅变更记录。
/// </summary>
public sealed class TenantSubscriptionHistory : AuditableEntityBase
{
/// <summary>
/// 租户标识。
/// </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

@@ -1,95 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Tenants.Entities;
/// <summary>
/// 租户实名认证资料。
/// </summary>
public sealed class TenantVerificationProfile : AuditableEntityBase
{
/// <summary>
/// 对应的租户标识。
/// </summary>
public long TenantId { get; set; }
/// <summary>
/// 实名状态。
/// </summary>
public TenantVerificationStatus Status { get; set; } = TenantVerificationStatus.Draft;
/// <summary>
/// 营业执照编号。
/// </summary>
public string? BusinessLicenseNumber { get; set; }
/// <summary>
/// 营业执照文件地址。
/// </summary>
public string? BusinessLicenseUrl { get; set; }
/// <summary>
/// 法人姓名。
/// </summary>
public string? LegalPersonName { get; set; }
/// <summary>
/// 法人身份证号。
/// </summary>
public string? LegalPersonIdNumber { get; set; }
/// <summary>
/// 法人身份证正面。
/// </summary>
public string? LegalPersonIdFrontUrl { get; set; }
/// <summary>
/// 法人身份证反面。
/// </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

@@ -1,22 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 公告状态。
/// </summary>
public enum AnnouncementStatus
{
/// <summary>
/// 草稿。
/// </summary>
Draft = 0,
/// <summary>
/// 已发布。
/// </summary>
Published = 1,
/// <summary>
/// 已撤销。
/// </summary>
Revoked = 2
}

View File

@@ -1,22 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 账单导出格式。
/// </summary>
public enum BillingExportFormat
{
/// <summary>
/// Excel 格式(.xlsx
/// </summary>
Excel = 0,
/// <summary>
/// PDF 格式(.pdf
/// </summary>
Pdf = 1,
/// <summary>
/// CSV 格式(.csv
/// </summary>
Csv = 2
}

View File

@@ -1,27 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 账单类型。
/// </summary>
public enum BillingType
{
/// <summary>
/// 订阅账单(周期性订阅费用)。
/// </summary>
Subscription = 0,
/// <summary>
/// 配额包购买(一次性配额包购买)。
/// </summary>
QuotaPurchase = 1,
/// <summary>
/// 手动创建(管理员手动生成的账单)。
/// </summary>
Manual = 2,
/// <summary>
/// 续费账单(自动续费生成的账单)。
/// </summary>
Renewal = 3
}

View File

@@ -1,17 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 发布者范围。
/// </summary>
public enum PublisherScope
{
/// <summary>
/// 平台发布。
/// </summary>
Platform = 0,
/// <summary>
/// 租户发布。
/// </summary>
Tenant = 1
}

View File

@@ -1,27 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 套餐订阅的操作类型。
/// </summary>
public enum SubscriptionChangeType
{
/// <summary>
/// 新订阅。
/// </summary>
New = 0,
/// <summary>
/// 续费。
/// </summary>
Renew = 1,
/// <summary>
/// 升级套餐。
/// </summary>
Upgrade = 2,
/// <summary>
/// 降级套餐。
/// </summary>
Downgrade = 3
}

View File

@@ -1,32 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 订阅状态。
/// </summary>
public enum SubscriptionStatus
{
/// <summary>
/// 尚未支付或等待审批。
/// </summary>
Pending = 0,
/// <summary>
/// 订阅已生效。
/// </summary>
Active = 1,
/// <summary>
/// 已到期但仍保留数据。
/// </summary>
GracePeriod = 2,
/// <summary>
/// 已取消。
/// </summary>
Cancelled = 3,
/// <summary>
/// 因欠费被暂停。
/// </summary>
Suspended = 4
}

View File

@@ -1,52 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 租户公告类型。
/// </summary>
public enum TenantAnnouncementType
{
/// <summary>
/// 系统公告。
/// </summary>
System = 0,
/// <summary>
/// 账单/订阅相关提醒。
/// </summary>
Billing = 1,
/// <summary>
/// 运营通知。
/// </summary>
Operation = 2,
/// <summary>
/// 平台系统更新公告。
/// </summary>
SYSTEM_PLATFORM_UPDATE = 3,
/// <summary>
/// 系统安全公告。
/// </summary>
SYSTEM_SECURITY_NOTICE = 4,
/// <summary>
/// 系统合规公告。
/// </summary>
SYSTEM_COMPLIANCE = 5,
/// <summary>
/// 租户内部公告。
/// </summary>
TENANT_INTERNAL = 6,
/// <summary>
/// 租户财务公告。
/// </summary>
TENANT_FINANCE = 7,
/// <summary>
/// 租户运营公告。
/// </summary>
TENANT_OPERATION = 8
}

View File

@@ -1,67 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 租户运营审核动作。
/// </summary>
public enum TenantAuditAction
{
/// <summary>
/// 注册信息提交。
/// </summary>
RegistrationSubmitted = 1,
/// <summary>
/// 实名资料提交或更新。
/// </summary>
VerificationSubmitted = 2,
/// <summary>
/// 实名审核通过。
/// </summary>
VerificationApproved = 3,
/// <summary>
/// 实名审核驳回。
/// </summary>
VerificationRejected = 4,
/// <summary>
/// 订阅创建或续费。
/// </summary>
SubscriptionUpdated = 5,
/// <summary>
/// 套餐升降配。
/// </summary>
SubscriptionPlanChanged = 6,
/// <summary>
/// 租户状态变更(启用/停用/到期等)。
/// </summary>
StatusChanged = 7,
/// <summary>
/// 领取入驻审核。
/// </summary>
ReviewClaimed = 8,
/// <summary>
/// 强制接管入驻审核。
/// </summary>
ReviewForceClaimed = 9,
/// <summary>
/// 释放入驻审核(审核完成或手动释放)。
/// </summary>
ReviewClaimReleased = 10,
/// <summary>
/// 平台伪装登录租户。
/// </summary>
ImpersonatedLogin = 11,
/// <summary>
/// 生成主管理员重置链接。
/// </summary>
AdminResetLinkIssued = 12
}

View File

@@ -1,27 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 账单状态。
/// </summary>
public enum TenantBillingStatus
{
/// <summary>
/// 等待付款。
/// </summary>
Pending = 0,
/// <summary>
/// 已付款结清。
/// </summary>
Paid = 1,
/// <summary>
/// 已逾期。
/// </summary>
Overdue = 2,
/// <summary>
/// 已取消或作废。
/// </summary>
Cancelled = 3
}

View File

@@ -1,27 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 通知推送渠道。
/// </summary>
public enum TenantNotificationChannel
{
/// <summary>
/// 站内消息。
/// </summary>
InApp = 0,
/// <summary>
/// 邮件推送。
/// </summary>
Email = 1,
/// <summary>
/// 短信提醒。
/// </summary>
Sms = 2,
/// <summary>
/// 管理后台弹窗。
/// </summary>
Portal = 3
}

View File

@@ -1,22 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 租户通知的重要程度。
/// </summary>
public enum TenantNotificationSeverity
{
/// <summary>
/// 普通提示。
/// </summary>
Info = 0,
/// <summary>
/// 需要关注的提醒。
/// </summary>
Warning = 1,
/// <summary>
/// 影响业务的严重事件。
/// </summary>
Critical = 2
}

View File

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

View File

@@ -1,27 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 套餐类型枚举。
/// </summary>
public enum TenantPackageType
{
/// <summary>
/// 免费试用套餐。
/// </summary>
Trial = 0,
/// <summary>
/// 标准商业套餐。
/// </summary>
Standard = 1,
/// <summary>
/// 面向成长型商户的高级套餐。
/// </summary>
Professional = 2,
/// <summary>
/// 提供完整能力的旗舰套餐。
/// </summary>
Enterprise = 3
}

View File

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

View File

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

View File

@@ -1,37 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 配额类型,覆盖容量及调用次数。
/// </summary>
public enum TenantQuotaType
{
/// <summary>
/// 门店数量限制。
/// </summary>
StoreCount = 0,
/// <summary>
/// 员工账号数量限制。
/// </summary>
AccountCount = 1,
/// <summary>
/// 存储空间限制。
/// </summary>
Storage = 2,
/// <summary>
/// 短信额度。
/// </summary>
SmsCredits = 3,
/// <summary>
/// 配送订单数量限制。
/// </summary>
DeliveryOrders = 4,
/// <summary>
/// 营销活动并发数量。
/// </summary>
PromotionSlots = 5
}

View File

@@ -1,28 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Enums;
/// <summary>
/// 租户配额使用历史变更类型。
/// </summary>
public enum TenantQuotaUsageHistoryChangeType
{
/// <summary>
/// 初始化(首次建立配额基线)。
/// </summary>
Init = 0,
/// <summary>
/// 快照(定时或手动记录的当前状态)。
/// </summary>
Snapshot = 1,
/// <summary>
/// 增加(剩余可用额度增加,例如购买配额包、重置等)。
/// </summary>
Increase = 2,
/// <summary>
/// 减少(剩余可用额度减少,例如配额消耗等)。
/// </summary>
Decrease = 3
}

View File

@@ -1,27 +0,0 @@
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

@@ -1,22 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Events;
/// <summary>
/// 公告发布事件。
/// </summary>
public sealed class AnnouncementPublished
{
/// <summary>
/// 公告 ID。
/// </summary>
public long AnnouncementId { get; init; }
/// <summary>
/// 发布时间UTC
/// </summary>
public DateTime PublishedAt { get; init; }
/// <summary>
/// 目标受众类型。
/// </summary>
public string TargetType { get; init; } = string.Empty;
}

View File

@@ -1,17 +0,0 @@
namespace TakeoutSaaS.Domain.Tenants.Events;
/// <summary>
/// 公告撤销事件。
/// </summary>
public sealed class AnnouncementRevoked
{
/// <summary>
/// 公告 ID。
/// </summary>
public long AnnouncementId { get; init; }
/// <summary>
/// 撤销时间UTC
/// </summary>
public DateTime RevokedAt { get; init; }
}

View File

@@ -1,131 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 配额包仓储。
/// </summary>
public interface IQuotaPackageRepository
{
#region
/// <summary>
/// 按 ID 查找配额包。
/// </summary>
/// <param name="id">配额包 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>配额包实体,未找到返回 null。</returns>
Task<QuotaPackage?> FindByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// 分页查询配额包。
/// </summary>
/// <param name="quotaType">配额类型,为空不按类型过滤。</param>
/// <param name="isActive">启用状态,为空不按状态过滤。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页大小。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>分页数据与总数。</returns>
Task<(IReadOnlyList<QuotaPackage> Items, int Total)> SearchPagedAsync(
TenantQuotaType? quotaType,
bool? isActive,
int page,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增配额包。
/// </summary>
/// <param name="quotaPackage">配额包实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddAsync(QuotaPackage quotaPackage, CancellationToken cancellationToken = default);
/// <summary>
/// 更新配额包。
/// </summary>
/// <param name="quotaPackage">配额包实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateAsync(QuotaPackage quotaPackage, CancellationToken cancellationToken = default);
/// <summary>
/// 软删除配额包。
/// </summary>
/// <param name="id">配额包 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>删除成功返回 true未找到返回 false。</returns>
Task<bool> SoftDeleteAsync(long id, CancellationToken cancellationToken = default);
#endregion
#region
/// <summary>
/// 分页查询租户配额购买记录(包含配额包信息)。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页大小。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>分页数据与总数。</returns>
Task<(IReadOnlyList<(TenantQuotaPackagePurchase Purchase, QuotaPackage Package)> Items, int Total)> GetPurchasesPagedAsync(
long tenantId,
int page,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增配额购买记录。
/// </summary>
/// <param name="purchase">购买记录实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddPurchaseAsync(TenantQuotaPackagePurchase purchase, CancellationToken cancellationToken = default);
#endregion
#region 使
/// <summary>
/// 查询租户配额使用情况。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="quotaType">配额类型,为空查询全部。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>配额使用情况列表。</returns>
Task<IReadOnlyList<TenantQuotaUsage>> GetUsageByTenantAsync(
long tenantId,
TenantQuotaType? quotaType,
CancellationToken cancellationToken = default);
/// <summary>
/// 查找特定配额使用记录。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="quotaType">配额类型。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>配额使用记录,未找到返回 null。</returns>
Task<TenantQuotaUsage?> FindUsageAsync(
long tenantId,
TenantQuotaType quotaType,
CancellationToken cancellationToken = default);
/// <summary>
/// 更新配额使用情况。
/// </summary>
/// <param name="usage">配额使用实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateUsageAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default);
#endregion
/// <summary>
/// 持久化。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,112 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 统计数据仓储接口。
/// </summary>
public interface IStatisticsRepository
{
#region
/// <summary>
/// 获取所有订阅(用于统计)。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>所有订阅记录。</returns>
Task<IReadOnlyList<TenantSubscription>> GetAllSubscriptionsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 获取即将到期的订阅(含租户和套餐信息)。
/// </summary>
/// <param name="daysAhead">到期天数。</param>
/// <param name="onlyWithoutAutoRenew">是否仅查询未开启自动续费的。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>即将到期的订阅信息列表。</returns>
Task<IReadOnlyList<ExpiringSubscriptionInfo>> GetExpiringSubscriptionsAsync(
int daysAhead,
bool onlyWithoutAutoRenew,
CancellationToken cancellationToken = default);
#endregion
#region
/// <summary>
/// 获取所有已付款账单(用于收入统计)。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>已付款账单列表。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetPaidBillsAsync(CancellationToken cancellationToken = default);
#endregion
#region 使
/// <summary>
/// 获取配额使用排行(含租户信息)。
/// </summary>
/// <param name="quotaType">配额类型。</param>
/// <param name="topN">前 N 名。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>配额使用排行列表。</returns>
Task<IReadOnlyList<QuotaUsageRankInfo>> GetQuotaUsageRankingAsync(
TenantQuotaType quotaType,
int topN,
CancellationToken cancellationToken = default);
#endregion
}
/// <summary>
/// 即将到期的订阅信息(含关联数据)。
/// </summary>
public record ExpiringSubscriptionInfo
{
/// <summary>
/// 订阅实体。
/// </summary>
public required TenantSubscription Subscription { get; init; }
/// <summary>
/// 租户名称。
/// </summary>
public required string TenantName { get; init; }
/// <summary>
/// 套餐名称。
/// </summary>
public required string PackageName { get; init; }
}
/// <summary>
/// 配额使用排行信息(含租户名称)。
/// </summary>
public record QuotaUsageRankInfo
{
/// <summary>
/// 租户 ID。
/// </summary>
public long TenantId { get; init; }
/// <summary>
/// 租户名称。
/// </summary>
public required string TenantName { get; init; }
/// <summary>
/// 已使用值。
/// </summary>
public decimal UsedValue { get; init; }
/// <summary>
/// 限制值。
/// </summary>
public decimal LimitValue { get; init; }
/// <summary>
/// 使用百分比。
/// </summary>
public decimal UsagePercentage { get; init; }
}

View File

@@ -1,402 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 订阅管理仓储接口。
/// </summary>
public interface ISubscriptionRepository
{
#region
/// <summary>
/// 按 ID 查询订阅。
/// </summary>
/// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>订阅实体,未找到返回 null。</returns>
Task<TenantSubscription?> FindByIdAsync(
long subscriptionId,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
/// <summary>
/// 按 ID 列表批量查询订阅。
/// </summary>
/// <param name="subscriptionIds">订阅 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>订阅实体列表。</returns>
Task<IReadOnlyList<TenantSubscription>> FindByIdsAsync(
IEnumerable<long> subscriptionIds,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
/// <summary>
/// 分页查询订阅列表(含关联信息)。
/// </summary>
/// <param name="filter">查询过滤条件。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>分页结果。</returns>
Task<(IReadOnlyList<SubscriptionWithRelations> Items, int Total)> SearchPagedAsync(
SubscriptionSearchFilter filter,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
/// <summary>
/// 获取订阅详情(含关联信息)。
/// </summary>
/// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>订阅详情信息。</returns>
Task<SubscriptionDetailInfo?> GetDetailAsync(
long subscriptionId,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
/// <summary>
/// 按 ID 列表批量查询订阅(含租户信息)。
/// </summary>
/// <param name="subscriptionIds">订阅 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>订阅与租户信息列表。</returns>
Task<IReadOnlyList<SubscriptionWithTenant>> FindByIdsWithTenantAsync(
IEnumerable<long> subscriptionIds,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
/// <summary>
/// 查询自动续费候选订阅(活跃 + 开启自动续费 + 即将到期)。
/// </summary>
/// <param name="now">当前时间UTC。</param>
/// <param name="renewalThreshold">续费阈值时间UTC到期时间小于等于该时间视为候选。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>候选订阅集合(含套餐信息)。</returns>
Task<IReadOnlyList<AutoRenewalCandidate>> FindAutoRenewalCandidatesAsync(
DateTime now,
DateTime renewalThreshold,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
/// <summary>
/// 查询续费提醒候选订阅(活跃 + 未开启自动续费 + 到期时间落在指定日期范围)。
/// </summary>
/// <param name="startOfDay">筛选开始时间UTC。</param>
/// <param name="endOfDay">筛选结束时间UTC不含。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>候选订阅集合(含租户与套餐信息)。</returns>
Task<IReadOnlyList<RenewalReminderCandidate>> FindRenewalReminderCandidatesAsync(
DateTime startOfDay,
DateTime endOfDay,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
/// <summary>
/// 查询已到期仍处于 Active 的订阅(用于进入宽限期)。
/// </summary>
/// <param name="now">当前时间UTC。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>到期订阅集合。</returns>
Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync(
DateTime now,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
/// <summary>
/// 查询宽限期已结束的订阅(用于自动暂停)。
/// </summary>
/// <param name="now">当前时间UTC。</param>
/// <param name="gracePeriodDays">宽限期天数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>宽限期到期订阅集合。</returns>
Task<IReadOnlyList<TenantSubscription>> FindGracePeriodExpiredSubscriptionsAsync(
DateTime now,
int gracePeriodDays,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
#endregion
#region
/// <summary>
/// 按 ID 查询套餐。
/// </summary>
/// <param name="packageId">套餐 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>套餐实体,未找到返回 null。</returns>
Task<TenantPackage?> FindPackageByIdAsync(long packageId, CancellationToken cancellationToken = default);
#endregion
#region
/// <summary>
/// 更新订阅。
/// </summary>
/// <param name="subscription">订阅实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task UpdateAsync(TenantSubscription subscription, CancellationToken cancellationToken = default);
#endregion
#region
/// <summary>
/// 添加订阅变更历史。
/// </summary>
/// <param name="history">历史记录实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default);
/// <summary>
/// 获取订阅变更历史(含套餐名称)。
/// </summary>
/// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>历史记录列表。</returns>
Task<IReadOnlyList<SubscriptionHistoryWithPackageNames>> GetHistoryAsync(
long subscriptionId,
CancellationToken cancellationToken = default);
#endregion
#region 使
/// <summary>
/// 获取租户配额使用情况。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <param name="includeDeleted">是否包含已删除数据。</param>
/// <returns>配额使用列表。</returns>
Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync(
long tenantId,
CancellationToken cancellationToken = default,
bool includeDeleted = false);
#endregion
#region
/// <summary>
/// 添加租户通知。
/// </summary>
/// <param name="notification">通知实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddNotificationAsync(TenantNotification notification, CancellationToken cancellationToken = default);
#endregion
#region
/// <summary>
/// 添加操作日志。
/// </summary>
/// <param name="log">日志实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddOperationLogAsync(OperationLog log, CancellationToken cancellationToken = default);
#endregion
/// <summary>
/// 保存变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}
#region
/// <summary>
/// 订阅查询过滤条件。
/// </summary>
public record SubscriptionSearchFilter
{
/// <summary>
/// 订阅状态。
/// </summary>
public SubscriptionStatus? Status { get; init; }
/// <summary>
/// 套餐 ID。
/// </summary>
public long? TenantPackageId { get; init; }
/// <summary>
/// 租户 ID。
/// </summary>
public long? TenantId { get; init; }
/// <summary>
/// 租户关键词(名称或编码)。
/// </summary>
public string? TenantKeyword { get; init; }
/// <summary>
/// 即将到期天数。
/// </summary>
public int? ExpiringWithinDays { get; init; }
/// <summary>
/// 自动续费状态。
/// </summary>
public bool? AutoRenew { get; init; }
/// <summary>
/// 页码(从 1 开始)。
/// </summary>
public int Page { get; init; } = 1;
/// <summary>
/// 每页数量。
/// </summary>
public int PageSize { get; init; } = 20;
}
/// <summary>
/// 订阅及关联信息。
/// </summary>
public record SubscriptionWithRelations
{
/// <summary>
/// 订阅实体。
/// </summary>
public required TenantSubscription Subscription { get; init; }
/// <summary>
/// 租户名称。
/// </summary>
public required string TenantName { get; init; }
/// <summary>
/// 租户编码。
/// </summary>
public required string TenantCode { get; init; }
/// <summary>
/// 套餐名称。
/// </summary>
public required string PackageName { get; init; }
/// <summary>
/// 排期套餐名称(可选)。
/// </summary>
public string? ScheduledPackageName { get; init; }
}
/// <summary>
/// 订阅详情信息。
/// </summary>
public record SubscriptionDetailInfo
{
/// <summary>
/// 订阅实体。
/// </summary>
public required TenantSubscription Subscription { get; init; }
/// <summary>
/// 租户名称。
/// </summary>
public required string TenantName { get; init; }
/// <summary>
/// 租户编码。
/// </summary>
public required string TenantCode { get; init; }
/// <summary>
/// 当前套餐。
/// </summary>
public TenantPackage? Package { get; init; }
/// <summary>
/// 排期套餐。
/// </summary>
public TenantPackage? ScheduledPackage { get; init; }
}
/// <summary>
/// 订阅与租户信息。
/// </summary>
public record SubscriptionWithTenant
{
/// <summary>
/// 订阅实体。
/// </summary>
public required TenantSubscription Subscription { get; init; }
/// <summary>
/// 租户实体。
/// </summary>
public required Tenant Tenant { get; init; }
}
/// <summary>
/// 自动续费候选订阅信息。
/// </summary>
public sealed record AutoRenewalCandidate
{
/// <summary>
/// 订阅实体。
/// </summary>
public required TenantSubscription Subscription { get; init; }
/// <summary>
/// 当前套餐实体。
/// </summary>
public required TenantPackage Package { get; init; }
}
/// <summary>
/// 续费提醒候选订阅信息。
/// </summary>
public sealed record RenewalReminderCandidate
{
/// <summary>
/// 订阅实体。
/// </summary>
public required TenantSubscription Subscription { get; init; }
/// <summary>
/// 租户实体。
/// </summary>
public required Tenant Tenant { get; init; }
/// <summary>
/// 当前套餐实体。
/// </summary>
public required TenantPackage Package { get; init; }
}
/// <summary>
/// 订阅历史(含套餐名称)。
/// </summary>
public record SubscriptionHistoryWithPackageNames
{
/// <summary>
/// 历史记录实体。
/// </summary>
public required TenantSubscriptionHistory History { get; init; }
/// <summary>
/// 原套餐名称。
/// </summary>
public required string FromPackageName { get; init; }
/// <summary>
/// 目标套餐名称。
/// </summary>
public required string ToPackageName { get; init; }
}
#endregion

View File

@@ -1,79 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户公告仓储。
/// </summary>
public interface ITenantAnnouncementRepository
{
/// <summary>
/// 查询公告列表(包含平台公告 TenantId=0按类型、状态与生效时间筛选。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="keyword">关键词(标题/内容)。</param>
/// <param name="status">公告状态。</param>
/// <param name="type">公告类型。</param>
/// <param name="isActive">启用状态。</param>
/// <param name="effectiveFrom">生效开始时间筛选。</param>
/// <param name="effectiveTo">生效结束时间筛选。</param>
/// <param name="effectiveAt">生效时间点,为空不限制。</param>
/// <param name="orderByPriority">是否按优先级降序和生效时间降序排序,默认 false。</param>
/// <param name="limit">限制返回数量,为空不限制。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>公告集合。</returns>
Task<IReadOnlyList<TenantAnnouncement>> SearchAsync(
long tenantId,
string? keyword,
AnnouncementStatus? status,
TenantAnnouncementType? type,
bool? isActive,
DateTime? effectiveFrom,
DateTime? effectiveTo,
DateTime? effectiveAt,
bool orderByPriority = false,
int? limit = null,
CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 获取公告。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="announcementId">公告 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>公告实体或 null。</returns>
Task<TenantAnnouncement?> FindByIdAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增公告。
/// </summary>
/// <param name="announcement">公告实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddAsync(TenantAnnouncement announcement, CancellationToken cancellationToken = default);
/// <summary>
/// 更新公告。
/// </summary>
/// <param name="announcement">公告实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateAsync(TenantAnnouncement announcement, CancellationToken cancellationToken = default);
/// <summary>
/// 删除公告。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="announcementId">公告 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task DeleteAsync(long tenantId, long announcementId, CancellationToken cancellationToken = default);
/// <summary>
/// 保存变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,247 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户账单仓储。
/// </summary>
public interface ITenantBillingRepository
{
/// <summary>
/// 查询账单列表,按状态与时间范围筛选。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="status">账单状态。</param>
/// <param name="from">开始时间UTC。</param>
/// <param name="to">结束时间UTC。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单集合。</returns>
Task<IReadOnlyList<TenantBillingStatement>> SearchAsync(
long tenantId,
TenantBillingStatus? status,
DateTime? from,
DateTime? to,
CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 获取账单。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="billingId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体或 null。</returns>
Task<TenantBillingStatement?> FindByIdAsync(long tenantId, long billingId, CancellationToken cancellationToken = default);
/// <summary>
/// 按账单编号获取账单。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="statementNo">账单编号。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体或 null。</returns>
Task<TenantBillingStatement?> FindByStatementNoAsync(long tenantId, string statementNo, CancellationToken cancellationToken = default);
/// <summary>
/// 按账单编号获取账单(不限租户,管理员端使用)。
/// </summary>
/// <param name="statementNo">账单编号。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体或 null。</returns>
Task<TenantBillingStatement?> GetByStatementNoAsync(string statementNo, CancellationToken cancellationToken = default);
/// <summary>
/// 判断是否已存在指定周期开始时间的未取消账单(用于自动续费幂等)。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="periodStart">账单周期开始时间UTC。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>存在返回 true否则 false。</returns>
Task<bool> ExistsNotCancelledByPeriodStartAsync(
long tenantId,
DateTime periodStart,
CancellationToken cancellationToken = default);
/// <summary>
/// 获取逾期账单列表(已过到期日且未支付)。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>逾期账单集合。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetOverdueBillingsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 获取即将到期的账单列表(未来 N 天内到期且未支付)。
/// </summary>
/// <param name="daysAhead">提前天数。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>即将到期的账单集合。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetBillingsDueSoonAsync(int daysAhead, CancellationToken cancellationToken = default);
/// <summary>
/// 按租户 ID 获取账单列表。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单集合。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetByTenantIdAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 列表批量获取账单(管理员端/批量操作场景)。
/// </summary>
/// <param name="billingIds">账单 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体列表。</returns>
Task<IReadOnlyList<TenantBillingStatement>> GetByIdsAsync(
IReadOnlyCollection<long> billingIds,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增账单。
/// </summary>
/// <param name="bill">账单实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default);
/// <summary>
/// 更新账单。
/// </summary>
/// <param name="bill">账单实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateAsync(TenantBillingStatement bill, CancellationToken cancellationToken = default);
/// <summary>
/// 保存变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 管理员端分页查询账单列表(跨租户)。
/// </summary>
/// <param name="tenantId">租户 ID 筛选(可选)。</param>
/// <param name="status">账单状态筛选(可选)。</param>
/// <param name="from">开始时间UTC可选。</param>
/// <param name="to">结束时间UTC可选。</param>
/// <param name="minAmount">最小应付金额筛选(包含,可选)。</param>
/// <param name="maxAmount">最大应付金额筛选(包含,可选)。</param>
/// <param name="keyword">关键词搜索(账单号或租户名)。</param>
/// <param name="pageNumber">页码(从 1 开始)。</param>
/// <param name="pageSize">页大小。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单集合与总数。</returns>
Task<(IReadOnlyList<TenantBillingStatement> Items, int Total)> SearchPagedAsync(
long? tenantId,
TenantBillingStatus? status,
DateTime? from,
DateTime? to,
decimal? minAmount,
decimal? maxAmount,
string? keyword,
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 获取账单统计数据(用于报表与仪表盘)。
/// </summary>
/// <param name="tenantId">租户 ID可选管理员可查询所有租户。</param>
/// <param name="startDate">统计开始时间UTC。</param>
/// <param name="endDate">统计结束时间UTC。</param>
/// <param name="groupBy">分组方式Day/Week/Month。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>统计结果。</returns>
Task<TenantBillingStatistics> GetStatisticsAsync(
long? tenantId,
DateTime startDate,
DateTime endDate,
string groupBy,
CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 获取账单(不限租户,管理员端使用)。
/// </summary>
/// <param name="billingId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>账单实体或 null。</returns>
Task<TenantBillingStatement?> FindByIdAsync(long billingId, CancellationToken cancellationToken = default);
}
/// <summary>
/// 账单统计结果。
/// </summary>
public sealed record TenantBillingStatistics
{
/// <summary>
/// 总账单金额(统计区间内)。
/// </summary>
public decimal TotalAmount { get; init; }
/// <summary>
/// 已支付金额(统计区间内)。
/// </summary>
public decimal PaidAmount { get; init; }
/// <summary>
/// 未支付金额(统计区间内)。
/// </summary>
public decimal UnpaidAmount { get; init; }
/// <summary>
/// 逾期金额(统计区间内)。
/// </summary>
public decimal OverdueAmount { get; init; }
/// <summary>
/// 总账单数量(统计区间内)。
/// </summary>
public int TotalCount { get; init; }
/// <summary>
/// 已支付账单数量(统计区间内)。
/// </summary>
public int PaidCount { get; init; }
/// <summary>
/// 未支付账单数量(统计区间内)。
/// </summary>
public int UnpaidCount { get; init; }
/// <summary>
/// 逾期账单数量(统计区间内)。
/// </summary>
public int OverdueCount { get; init; }
/// <summary>
/// 趋势数据(按 groupBy 聚合)。
/// </summary>
public IReadOnlyList<TenantBillingTrendDataPoint> TrendData { get; init; } = [];
}
/// <summary>
/// 账单趋势统计点。
/// </summary>
public sealed record TenantBillingTrendDataPoint
{
/// <summary>
/// 分组时间点Day/Week/Month 的代表日期UTC
/// </summary>
public DateTime Period { get; init; }
/// <summary>
/// 账单数量。
/// </summary>
public int Count { get; init; }
/// <summary>
/// 总金额。
/// </summary>
public decimal TotalAmount { get; init; }
/// <summary>
/// 已支付金额。
/// </summary>
public decimal PaidAmount { get; init; }
}

View File

@@ -1,76 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户通知仓储。
/// </summary>
public interface ITenantNotificationRepository
{
/// <summary>
/// 查询通知列表,按等级、未读状态与时间范围筛选。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="severity">通知等级。</param>
/// <param name="unreadOnly">仅返回未读。</param>
/// <param name="from">开始时间UTC。</param>
/// <param name="to">结束时间UTC。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>通知集合。</returns>
Task<IReadOnlyList<TenantNotification>> SearchAsync(
long tenantId,
TenantNotificationSeverity? severity,
bool? unreadOnly,
DateTime? from,
DateTime? to,
CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 获取通知。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="notificationId">通知 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>通知实体或 null。</returns>
Task<TenantNotification?> FindByIdAsync(long tenantId, long notificationId, CancellationToken cancellationToken = default);
/// <summary>
/// 判断是否已发送过指定元数据的通知(用于幂等控制)。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="title">通知标题。</param>
/// <param name="metadataJson">元数据 JSON需与写入值完全一致。</param>
/// <param name="sentAfter">只在该时间之后发送的记录范围内判断UTC。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>存在返回 true否则 false。</returns>
Task<bool> ExistsByMetadataAsync(
long tenantId,
string title,
string metadataJson,
DateTime sentAfter,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增通知。
/// </summary>
/// <param name="notification">通知实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddAsync(TenantNotification notification, CancellationToken cancellationToken = default);
/// <summary>
/// 更新通知。
/// </summary>
/// <param name="notification">通知实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateAsync(TenantNotification notification, CancellationToken cancellationToken = default);
/// <summary>
/// 保存变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,64 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户套餐仓储。
/// </summary>
public interface ITenantPackageRepository
{
/// <summary>
/// 按套餐 ID 查询套餐。
/// </summary>
/// <param name="id">套餐 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>匹配的套餐实体,未找到返回 null。</returns>
Task<TenantPackage?> FindByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// 按关键词与启用状态搜索套餐。
/// </summary>
/// <param name="keyword">名称或描述关键字,空则不按关键字过滤。</param>
/// <param name="isActive">启用状态,空则不按状态过滤。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>符合条件的套餐列表。</returns>
Task<IReadOnlyList<TenantPackage>> SearchAsync(string? keyword, bool? isActive, CancellationToken cancellationToken = default);
/// <summary>
/// 查询公共可选购套餐(仅返回:已发布 + 对外可见 + 允许新购 + 启用)。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>公共可选购套餐列表。</returns>
Task<IReadOnlyList<TenantPackage>> SearchPublicPurchasableAsync(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 UpdateAsync(TenantPackage package, CancellationToken cancellationToken = default);
/// <summary>
/// 删除套餐。
/// </summary>
/// <param name="id">套餐 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task DeleteAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,64 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户支付记录仓储。
/// </summary>
public interface ITenantPaymentRepository
{
/// <summary>
/// 查询指定账单的支付记录列表。
/// </summary>
/// <param name="billingStatementId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>支付记录集合。</returns>
Task<IReadOnlyList<TenantPayment>> GetByBillingIdAsync(long billingStatementId, CancellationToken cancellationToken = default);
/// <summary>
/// 计算指定账单的累计已支付金额。
/// </summary>
/// <param name="billingStatementId">账单 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>累计已支付金额。</returns>
Task<decimal> GetTotalPaidAmountAsync(long billingStatementId, CancellationToken cancellationToken = default);
/// <summary>
/// 按 ID 获取支付记录。
/// </summary>
/// <param name="paymentId">支付记录 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>支付记录实体或 null。</returns>
Task<TenantPayment?> FindByIdAsync(long paymentId, CancellationToken cancellationToken = default);
/// <summary>
/// 按交易号获取支付记录。
/// </summary>
/// <param name="transactionNo">交易号。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>支付记录实体或 null。</returns>
Task<TenantPayment?> GetByTransactionNoAsync(string transactionNo, CancellationToken cancellationToken = default);
/// <summary>
/// 新增支付记录。
/// </summary>
/// <param name="payment">支付记录实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddAsync(TenantPayment payment, CancellationToken cancellationToken = default);
/// <summary>
/// 更新支付记录。
/// </summary>
/// <param name="payment">支付记录实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateAsync(TenantPayment payment, CancellationToken cancellationToken = default);
/// <summary>
/// 保存变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,23 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户配额使用历史仓储。
/// </summary>
public interface ITenantQuotaUsageHistoryRepository
{
/// <summary>
/// 新增历史记录。
/// </summary>
/// <param name="history">历史记录实体。</param>
/// <param name="cancellationToken">取消标记。</param>
Task AddAsync(TenantQuotaUsageHistory history, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,50 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary>
/// 租户配额使用仓储。
/// </summary>
public interface ITenantQuotaUsageRepository
{
/// <summary>
/// 获取租户指定配额的使用情况。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="quotaType">配额类型。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>配额使用记录,未初始化则返回 null。</returns>
Task<TenantQuotaUsage?> FindAsync(long tenantId, TenantQuotaType quotaType, CancellationToken cancellationToken = default);
/// <summary>
/// 按租户批量获取配额使用记录。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>该租户的所有配额使用记录。</returns>
Task<IReadOnlyList<TenantQuotaUsage>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增配额使用记录。
/// </summary>
/// <param name="usage">配额使用实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default);
/// <summary>
/// 更新配额使用记录。
/// </summary>
/// <param name="usage">配额使用实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateAsync(TenantQuotaUsage usage, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -1,10 +1,9 @@
using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Domain.Tenants.Repositories; namespace TakeoutSaaS.Domain.Tenants.Repositories;
/// <summary> /// <summary>
/// 租户聚合仓储 /// 租户只读仓储AdminApi 使用)
/// </summary> /// </summary>
public interface ITenantRepository public interface ITenantRepository
{ {
@@ -23,229 +22,4 @@ public interface ITenantRepository
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <returns>租户列表(仅返回找到的租户)。</returns> /// <returns>租户列表(仅返回找到的租户)。</returns>
Task<IReadOnlyList<Tenant>> FindByIdsAsync(IReadOnlyCollection<long> tenantIds, CancellationToken cancellationToken = default); Task<IReadOnlyList<Tenant>> FindByIdsAsync(IReadOnlyCollection<long> tenantIds, CancellationToken cancellationToken = default);
/// <summary>
/// 按状态与关键词查询租户列表。
/// </summary>
/// <param name="status">租户状态,为空不按状态过滤。</param>
/// <param name="keyword">名称或编码关键字,为空不按关键字过滤。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>符合条件的租户列表。</returns>
Task<IReadOnlyList<Tenant>> SearchAsync(
TenantStatus? status,
string? keyword,
CancellationToken cancellationToken = default);
/// <summary>
/// 分页查询租户(支持多条件过滤)。
/// </summary>
/// <param name="status">租户状态,为空不按状态过滤。</param>
/// <param name="verificationStatus">实名认证状态,为空不按认证状态过滤。</param>
/// <param name="name">租户名称,为空不按名称过滤。</param>
/// <param name="contactName">联系人姓名,为空不按联系人过滤。</param>
/// <param name="contactPhone">联系电话,为空不按电话过滤。</param>
/// <param name="keyword">兼容关键词:名称/编码/联系人/电话,为空不按关键字过滤。</param>
/// <param name="page">页码(从 1 开始)。</param>
/// <param name="pageSize">每页大小。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>分页数据与总数。</returns>
Task<(IReadOnlyList<Tenant> Items, int Total)> SearchPagedAsync(
TenantStatus? status,
TenantVerificationStatus? verificationStatus,
string? name,
string? contactName,
string? contactPhone,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增租户。
/// </summary>
/// <param name="tenant">租户实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddTenantAsync(Tenant tenant, CancellationToken cancellationToken = default);
/// <summary>
/// 更新租户。
/// </summary>
/// <param name="tenant">租户实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateTenantAsync(Tenant tenant, CancellationToken cancellationToken = default);
/// <summary>
/// 判断编码是否存在。
/// </summary>
/// <param name="code">租户编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>存在返回 true否则 false。</returns>
Task<bool> ExistsByCodeAsync(string code, CancellationToken cancellationToken = default);
/// <summary>
/// 判断租户名称是否存在(支持排除指定租户)。
/// </summary>
/// <param name="name">租户名称。</param>
/// <param name="excludeTenantId">排除的租户 ID用于更新场景。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>存在返回 true否则 false。</returns>
Task<bool> ExistsByNameAsync(string name, long? excludeTenantId = null, CancellationToken cancellationToken = default);
/// <summary>
/// 判断联系人手机号是否存在。
/// </summary>
/// <param name="phone">联系人手机号。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>存在返回 true否则 false。</returns>
Task<bool> ExistsByContactPhoneAsync(string phone, CancellationToken cancellationToken = default);
/// <summary>
/// 依据联系人手机号查询租户 ID。
/// </summary>
/// <param name="phone">联系人手机号。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>租户 ID未找到返回 null。</returns>
Task<long?> FindTenantIdByContactPhoneAsync(string phone, CancellationToken cancellationToken = default);
/// <summary>
/// 获取实名资料。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>实名资料实体,未提交返回 null。</returns>
Task<TenantVerificationProfile?> GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 批量获取实名资料。
/// </summary>
/// <param name="tenantIds">租户 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>实名资料列表(未提交的不返回)。</returns>
Task<IReadOnlyList<TenantVerificationProfile>> GetVerificationProfilesAsync(
IReadOnlyCollection<long> tenantIds,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增或更新实名资料。
/// </summary>
/// <param name="profile">实名资料实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpsertVerificationProfileAsync(TenantVerificationProfile profile, CancellationToken cancellationToken = default);
/// <summary>
/// 获取当前审核领取信息(仅返回未释放的记录)。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>领取记录,未领取返回 null。</returns>
Task<TenantReviewClaim?> GetActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 查询当前审核领取信息(用于更新,返回可跟踪实体)。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>领取记录,未领取返回 null。</returns>
Task<TenantReviewClaim?> FindActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增审核领取记录。
/// </summary>
/// <param name="claim">领取记录。</param>
/// <param name="auditLog">领取动作审计日志。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>新增成功返回 true若已被其他人领取导致冲突则返回 false。</returns>
Task<bool> TryAddReviewClaimAsync(TenantReviewClaim claim, TenantAuditLog auditLog, CancellationToken cancellationToken = default);
/// <summary>
/// 更新审核领取记录。
/// </summary>
/// <param name="claim">领取记录。</param>
/// <param name="cancellationToken">取消标记。</param>
Task UpdateReviewClaimAsync(TenantReviewClaim claim, CancellationToken cancellationToken = default);
/// <summary>
/// 获取当前订阅。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>当前有效订阅,若无则 null。</returns>
Task<TenantSubscription?> GetActiveSubscriptionAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 批量获取当前订阅。
/// </summary>
/// <param name="tenantIds">租户 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅列表(可能包含同一租户的多条订阅记录)。</returns>
Task<IReadOnlyList<TenantSubscription>> GetSubscriptionsAsync(
IReadOnlyCollection<long> tenantIds,
CancellationToken cancellationToken = default);
/// <summary>
/// 依据订阅 ID 查询。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="subscriptionId">订阅 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅实体,未找到返回 null。</returns>
Task<TenantSubscription?> FindSubscriptionByIdAsync(long tenantId, long subscriptionId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增订阅。
/// </summary>
/// <param name="subscription">订阅实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default);
/// <summary>
/// 更新订阅。
/// </summary>
/// <param name="subscription">订阅实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task UpdateSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default);
/// <summary>
/// 记录订阅历史。
/// </summary>
/// <param name="history">订阅历史实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddSubscriptionHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default);
/// <summary>
/// 获取订阅历史。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>订阅历史列表。</returns>
Task<IReadOnlyList<TenantSubscriptionHistory>> GetSubscriptionHistoryAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增审核日志。
/// </summary>
/// <param name="log">审核日志实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task AddAuditLogAsync(TenantAuditLog log, CancellationToken cancellationToken = default);
/// <summary>
/// 查询审核日志。
/// </summary>
/// <param name="tenantId">租户 ID雪花算法。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>审核日志列表。</returns>
Task<IReadOnlyList<TenantAuditLog>> GetAuditLogsAsync(long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
} }

View File

@@ -1,64 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
namespace TakeoutSaaS.Domain.Tenants.Services;
/// <summary>
/// 账单领域服务接口。
/// 负责处理账单生成、账单编号生成、逾期处理等跨实体的业务逻辑。
/// </summary>
public interface IBillingDomainService
{
/// <summary>
/// 根据订阅信息生成账单。
/// </summary>
/// <param name="subscription">租户订阅信息。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>生成的账单实体。</returns>
Task<TenantBillingStatement> GenerateSubscriptionBillingAsync(
TenantSubscription subscription,
CancellationToken cancellationToken = default);
/// <summary>
/// 根据配额包购买信息生成账单。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="quotaPackage">配额包信息。</param>
/// <param name="quantity">购买数量。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>生成的账单实体。</returns>
Task<TenantBillingStatement> GenerateQuotaPurchaseBillingAsync(
long tenantId,
QuotaPackage quotaPackage,
int quantity,
CancellationToken cancellationToken = default);
/// <summary>
/// 生成唯一的账单编号。
/// 格式示例BIL-20251217-000001
/// </summary>
/// <returns>账单编号。</returns>
string GenerateStatementNo();
/// <summary>
/// 处理逾期账单(批量标记逾期状态)。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>处理的账单数量。</returns>
Task<int> ProcessOverdueBillingsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 计算账单总金额(含折扣和税费)。
/// </summary>
/// <param name="baseAmount">基础金额。</param>
/// <param name="discountAmount">折扣金额。</param>
/// <param name="taxAmount">税费金额。</param>
/// <returns>总金额。</returns>
decimal CalculateTotalAmount(decimal baseAmount, decimal discountAmount, decimal taxAmount);
/// <summary>
/// 验证账单状态是否可以进行支付操作。
/// </summary>
/// <param name="billing">账单实体。</param>
/// <returns>是否可以支付。</returns>
bool CanProcessPayment(TenantBillingStatement billing);
}

View File

@@ -1,33 +0,0 @@
using TakeoutSaaS.Domain.Tenants.Entities;
namespace TakeoutSaaS.Domain.Tenants.Services;
/// <summary>
/// 账单导出服务接口。
/// </summary>
public interface IBillingExportService
{
/// <summary>
/// 导出为 ExcelXLSX
/// </summary>
/// <param name="billings">账单数据。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>文件字节数组。</returns>
Task<byte[]> ExportToExcelAsync(IReadOnlyList<TenantBillingStatement> billings, CancellationToken cancellationToken = default);
/// <summary>
/// 导出为 PDF。
/// </summary>
/// <param name="billings">账单数据。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>文件字节数组。</returns>
Task<byte[]> ExportToPdfAsync(IReadOnlyList<TenantBillingStatement> billings, CancellationToken cancellationToken = default);
/// <summary>
/// 导出为 CSV。
/// </summary>
/// <param name="billings">账单数据。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>文件字节数组。</returns>
Task<byte[]> ExportToCsvAsync(IReadOnlyList<TenantBillingStatement> billings, CancellationToken cancellationToken = default);
}

View File

@@ -1,51 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
namespace TakeoutSaaS.Infrastructure.App.Persistence.Configurations;
/// <summary>
/// <see cref="TenantBillingStatement"/> EF Core 映射配置。
/// </summary>
public sealed class TenantBillingStatementConfiguration : IEntityTypeConfiguration<TenantBillingStatement>
{
/// <inheritdoc />
public void Configure(EntityTypeBuilder<TenantBillingStatement> builder)
{
builder.ToTable("tenant_billing_statements");
builder.HasKey(x => x.Id);
// 1. 字段约束
builder.Property(x => x.StatementNo).HasMaxLength(64).IsRequired();
builder.Property(x => x.BillingType).HasConversion<int>();
builder.Property(x => x.AmountDue).HasPrecision(18, 2);
builder.Property(x => x.DiscountAmount).HasPrecision(18, 2);
builder.Property(x => x.TaxAmount).HasPrecision(18, 2);
builder.Property(x => x.AmountPaid).HasPrecision(18, 2);
builder.Property(x => x.Currency).HasMaxLength(8).HasDefaultValue("CNY");
builder.Property(x => x.Status).HasConversion<int>();
// 2. JSON 字段(当前以 text 存储 JSON 字符串,便于兼容历史迁移)
builder.Property(x => x.LineItemsJson).HasColumnType("text");
// 3. 备注字段
builder.Property(x => x.Notes).HasMaxLength(512);
// 4. 唯一约束与索引
builder.HasIndex(x => new { x.TenantId, x.StatementNo }).IsUnique();
// 5. 性能索引(高频查询:租户+状态+到期日)
builder.HasIndex(x => new { x.TenantId, x.Status, x.DueDate })
.HasDatabaseName("idx_billing_tenant_status_duedate");
// 6. 逾期扫描索引(仅索引 Pending/Overdue
builder.HasIndex(x => new { x.Status, x.DueDate })
.HasDatabaseName("idx_billing_status_duedate")
.HasFilter($"\"Status\" IN ({(int)TenantBillingStatus.Pending}, {(int)TenantBillingStatus.Overdue})");
// 7. 创建时间索引(支持列表倒序)
builder.HasIndex(x => x.CreatedAt)
.HasDatabaseName("idx_billing_created_at");
}
}

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using TakeoutSaaS.Domain.Tenants.Entities;
namespace TakeoutSaaS.Infrastructure.App.Persistence.Configurations;
/// <summary>
/// <see cref="TenantPayment"/> EF Core 映射配置。
/// </summary>
public sealed class TenantPaymentConfiguration : IEntityTypeConfiguration<TenantPayment>
{
/// <inheritdoc />
public void Configure(EntityTypeBuilder<TenantPayment> builder)
{
builder.ToTable("tenant_payments");
builder.HasKey(x => x.Id);
// 1. 字段约束
builder.Property(x => x.BillingStatementId).IsRequired();
builder.Property(x => x.Amount).HasPrecision(18, 2).IsRequired();
builder.Property(x => x.Method).HasConversion<int>();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.TransactionNo).HasMaxLength(64);
builder.Property(x => x.ProofUrl).HasMaxLength(512);
builder.Property(x => x.RefundReason).HasMaxLength(512);
builder.Property(x => x.Notes).HasMaxLength(512);
// 2. 复合索引:租户+账单
builder.HasIndex(x => new { x.TenantId, x.BillingStatementId });
// 3. 支付记录时间排序索引
builder.HasIndex(x => new { x.BillingStatementId, x.PaidAt })
.HasDatabaseName("idx_payment_billing_paidat");
// 4. 交易号索引(部分索引:仅非空)
builder.HasIndex(x => x.TransactionNo)
.HasDatabaseName("idx_payment_transaction_no")
.HasFilter("\"TransactionNo\" IS NOT NULL");
}
}

View File

@@ -20,11 +20,9 @@ using TakeoutSaaS.Domain.Reservations.Entities;
using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Entities;
using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Enums;
using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Infrastructure.Common.Persistence;
using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Ids;
using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Infrastructure.App.Persistence.Configurations;
namespace TakeoutSaaS.Infrastructure.App.Persistence; namespace TakeoutSaaS.Infrastructure.App.Persistence;
@@ -42,62 +40,6 @@ public class TakeoutAppDbContext(
/// </summary> /// </summary>
public DbSet<Tenant> Tenants => Set<Tenant>(); public DbSet<Tenant> Tenants => Set<Tenant>();
/// <summary> /// <summary>
/// 租户套餐。
/// </summary>
public DbSet<TenantPackage> TenantPackages => Set<TenantPackage>();
/// <summary>
/// 租户订阅。
/// </summary>
public DbSet<TenantSubscription> TenantSubscriptions => Set<TenantSubscription>();
/// <summary>
/// 租户订阅历史。
/// </summary>
public DbSet<TenantSubscriptionHistory> TenantSubscriptionHistories => Set<TenantSubscriptionHistory>();
/// <summary>
/// 租户配额使用记录。
/// </summary>
public DbSet<TenantQuotaUsage> TenantQuotaUsages => Set<TenantQuotaUsage>();
/// <summary>
/// 租户配额使用历史记录。
/// </summary>
public DbSet<TenantQuotaUsageHistory> TenantQuotaUsageHistories => Set<TenantQuotaUsageHistory>();
/// <summary>
/// 租户账单。
/// </summary>
public DbSet<TenantBillingStatement> TenantBillingStatements => Set<TenantBillingStatement>();
/// <summary>
/// 租户支付记录。
/// </summary>
public DbSet<TenantPayment> TenantPayments => Set<TenantPayment>();
/// <summary>
/// 租户通知。
/// </summary>
public DbSet<TenantNotification> TenantNotifications => Set<TenantNotification>();
/// <summary>
/// 租户公告。
/// </summary>
public DbSet<TenantAnnouncement> TenantAnnouncements => Set<TenantAnnouncement>();
/// <summary>
/// 租户公告已读记录。
/// </summary>
public DbSet<TenantAnnouncementRead> TenantAnnouncementReads => Set<TenantAnnouncementRead>();
/// <summary>
/// 租户认证资料。
/// </summary>
public DbSet<TenantVerificationProfile> TenantVerificationProfiles => Set<TenantVerificationProfile>();
/// <summary>
/// 租户审核领取记录。
/// </summary>
public DbSet<TenantReviewClaim> TenantReviewClaims => Set<TenantReviewClaim>();
/// <summary>
/// 配额包定义。
/// </summary>
public DbSet<QuotaPackage> QuotaPackages => Set<QuotaPackage>();
/// <summary>
/// 租户配额包购买记录。
/// </summary>
public DbSet<TenantQuotaPackagePurchase> TenantQuotaPackagePurchases => Set<TenantQuotaPackagePurchase>();
/// <summary>
/// 商户实体。 /// 商户实体。
/// </summary> /// </summary>
public DbSet<Merchant> Merchants => Set<Merchant>(); public DbSet<Merchant> Merchants => Set<Merchant>();
@@ -401,20 +343,6 @@ public class TakeoutAppDbContext(
ConfigureTenant(modelBuilder.Entity<Tenant>()); ConfigureTenant(modelBuilder.Entity<Tenant>());
ConfigureMerchant(modelBuilder.Entity<Merchant>()); ConfigureMerchant(modelBuilder.Entity<Merchant>());
ConfigureStore(modelBuilder.Entity<Store>()); ConfigureStore(modelBuilder.Entity<Store>());
ConfigureTenantPackage(modelBuilder.Entity<TenantPackage>());
ConfigureTenantSubscription(modelBuilder.Entity<TenantSubscription>());
ConfigureTenantSubscriptionHistory(modelBuilder.Entity<TenantSubscriptionHistory>());
ConfigureTenantQuotaUsage(modelBuilder.Entity<TenantQuotaUsage>());
ConfigureTenantQuotaUsageHistory(modelBuilder.Entity<TenantQuotaUsageHistory>());
ConfigureTenantBilling(modelBuilder.Entity<TenantBillingStatement>());
ConfigureTenantPayment(modelBuilder.Entity<TenantPayment>());
ConfigureTenantNotification(modelBuilder.Entity<TenantNotification>());
ConfigureTenantAnnouncement(modelBuilder.Entity<TenantAnnouncement>());
ConfigureTenantAnnouncementRead(modelBuilder.Entity<TenantAnnouncementRead>());
ConfigureTenantVerificationProfile(modelBuilder.Entity<TenantVerificationProfile>());
ConfigureTenantReviewClaim(modelBuilder.Entity<TenantReviewClaim>());
ConfigureQuotaPackage(modelBuilder.Entity<QuotaPackage>());
ConfigureTenantQuotaPackagePurchase(modelBuilder.Entity<TenantQuotaPackagePurchase>());
ConfigureMerchantDocument(modelBuilder.Entity<MerchantDocument>()); ConfigureMerchantDocument(modelBuilder.Entity<MerchantDocument>());
ConfigureMerchantContract(modelBuilder.Entity<MerchantContract>()); ConfigureMerchantContract(modelBuilder.Entity<MerchantContract>());
ConfigureMerchantStaff(modelBuilder.Entity<MerchantStaff>()); ConfigureMerchantStaff(modelBuilder.Entity<MerchantStaff>());
@@ -502,51 +430,6 @@ public class TakeoutAppDbContext(
builder.HasIndex(x => x.ContactPhone).IsUnique(); builder.HasIndex(x => x.ContactPhone).IsUnique();
} }
private static void ConfigureTenantVerificationProfile(EntityTypeBuilder<TenantVerificationProfile> builder)
{
builder.ToTable("tenant_verification_profiles");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantId).IsRequired();
builder.Property(x => x.BusinessLicenseNumber).HasMaxLength(64);
builder.Property(x => x.BusinessLicenseUrl).HasMaxLength(512);
builder.Property(x => x.LegalPersonName).HasMaxLength(64);
builder.Property(x => x.LegalPersonIdNumber).HasMaxLength(32);
builder.Property(x => x.LegalPersonIdFrontUrl).HasMaxLength(512);
builder.Property(x => x.LegalPersonIdBackUrl).HasMaxLength(512);
builder.Property(x => x.BankAccountName).HasMaxLength(128);
builder.Property(x => x.BankAccountNumber).HasMaxLength(64);
builder.Property(x => x.BankName).HasMaxLength(128);
builder.Property(x => x.ReviewRemarks).HasMaxLength(512);
builder.Property(x => x.ReviewedByName).HasMaxLength(64);
builder.HasIndex(x => x.TenantId).IsUnique();
}
private static void ConfigureTenantReviewClaim(EntityTypeBuilder<TenantReviewClaim> builder)
{
builder.ToTable("tenant_review_claims");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantId).IsRequired();
builder.Property(x => x.ClaimedBy).IsRequired();
builder.Property(x => x.ClaimedByName).HasMaxLength(64).IsRequired();
builder.Property(x => x.ClaimedAt).IsRequired();
builder.Property(x => x.ReleasedAt);
builder.HasIndex(x => x.TenantId);
builder.HasIndex(x => x.ClaimedBy);
builder.HasIndex(x => x.TenantId).IsUnique().HasFilter("\"ReleasedAt\" IS NULL AND \"DeletedAt\" IS NULL");
}
private static void ConfigureTenantSubscriptionHistory(EntityTypeBuilder<TenantSubscriptionHistory> builder)
{
builder.ToTable("tenant_subscription_histories");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantId).IsRequired();
builder.Property(x => x.TenantSubscriptionId).IsRequired();
builder.Property(x => x.Notes).HasMaxLength(512);
builder.Property(x => x.Currency).HasMaxLength(8);
builder.HasIndex(x => new { x.TenantId, x.TenantSubscriptionId });
}
private static void ConfigureMerchant(EntityTypeBuilder<Merchant> builder) private static void ConfigureMerchant(EntityTypeBuilder<Merchant> builder)
{ {
builder.ToTable("merchants"); builder.ToTable("merchants");
@@ -774,141 +657,6 @@ public class TakeoutAppDbContext(
builder.HasIndex(x => new { x.TenantId, x.OrderId }).IsUnique(); builder.HasIndex(x => new { x.TenantId, x.OrderId }).IsUnique();
} }
private static void ConfigureTenantPackage(EntityTypeBuilder<TenantPackage> builder)
{
builder.ToTable("tenant_packages");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.Description).HasMaxLength(512);
builder.Property(x => x.FeaturePoliciesJson).HasColumnType("text");
builder.Property(x => x.PublishStatus)
.HasConversion<int>()
.HasDefaultValue(TenantPackagePublishStatus.Draft)
.HasSentinel((TenantPackagePublishStatus)(-1))
.HasComment("发布状态0=草稿1=已发布。");
// 1. 解决 EF Core 默认值哨兵问题:当我们希望插入 false/0 时,若数据库配置了 default 且 EF 认为该值是“未设置”,会导致 insert 省略列,最终落库为默认值。
// 2. 发布状态使用 -1 作为哨兵,避免 Draft=0 被误判为“未设置”而触发数据库默认值(发布/草稿切换必须可控)。
// 3. 将布尔开关哨兵值设置为数据库默认值true 作为哨兵false 才会被显式写入,从而保证“可见性/可售开关”在新增时可正确落库。
builder.Property(x => x.IsPublicVisible)
.HasDefaultValue(true)
.HasSentinel(true)
.HasComment("是否对外可见(展示页/套餐列表可见性)。");
builder.Property(x => x.IsAllowNewTenantPurchase)
.HasDefaultValue(true)
.HasSentinel(true)
.HasComment("是否允许新租户购买/选择(仅影响新购)。");
// 4. 展示配置:推荐标识与标签(用于套餐展示页/对比页)
builder.Property(x => x.IsRecommended)
.HasDefaultValue(false)
.HasComment("是否推荐展示(运营推荐标识)。");
builder.Property(x => x.Tags)
.HasColumnType("text[]")
.HasComment("套餐标签(用于展示与对比页)。");
builder.Property(x => x.SortOrder).HasDefaultValue(0).HasComment("展示排序,数值越小越靠前。");
builder.HasIndex(x => new { x.IsActive, x.SortOrder });
builder.HasIndex(x => new { x.PublishStatus, x.IsActive, x.IsPublicVisible, x.IsAllowNewTenantPurchase, x.SortOrder });
}
private static void ConfigureTenantSubscription(EntityTypeBuilder<TenantSubscription> builder)
{
builder.ToTable("tenant_subscriptions");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantPackageId).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.TenantPackageId });
}
private static void ConfigureTenantQuotaUsage(EntityTypeBuilder<TenantQuotaUsage> builder)
{
builder.ToTable("tenant_quota_usages");
builder.HasKey(x => x.Id);
builder.Property(x => x.QuotaType).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.QuotaType }).IsUnique();
}
private static void ConfigureTenantQuotaUsageHistory(EntityTypeBuilder<TenantQuotaUsageHistory> builder)
{
builder.ToTable("tenant_quota_usage_histories");
builder.HasKey(x => x.Id);
builder.Property(x => x.QuotaType).HasConversion<int>();
builder.Property(x => x.ChangeType).HasConversion<int>();
builder.Property(x => x.ChangeReason).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.QuotaType, x.RecordedAt });
builder.HasIndex(x => new { x.TenantId, x.RecordedAt });
}
private static void ConfigureTenantBilling(EntityTypeBuilder<TenantBillingStatement> builder)
{
new TenantBillingStatementConfiguration().Configure(builder);
}
private static void ConfigureTenantPayment(EntityTypeBuilder<TenantPayment> builder)
{
new TenantPaymentConfiguration().Configure(builder);
}
private static void ConfigureTenantNotification(EntityTypeBuilder<TenantNotification> builder)
{
builder.ToTable("tenant_notifications");
builder.HasKey(x => x.Id);
builder.Property(x => x.Title).HasMaxLength(128).IsRequired();
builder.Property(x => x.Message).HasMaxLength(1024).IsRequired();
builder.Property(x => x.Channel).HasConversion<int>();
builder.Property(x => x.Severity).HasConversion<int>();
builder.Property(x => x.MetadataJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.Channel, x.SentAt });
}
private static void ConfigureTenantAnnouncement(EntityTypeBuilder<TenantAnnouncement> builder)
{
builder.ToTable("tenant_announcements");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantId).IsRequired();
builder.Property(x => x.Title).HasMaxLength(128).IsRequired();
builder.Property(x => x.Content).HasColumnType("text").IsRequired();
builder.Property(x => x.AnnouncementType)
.HasConversion<int>();
builder.Property(x => x.PublisherScope)
.HasConversion<int>();
builder.Property(x => x.PublisherUserId);
builder.Property(x => x.Status)
.HasConversion<int>();
builder.Property(x => x.PublishedAt);
builder.Property(x => x.RevokedAt);
builder.Property(x => x.ScheduledPublishAt);
builder.Property(x => x.TargetType).HasMaxLength(64).IsRequired();
builder.Property(x => x.TargetParameters).HasColumnType("text");
builder.Property(x => x.Priority).IsRequired();
builder.Property<bool>("IsActive").IsRequired();
builder.Property(x => x.RowVersion)
.IsRowVersion()
.IsConcurrencyToken()
.HasColumnType("bytea");
ConfigureAuditableEntity(builder);
ConfigureSoftDeleteEntity(builder);
builder.HasIndex("TenantId", "AnnouncementType", "IsActive");
builder.HasIndex(x => new { x.TenantId, x.EffectiveFrom, x.EffectiveTo });
builder.HasIndex(x => new { x.TenantId, x.Status, x.EffectiveFrom });
builder.HasIndex(x => new { x.Status, x.EffectiveFrom })
.HasFilter("\"TenantId\" = 0");
}
private static void ConfigureTenantAnnouncementRead(EntityTypeBuilder<TenantAnnouncementRead> builder)
{
builder.ToTable("tenant_announcement_reads");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantId).IsRequired();
builder.Property(x => x.AnnouncementId).IsRequired();
builder.Property(x => x.UserId);
builder.Property(x => x.ReadAt).IsRequired();
ConfigureAuditableEntity(builder);
ConfigureSoftDeleteEntity(builder);
builder.HasIndex(x => new { x.TenantId, x.AnnouncementId, x.UserId }).IsUnique();
}
private static void ConfigureMerchantDocument(EntityTypeBuilder<MerchantDocument> builder) private static void ConfigureMerchantDocument(EntityTypeBuilder<MerchantDocument> builder)
{ {
builder.ToTable("merchant_documents"); builder.ToTable("merchant_documents");
@@ -1538,30 +1286,4 @@ public class TakeoutAppDbContext(
builder.HasIndex(x => new { x.TenantId, x.MetricDefinitionId, x.Severity }); builder.HasIndex(x => new { x.TenantId, x.MetricDefinitionId, x.Severity });
} }
private static void ConfigureQuotaPackage(EntityTypeBuilder<QuotaPackage> builder)
{
builder.ToTable("quota_packages");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.QuotaType).HasConversion<int>().IsRequired();
builder.Property(x => x.QuotaValue).HasPrecision(18, 2).IsRequired();
builder.Property(x => x.Price).HasPrecision(18, 2).IsRequired();
builder.Property(x => x.IsActive).IsRequired();
builder.Property(x => x.SortOrder).HasDefaultValue(0);
builder.Property(x => x.Description).HasMaxLength(512);
builder.HasIndex(x => new { x.QuotaType, x.IsActive, x.SortOrder });
}
private static void ConfigureTenantQuotaPackagePurchase(EntityTypeBuilder<TenantQuotaPackagePurchase> builder)
{
builder.ToTable("tenant_quota_package_purchases");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantId).IsRequired();
builder.Property(x => x.QuotaPackageId).IsRequired();
builder.Property(x => x.QuotaValue).HasPrecision(18, 2).IsRequired();
builder.Property(x => x.Price).HasPrecision(18, 2).IsRequired();
builder.Property(x => x.PurchasedAt).IsRequired();
builder.Property(x => x.Notes).HasMaxLength(512);
builder.HasIndex(x => new { x.TenantId, x.QuotaPackageId, x.PurchasedAt });
}
} }

View File

@@ -1,21 +1,19 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Npgsql;
using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Infrastructure.App.Persistence; using TakeoutSaaS.Infrastructure.App.Persistence;
using TakeoutSaaS.Infrastructure.Logs.Persistence;
namespace TakeoutSaaS.Infrastructure.App.Repositories; namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary> /// <summary>
/// 租户聚合的 EF Core 仓储实现 /// 租户只读仓储实现AdminApi 使用)
/// </summary> /// </summary>
public sealed class EfTenantRepository(TakeoutAdminDbContext context, TakeoutLogsDbContext logsContext) : ITenantRepository public sealed class EfTenantRepository(TakeoutAdminDbContext context) : ITenantRepository
{ {
/// <inheritdoc /> /// <inheritdoc />
public Task<Tenant?> FindByIdAsync(long tenantId, CancellationToken cancellationToken = default) public Task<Tenant?> FindByIdAsync(long tenantId, CancellationToken cancellationToken = default)
{ {
// 1. 只读查询租户(跨租户)
return context.Tenants return context.Tenants
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == tenantId, cancellationToken); .FirstOrDefaultAsync(x => x.Id == tenantId, cancellationToken);
@@ -24,381 +22,16 @@ public sealed class EfTenantRepository(TakeoutAdminDbContext context, TakeoutLog
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<Tenant>> FindByIdsAsync(IReadOnlyCollection<long> tenantIds, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<Tenant>> FindByIdsAsync(IReadOnlyCollection<long> tenantIds, CancellationToken cancellationToken = default)
{ {
// 1. tenantIds 为空直接返回
if (tenantIds.Count == 0) if (tenantIds.Count == 0)
{ {
return Array.Empty<Tenant>(); return Array.Empty<Tenant>();
} }
// 2. (空行后) 批量查询租户
return await context.Tenants return await context.Tenants
.AsNoTracking() .AsNoTracking()
.Where(x => tenantIds.Contains(x.Id)) .Where(x => tenantIds.Contains(x.Id))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
/// <inheritdoc />
public async Task<IReadOnlyList<Tenant>> SearchAsync(
TenantStatus? status,
string? keyword,
CancellationToken cancellationToken = default)
{
// 1. 构建基础查询
var query = context.Tenants.AsNoTracking();
// 2. 按状态过滤
if (status.HasValue)
{
query = query.Where(x => x.Status == status.Value);
}
// 3. 按关键字过滤
if (!string.IsNullOrWhiteSpace(keyword))
{
keyword = keyword.Trim();
query = query.Where(x =>
EF.Functions.ILike(x.Name, $"%{keyword}%") ||
EF.Functions.ILike(x.Code, $"%{keyword}%") ||
EF.Functions.ILike(x.ContactName ?? string.Empty, $"%{keyword}%") ||
EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{keyword}%"));
}
// 4. 排序返回
return await query
.OrderByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<(IReadOnlyList<Tenant> Items, int Total)> SearchPagedAsync(
TenantStatus? status,
TenantVerificationStatus? verificationStatus,
string? name,
string? contactName,
string? contactPhone,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default)
{
var query = context.Tenants.AsNoTracking();
// 1. 按租户状态过滤
if (status.HasValue)
{
query = query.Where(x => x.Status == status.Value);
}
// 2. 按实名认证状态过滤(未提交视为 Draft
if (verificationStatus.HasValue)
{
query = from tenant in query
join profile in context.TenantVerificationProfiles.AsNoTracking()
on tenant.Id equals profile.TenantId into profiles
from profile in profiles.DefaultIfEmpty()
where (profile == null ? TenantVerificationStatus.Draft : profile.Status) == verificationStatus.Value
select tenant;
}
// 3. 按名称/联系人/电话过滤(模糊匹配)
if (!string.IsNullOrWhiteSpace(name))
{
var normalizedName = name.Trim();
query = query.Where(x => EF.Functions.ILike(x.Name, $"%{normalizedName}%"));
}
// 4. 按联系人过滤(模糊匹配)
if (!string.IsNullOrWhiteSpace(contactName))
{
var normalizedContactName = contactName.Trim();
query = query.Where(x => EF.Functions.ILike(x.ContactName ?? string.Empty, $"%{normalizedContactName}%"));
}
// 5. 按联系电话过滤(模糊匹配)
if (!string.IsNullOrWhiteSpace(contactPhone))
{
var normalizedContactPhone = contactPhone.Trim();
query = query.Where(x => EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{normalizedContactPhone}%"));
}
// 6. 兼容关键字查询:名称/编码/联系人/电话
if (!string.IsNullOrWhiteSpace(keyword))
{
var normalizedKeyword = keyword.Trim();
query = query.Where(x =>
EF.Functions.ILike(x.Name, $"%{normalizedKeyword}%") ||
EF.Functions.ILike(x.Code, $"%{normalizedKeyword}%") ||
EF.Functions.ILike(x.ContactName ?? string.Empty, $"%{normalizedKeyword}%") ||
EF.Functions.ILike(x.ContactPhone ?? string.Empty, $"%{normalizedKeyword}%"));
}
// 7. 先统计总数,再按创建时间倒序分页
var total = await query.CountAsync(cancellationToken);
// 8. 查询当前页数据
var items = await query
.OrderByDescending(x => x.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (items, total);
}
/// <inheritdoc />
public Task AddTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
{
return context.Tenants.AddAsync(tenant, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
{
context.Tenants.Update(tenant);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<bool> ExistsByCodeAsync(string code, CancellationToken cancellationToken = default)
{
var normalized = code.Trim();
return context.Tenants.AnyAsync(x => x.Code == normalized, cancellationToken);
}
/// <inheritdoc />
public Task<bool> ExistsByNameAsync(string name, long? excludeTenantId = null, CancellationToken cancellationToken = default)
{
// 1. 标准化名称
var normalized = name.Trim();
// 2. 构建查询(名称使用 ILike 做不区分大小写的等值匹配)
var query = context.Tenants
.AsNoTracking()
.Where(x => EF.Functions.ILike(x.Name, normalized));
// 3. 更新场景排除自身
if (excludeTenantId.HasValue)
{
query = query.Where(x => x.Id != excludeTenantId.Value);
}
// 4. 判断是否存在
return query.AnyAsync(cancellationToken);
}
/// <inheritdoc />
public Task<bool> ExistsByContactPhoneAsync(string phone, CancellationToken cancellationToken = default)
{
var normalized = phone.Trim();
return context.Tenants.AnyAsync(x => x.ContactPhone == normalized, cancellationToken);
}
/// <inheritdoc />
public Task<long?> FindTenantIdByContactPhoneAsync(string phone, CancellationToken cancellationToken = default)
{
// 1. 标准化手机号
var normalized = phone.Trim();
// 2. 查询租户 ID
return context.Tenants.AsNoTracking()
.Where(x => x.ContactPhone == normalized)
.Select(x => (long?)x.Id)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task<TenantVerificationProfile?> GetVerificationProfileAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantVerificationProfiles
.IgnoreQueryFilters()
.AsNoTracking()
.FirstOrDefaultAsync(x => x.DeletedAt == null && x.TenantId == tenantId, cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<TenantVerificationProfile>> GetVerificationProfilesAsync(
IReadOnlyCollection<long> tenantIds,
CancellationToken cancellationToken = default)
{
// 1. tenantIds 为空直接返回
if (tenantIds.Count == 0)
{
return Array.Empty<TenantVerificationProfile>();
}
// 2. 批量查询实名资料
return await context.TenantVerificationProfiles
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && tenantIds.Contains(x.TenantId))
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task UpsertVerificationProfileAsync(TenantVerificationProfile profile, CancellationToken cancellationToken = default)
{
// 1. 查询现有实名资料
var existing = await context.TenantVerificationProfiles
.IgnoreQueryFilters()
.FirstOrDefaultAsync(x => x.DeletedAt == null && x.TenantId == profile.TenantId, cancellationToken);
if (existing == null)
{
// 2. 不存在则新增
await context.TenantVerificationProfiles.AddAsync(profile, cancellationToken);
return;
}
// 3. 存在则更新当前值
profile.Id = existing.Id;
context.Entry(existing).CurrentValues.SetValues(profile);
}
/// <inheritdoc />
public Task<TenantReviewClaim?> GetActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantReviewClaims
.AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ReleasedAt == null)
.OrderByDescending(x => x.ClaimedAt)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public Task<TenantReviewClaim?> FindActiveReviewClaimAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantReviewClaims
.Where(x => x.TenantId == tenantId && x.ReleasedAt == null)
.OrderByDescending(x => x.ClaimedAt)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<bool> TryAddReviewClaimAsync(
TenantReviewClaim claim,
TenantAuditLog auditLog,
CancellationToken cancellationToken = default)
{
try
{
// 1. 写入领取记录
await context.TenantReviewClaims.AddAsync(claim, cancellationToken);
await context.SaveChangesAsync(cancellationToken);
// 2. 写入审计日志
await logsContext.TenantAuditLogs.AddAsync(auditLog, cancellationToken);
await logsContext.SaveChangesAsync(cancellationToken);
return true;
}
catch (DbUpdateException ex) when (ex.InnerException is PostgresException pg && pg.SqlState == PostgresErrorCodes.UniqueViolation)
{
// 1. 释放实体跟踪避免重复写入
context.Entry(claim).State = EntityState.Detached;
// 2. 返回抢占失败
return false;
}
}
/// <inheritdoc />
public Task UpdateReviewClaimAsync(TenantReviewClaim claim, CancellationToken cancellationToken = default)
{
context.TenantReviewClaims.Update(claim);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<TenantSubscription?> GetActiveSubscriptionAsync(long tenantId, CancellationToken cancellationToken = default)
{
return context.TenantSubscriptions
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
.OrderByDescending(x => x.EffectiveTo)
.FirstOrDefaultAsync(cancellationToken);
}
/// <inheritdoc />
public async Task<IReadOnlyList<TenantSubscription>> GetSubscriptionsAsync(
IReadOnlyCollection<long> tenantIds,
CancellationToken cancellationToken = default)
{
// 1. tenantIds 为空直接返回
if (tenantIds.Count == 0)
{
return Array.Empty<TenantSubscription>();
}
// 2. 批量查询订阅数据
return await context.TenantSubscriptions
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && tenantIds.Contains(x.TenantId))
.OrderByDescending(x => x.EffectiveTo)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public Task<TenantSubscription?> FindSubscriptionByIdAsync(long tenantId, long subscriptionId, CancellationToken cancellationToken = default)
{
return context.TenantSubscriptions
.IgnoreQueryFilters()
.FirstOrDefaultAsync(
x => x.DeletedAt == null && x.TenantId == tenantId && x.Id == subscriptionId,
cancellationToken);
}
/// <inheritdoc />
public Task AddSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default)
{
return context.TenantSubscriptions.AddAsync(subscription, cancellationToken).AsTask();
}
/// <inheritdoc />
public Task UpdateSubscriptionAsync(TenantSubscription subscription, CancellationToken cancellationToken = default)
{
context.TenantSubscriptions.Update(subscription);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task AddSubscriptionHistoryAsync(TenantSubscriptionHistory history, CancellationToken cancellationToken = default)
{
return context.TenantSubscriptionHistories.AddAsync(history, cancellationToken).AsTask();
}
/// <inheritdoc />
public async Task<IReadOnlyList<TenantSubscriptionHistory>> GetSubscriptionHistoryAsync(long tenantId, CancellationToken cancellationToken = default)
{
return await context.TenantSubscriptionHistories
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
.OrderByDescending(x => x.EffectiveFrom)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public Task AddAuditLogAsync(TenantAuditLog log, CancellationToken cancellationToken = default)
{
return logsContext.TenantAuditLogs.AddAsync(log, cancellationToken).AsTask();
}
/// <inheritdoc />
public async Task<IReadOnlyList<TenantAuditLog>> GetAuditLogsAsync(long tenantId, CancellationToken cancellationToken = default)
{
return await logsContext.TenantAuditLogs
.IgnoreQueryFilters()
.AsNoTracking()
.Where(x => x.DeletedAt == null && x.TenantId == tenantId)
.OrderByDescending(x => x.CreatedAt)
.ToListAsync(cancellationToken);
}
/// <inheritdoc />
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
// 1. 保存业务库变更
await context.SaveChangesAsync(cancellationToken);
// 2. 保存日志库变更
await logsContext.SaveChangesAsync(cancellationToken);
}
} }

View File

@@ -18,11 +18,6 @@ public sealed class TakeoutLogsDbContext(
IIdGenerator? idGenerator = null) IIdGenerator? idGenerator = null)
: AppDbContext(options, currentUserAccessor, idGenerator) : AppDbContext(options, currentUserAccessor, idGenerator)
{ {
/// <summary>
/// 租户审计日志集合。
/// </summary>
public DbSet<TenantAuditLog> TenantAuditLogs => Set<TenantAuditLog>();
/// <summary> /// <summary>
/// 商户审计日志集合。 /// 商户审计日志集合。
/// </summary> /// </summary>
@@ -55,7 +50,6 @@ public sealed class TakeoutLogsDbContext(
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
ConfigureTenantAuditLog(modelBuilder.Entity<TenantAuditLog>());
ConfigureMerchantAuditLog(modelBuilder.Entity<MerchantAuditLog>()); ConfigureMerchantAuditLog(modelBuilder.Entity<MerchantAuditLog>());
ConfigureMerchantChangeLog(modelBuilder.Entity<MerchantChangeLog>()); ConfigureMerchantChangeLog(modelBuilder.Entity<MerchantChangeLog>());
ConfigureOperationLog(modelBuilder.Entity<OperationLog>()); ConfigureOperationLog(modelBuilder.Entity<OperationLog>());
@@ -63,17 +57,6 @@ public sealed class TakeoutLogsDbContext(
ConfigureMemberGrowthLog(modelBuilder.Entity<MemberGrowthLog>()); ConfigureMemberGrowthLog(modelBuilder.Entity<MemberGrowthLog>());
} }
private static void ConfigureTenantAuditLog(EntityTypeBuilder<TenantAuditLog> builder)
{
builder.ToTable("tenant_audit_logs");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantId).IsRequired();
builder.Property(x => x.Title).HasMaxLength(128).IsRequired();
builder.Property(x => x.Description).HasMaxLength(1024);
builder.Property(x => x.OperatorName).HasMaxLength(64);
builder.HasIndex(x => x.TenantId);
}
private static void ConfigureMerchantAuditLog(EntityTypeBuilder<MerchantAuditLog> builder) private static void ConfigureMerchantAuditLog(EntityTypeBuilder<MerchantAuditLog> builder)
{ {
builder.ToTable("merchant_audit_logs"); builder.ToTable("merchant_audit_logs");