diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs
index d1e3398..c613598 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantPackageUsageDto.cs
@@ -24,5 +24,14 @@ public sealed class TenantPackageUsageDto
/// 历史总订阅记录数量(不含软删)。
///
public int TotalSubscriptionCount { get; init; }
-}
+ ///
+ /// MRR(Monthly Recurring Revenue)粗看:按“当前有效订阅数 × 套餐月付等效价”估算。
+ ///
+ public decimal Mrr { get; init; }
+
+ ///
+ /// ARR(Annual Recurring Revenue)粗看:按“当前有效订阅数 × 套餐年付等效价”估算。
+ ///
+ public decimal Arr { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs
index e3aa0cb..0eb2750 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/GetTenantPackageUsagesQueryHandler.cs
@@ -45,7 +45,9 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
TenantPackageId = reader.GetInt64(0),
ActiveSubscriptionCount = reader.GetInt32(1),
ActiveTenantCount = reader.GetInt32(2),
- TotalSubscriptionCount = reader.GetInt32(3)
+ TotalSubscriptionCount = reader.GetInt32(3),
+ Mrr = reader.IsDBNull(4) ? 0m : reader.GetDecimal(4),
+ Arr = reader.IsDBNull(5) ? 0m : reader.GetDecimal(5)
});
}
@@ -56,16 +58,17 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
private static string BuildSql(long[]? ids, out (string Name, object? Value)[] parameters, DateTime now)
{
- // 1. 基础查询
+ // 1. 基础查询:先按订阅表聚合,再回连套餐表计算 MRR/ARR
var builder = new System.Text.StringBuilder();
builder.AppendLine("""
- select
- "TenantPackageId" as "TenantPackageId",
- count(*) filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now) as "ActiveSubscriptionCount",
- count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now) as "ActiveTenantCount",
- count(*) as "TotalSubscriptionCount"
- from public.tenant_subscriptions
- where "DeletedAt" is null
+ with stats as (
+ select
+ "TenantPackageId" as "TenantPackageId",
+ count(*) filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now) as "ActiveSubscriptionCount",
+ count(distinct "TenantId") filter (where "Status" = 1 and "EffectiveFrom" <= @now and "EffectiveTo" >= @now) as "ActiveTenantCount",
+ count(*) as "TotalSubscriptionCount"
+ from public.tenant_subscriptions
+ where "DeletedAt" is null
""");
var list = new List<(string Name, object? Value)>
@@ -92,8 +95,20 @@ public sealed class GetTenantPackageUsagesQueryHandler(IDapperExecutor dapperExe
builder.AppendLine(")");
}
- // 3. (空行后) 分组
- builder.AppendLine("group by \"TenantPackageId\";");
+ // 3. (空行后) 分组与回连套餐表
+ builder.AppendLine("""
+ group by "TenantPackageId"
+ )
+ select
+ s."TenantPackageId" as "TenantPackageId",
+ s."ActiveSubscriptionCount" as "ActiveSubscriptionCount",
+ s."ActiveTenantCount" as "ActiveTenantCount",
+ s."TotalSubscriptionCount" as "TotalSubscriptionCount",
+ (s."ActiveSubscriptionCount"::numeric * coalesce(p."MonthlyPrice", (p."YearlyPrice" / 12.0), 0))::numeric(18, 2) as "Mrr",
+ (s."ActiveSubscriptionCount"::numeric * coalesce(p."YearlyPrice", (p."MonthlyPrice" * 12), 0))::numeric(18, 2) as "Arr"
+ from stats s
+ left join public.tenant_packages p on p."Id" = s."TenantPackageId" and p."DeletedAt" is null;
+ """);
parameters = list.ToArray();
return builder.ToString();