188 lines
7.8 KiB
C#
188 lines
7.8 KiB
C#
using MediatR;
|
|
using System.Data;
|
|
using System.Data.Common;
|
|
using TakeoutSaaS.Application.App.Billings.Dto;
|
|
using TakeoutSaaS.Application.App.Billings.Queries;
|
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
|
using TakeoutSaaS.Shared.Abstractions.Data;
|
|
|
|
namespace TakeoutSaaS.Application.App.Billings.Handlers;
|
|
|
|
/// <summary>
|
|
/// 查询账单统计数据处理器。
|
|
/// </summary>
|
|
public sealed class GetBillingStatisticsQueryHandler(
|
|
IDapperExecutor dapperExecutor)
|
|
: IRequestHandler<GetBillingStatisticsQuery, BillingStatisticsDto>
|
|
{
|
|
/// <summary>
|
|
/// 处理查询账单统计数据请求。
|
|
/// </summary>
|
|
/// <param name="request">查询命令。</param>
|
|
/// <param name="cancellationToken">取消标记。</param>
|
|
/// <returns>账单统计数据 DTO。</returns>
|
|
public async Task<BillingStatisticsDto> Handle(GetBillingStatisticsQuery request, CancellationToken cancellationToken)
|
|
{
|
|
// 1. 参数规范化
|
|
var startDate = request.StartDate ?? DateTime.UtcNow.AddMonths(-1);
|
|
var endDate = request.EndDate ?? DateTime.UtcNow;
|
|
var groupBy = NormalizeGroupBy(request.GroupBy);
|
|
|
|
// 2. 查询统计数据(总览 + 趋势)
|
|
return await dapperExecutor.QueryAsync(
|
|
DatabaseConstants.AppDataSource,
|
|
DatabaseConnectionRole.Read,
|
|
async (connection, token) =>
|
|
{
|
|
// 2.1 总览统计
|
|
await using var summaryCommand = CreateCommand(
|
|
connection,
|
|
BuildSummarySql(),
|
|
[
|
|
("tenantId", request.TenantId),
|
|
("startDate", startDate),
|
|
("endDate", endDate),
|
|
("now", DateTime.UtcNow)
|
|
]);
|
|
|
|
await using var summaryReader = await summaryCommand.ExecuteReaderAsync(token);
|
|
await summaryReader.ReadAsync(token);
|
|
|
|
var totalCount = summaryReader.IsDBNull(0) ? 0 : summaryReader.GetInt32(0);
|
|
var pendingCount = summaryReader.IsDBNull(1) ? 0 : summaryReader.GetInt32(1);
|
|
var paidCount = summaryReader.IsDBNull(2) ? 0 : summaryReader.GetInt32(2);
|
|
var overdueCount = summaryReader.IsDBNull(3) ? 0 : summaryReader.GetInt32(3);
|
|
var cancelledCount = summaryReader.IsDBNull(4) ? 0 : summaryReader.GetInt32(4);
|
|
var totalAmountDue = summaryReader.IsDBNull(5) ? 0m : summaryReader.GetDecimal(5);
|
|
var totalAmountPaid = summaryReader.IsDBNull(6) ? 0m : summaryReader.GetDecimal(6);
|
|
var totalAmountUnpaid = summaryReader.IsDBNull(7) ? 0m : summaryReader.GetDecimal(7);
|
|
var totalOverdueAmount = summaryReader.IsDBNull(8) ? 0m : summaryReader.GetDecimal(8);
|
|
|
|
// 2.2 趋势数据
|
|
await using var trendCommand = CreateCommand(
|
|
connection,
|
|
BuildTrendSql(groupBy),
|
|
[
|
|
("tenantId", request.TenantId),
|
|
("startDate", startDate),
|
|
("endDate", endDate)
|
|
]);
|
|
|
|
await using var trendReader = await trendCommand.ExecuteReaderAsync(token);
|
|
var amountDueTrend = new Dictionary<string, decimal>();
|
|
var amountPaidTrend = new Dictionary<string, decimal>();
|
|
var countTrend = new Dictionary<string, int>();
|
|
while (await trendReader.ReadAsync(token))
|
|
{
|
|
var bucket = trendReader.GetDateTime(0);
|
|
var key = bucket.ToString("yyyy-MM-dd");
|
|
|
|
amountDueTrend[key] = trendReader.IsDBNull(1) ? 0m : trendReader.GetDecimal(1);
|
|
amountPaidTrend[key] = trendReader.IsDBNull(2) ? 0m : trendReader.GetDecimal(2);
|
|
countTrend[key] = trendReader.IsDBNull(3) ? 0 : trendReader.GetInt32(3);
|
|
}
|
|
|
|
// 2.3 组装 DTO
|
|
return new BillingStatisticsDto
|
|
{
|
|
TenantId = request.TenantId,
|
|
StartDate = startDate,
|
|
EndDate = endDate,
|
|
GroupBy = groupBy,
|
|
TotalCount = totalCount,
|
|
PendingCount = pendingCount,
|
|
PaidCount = paidCount,
|
|
OverdueCount = overdueCount,
|
|
CancelledCount = cancelledCount,
|
|
TotalAmountDue = totalAmountDue,
|
|
TotalAmountPaid = totalAmountPaid,
|
|
TotalAmountUnpaid = totalAmountUnpaid,
|
|
TotalOverdueAmount = totalOverdueAmount,
|
|
AmountDueTrend = amountDueTrend,
|
|
AmountPaidTrend = amountPaidTrend,
|
|
CountTrend = countTrend
|
|
};
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
private static string NormalizeGroupBy(string? groupBy)
|
|
{
|
|
return groupBy?.Trim() switch
|
|
{
|
|
"Week" => "Week",
|
|
"Month" => "Month",
|
|
_ => "Day"
|
|
};
|
|
}
|
|
|
|
private static string BuildSummarySql()
|
|
{
|
|
return """
|
|
select
|
|
count(*)::int as "TotalCount",
|
|
coalesce(sum(case when b."Status" = 0 then 1 else 0 end), 0)::int as "PendingCount",
|
|
coalesce(sum(case when b."Status" = 1 then 1 else 0 end), 0)::int as "PaidCount",
|
|
coalesce(sum(case when b."Status" = 2 then 1 else 0 end), 0)::int as "OverdueCount",
|
|
coalesce(sum(case when b."Status" = 3 then 1 else 0 end), 0)::int as "CancelledCount",
|
|
coalesce(sum(b."AmountDue"), 0)::numeric as "TotalAmountDue",
|
|
coalesce(sum(b."AmountPaid"), 0)::numeric as "TotalAmountPaid",
|
|
coalesce(sum((b."AmountDue" - b."DiscountAmount" + b."TaxAmount") - b."AmountPaid"), 0)::numeric as "TotalAmountUnpaid",
|
|
coalesce(sum(
|
|
case
|
|
when b."Status" in (0, 2) and b."DueDate" < @now
|
|
then (b."AmountDue" - b."DiscountAmount" + b."TaxAmount") - b."AmountPaid"
|
|
else 0
|
|
end
|
|
), 0)::numeric as "TotalOverdueAmount"
|
|
from public.tenant_billing_statements b
|
|
where b."DeletedAt" is null
|
|
and (@tenantId::bigint is null or b."TenantId" = @tenantId)
|
|
and b."PeriodStart" >= @startDate
|
|
and b."PeriodEnd" <= @endDate;
|
|
""";
|
|
}
|
|
|
|
private static string BuildTrendSql(string groupBy)
|
|
{
|
|
var dateTrunc = groupBy switch
|
|
{
|
|
"Week" => "week",
|
|
"Month" => "month",
|
|
_ => "day"
|
|
};
|
|
|
|
return $"""
|
|
select
|
|
date_trunc('{dateTrunc}', b."PeriodStart") as "Bucket",
|
|
coalesce(sum(b."AmountDue"), 0)::numeric as "AmountDue",
|
|
coalesce(sum(b."AmountPaid"), 0)::numeric as "AmountPaid",
|
|
count(*)::int as "Count"
|
|
from public.tenant_billing_statements b
|
|
where b."DeletedAt" is null
|
|
and (@tenantId::bigint is null or b."TenantId" = @tenantId)
|
|
and b."PeriodStart" >= @startDate
|
|
and b."PeriodEnd" <= @endDate
|
|
group by 1
|
|
order by 1 asc;
|
|
""";
|
|
}
|
|
|
|
private static DbCommand CreateCommand(IDbConnection connection, string sql, (string Name, object? Value)[] parameters)
|
|
{
|
|
var command = connection.CreateCommand();
|
|
command.CommandText = sql;
|
|
|
|
foreach (var (name, value) in parameters)
|
|
{
|
|
var p = command.CreateParameter();
|
|
p.ParameterName = name;
|
|
p.Value = value ?? DBNull.Value;
|
|
command.Parameters.Add(p);
|
|
}
|
|
|
|
return (DbCommand)command;
|
|
}
|
|
}
|