Files
TakeoutSaaS.AdminApi/src/Application/TakeoutSaaS.Application/App/Billings/Handlers/GetOverdueBillingsQueryHandler.cs
2026-01-04 21:22:26 +08:00

173 lines
6.4 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;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.Billings.Handlers;
/// <summary>
/// 查询逾期账单列表处理器。
/// </summary>
public sealed class GetOverdueBillingsQueryHandler(
IDapperExecutor dapperExecutor)
: IRequestHandler<GetOverdueBillingsQuery, PagedResult<BillingListDto>>
{
/// <summary>
/// 处理查询逾期账单列表请求。
/// </summary>
/// <param name="request">查询命令。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>分页逾期账单列表 DTO。</returns>
public async Task<PagedResult<BillingListDto>> Handle(GetOverdueBillingsQuery request, CancellationToken cancellationToken)
{
// 1. 参数规范化
var page = request.PageNumber <= 0 ? 1 : request.PageNumber;
var pageSize = request.PageSize is <= 0 or > 200 ? 20 : request.PageSize;
var offset = (page - 1) * pageSize;
var now = DateTime.UtcNow;
// 2. 查询总数 + 列表
return await dapperExecutor.QueryAsync(
DatabaseConstants.AppDataSource,
DatabaseConnectionRole.Read,
async (connection, token) =>
{
// 2.1 统计总数
var total = await ExecuteScalarIntAsync(
connection,
BuildCountSql(),
[
("now", now)
],
token);
// 2.2 查询列表
await using var listCommand = CreateCommand(
connection,
BuildListSql(),
[
("now", now),
("offset", offset),
("limit", pageSize)
]);
await using var reader = await listCommand.ExecuteReaderAsync(token);
var items = new List<BillingListDto>();
while (await reader.ReadAsync(token))
{
var dueDate = reader.GetDateTime(13);
var status = (TenantBillingStatus)reader.GetInt32(12);
var amountDue = reader.GetDecimal(8);
var discountAmount = reader.GetDecimal(9);
var taxAmount = reader.GetDecimal(10);
var totalAmount = amountDue - discountAmount + taxAmount;
var overdueDays = dueDate < now ? (int)(now - dueDate).TotalDays : 0;
items.Add(new BillingListDto
{
Id = reader.GetInt64(0),
TenantId = reader.GetInt64(1),
TenantName = reader.IsDBNull(2) ? string.Empty : reader.GetString(2),
SubscriptionId = reader.IsDBNull(3) ? null : reader.GetInt64(3),
StatementNo = reader.GetString(4),
BillingType = (BillingType)reader.GetInt32(5),
PeriodStart = reader.GetDateTime(6),
PeriodEnd = reader.GetDateTime(7),
AmountDue = amountDue,
DiscountAmount = discountAmount,
TaxAmount = taxAmount,
TotalAmount = totalAmount,
AmountPaid = reader.GetDecimal(11),
Status = status,
DueDate = dueDate,
Currency = reader.IsDBNull(14) ? "CNY" : reader.GetString(14),
IsOverdue = true,
OverdueDays = overdueDays,
CreatedAt = reader.GetDateTime(15),
UpdatedAt = reader.IsDBNull(16) ? null : reader.GetDateTime(16)
});
}
// 2.3 返回分页
return new PagedResult<BillingListDto>(items, page, pageSize, total);
},
cancellationToken);
}
private static string BuildCountSql()
{
return """
select count(*)
from public.tenant_billing_statements b
join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null
where b."DeletedAt" is null
and b."DueDate" < @now
and b."Status" in (0, 2);
""";
}
private static string BuildListSql()
{
return """
select
b."Id",
b."TenantId",
t."Name" as "TenantName",
b."SubscriptionId",
b."StatementNo",
b."BillingType",
b."PeriodStart",
b."PeriodEnd",
b."AmountDue",
b."DiscountAmount",
b."TaxAmount",
b."AmountPaid",
b."Status",
b."DueDate",
b."Currency",
b."CreatedAt",
b."UpdatedAt"
from public.tenant_billing_statements b
join public.tenants t on t."Id" = b."TenantId" and t."DeletedAt" is null
where b."DeletedAt" is null
and b."DueDate" < @now
and b."Status" in (0, 2)
order by b."DueDate" asc
offset @offset
limit @limit;
""";
}
private static async Task<int> ExecuteScalarIntAsync(
IDbConnection connection,
string sql,
(string Name, object? Value)[] parameters,
CancellationToken cancellationToken)
{
await using var command = CreateCommand(connection, sql, parameters);
var result = await command.ExecuteScalarAsync(cancellationToken);
return result is null or DBNull ? 0 : Convert.ToInt32(result);
}
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;
}
}